refactor: modules only need to know the config values they need to know

This commit is contained in:
2026-04-09 23:00:10 +02:00
parent 66a32c751a
commit 6d9cfaf8be
7 changed files with 29 additions and 44 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lrx-cli" name = "lrx-cli"
version = "0.6.5" version = "0.7.0"
description = "Fetch line-synced lyrics for your music player." description = "Fetch line-synced lyrics for your music player."
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
+2 -2
View File
@@ -430,7 +430,7 @@ def offset(delta: str) -> None:
logger.error(parse_error or "Invalid offset delta") logger.error(parse_error or "Invalid offset delta")
sys.exit(1) sys.exit(1)
response = ControlClient(config=_app_config).send( response = ControlClient(_app_config.watch.socket_path).send(
{"cmd": "offset", "delta": parsed_delta} {"cmd": "offset", "delta": parsed_delta}
) )
if not response.get("ok"): if not response.get("ok"):
@@ -442,7 +442,7 @@ def offset(delta: str) -> None:
@ctl_app.command @ctl_app.command
def status() -> None: def status() -> None:
"""Print current watch session status as JSON.""" """Print current watch session status as JSON."""
response = ControlClient(config=_app_config).send({"cmd": "status"}) response = ControlClient(_app_config.watch.socket_path).send({"cmd": "status"})
if not response.get("ok"): if not response.get("ok"):
logger.error(response.get("error", "Unknown error")) logger.error(response.get("error", "Unknown error"))
sys.exit(1) sys.exit(1)
+4 -14
View File
@@ -7,8 +7,6 @@ from typing import TYPE_CHECKING
from loguru import logger from loguru import logger
from ..config import AppConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from .session import WatchCoordinator from .session import WatchCoordinator
@@ -19,13 +17,9 @@ class ControlServer:
_socket_path: Path _socket_path: Path
_server: asyncio.AbstractServer | None _server: asyncio.AbstractServer | None
def __init__( def __init__(self, socket_path: str) -> None:
self,
config: AppConfig,
socket_path: Path | None = None,
) -> None:
"""Initialize control server with socket path from config or explicit override.""" """Initialize control server with socket path from config or explicit override."""
self._socket_path: Path = socket_path or Path(config.watch.socket_path) self._socket_path = Path(socket_path)
self._server: asyncio.AbstractServer | None = None self._server: asyncio.AbstractServer | None = None
async def start(self, session: "WatchCoordinator") -> bool: async def start(self, session: "WatchCoordinator") -> bool:
@@ -107,13 +101,9 @@ class ControlClient:
_socket_path: Path _socket_path: Path
def __init__( def __init__(self, socket_path: str) -> None:
self,
config: AppConfig,
socket_path: Path | None = None,
) -> None:
"""Initialize control client with socket path from config or explicit override.""" """Initialize control client with socket path from config or explicit override."""
self._socket_path: Path = socket_path or Path(config.watch.socket_path) self._socket_path = Path(socket_path)
async def _send_async(self, cmd: dict[str, object]) -> dict[str, object]: async def _send_async(self, cmd: dict[str, object]) -> dict[str, object]:
"""Send one JSON command to control server and return JSON response.""" """Send one JSON command to control server and return JSON response."""
+4 -5
View File
@@ -3,7 +3,6 @@
import asyncio import asyncio
from typing import Awaitable, Callable, Optional from typing import Awaitable, Callable, Optional
from ..config import AppConfig
from ..lrc import LRCData from ..lrc import LRCData
from ..models import TrackMeta from ..models import TrackMeta
@@ -11,7 +10,7 @@ from ..models import TrackMeta
class LyricFetcher: class LyricFetcher:
"""Debounces track updates and runs at most one lyric fetch task at a time.""" """Debounces track updates and runs at most one lyric fetch task at a time."""
_config: AppConfig _watch_debounce_ms: int
_fetch_func: Callable[[TrackMeta], Awaitable[Optional[LRCData]]] _fetch_func: Callable[[TrackMeta], Awaitable[Optional[LRCData]]]
_on_fetching: Callable[[], Awaitable[None] | None] _on_fetching: Callable[[], Awaitable[None] | None]
_on_result: Callable[[Optional[LRCData]], Awaitable[None] | None] _on_result: Callable[[Optional[LRCData]], Awaitable[None] | None]
@@ -24,10 +23,10 @@ class LyricFetcher:
fetch_func: Callable[[TrackMeta], Awaitable[Optional[LRCData]]], fetch_func: Callable[[TrackMeta], Awaitable[Optional[LRCData]]],
on_fetching: Callable[[], Awaitable[None] | None], on_fetching: Callable[[], Awaitable[None] | None],
on_result: Callable[[Optional[LRCData]], Awaitable[None] | None], on_result: Callable[[Optional[LRCData]], Awaitable[None] | None],
config: AppConfig, watch_debounce_ms: int,
) -> None: ) -> None:
"""Initialize fetch callbacks and runtime options.""" """Initialize fetch callbacks and runtime options."""
self._config = config self._watch_debounce_ms = watch_debounce_ms
self._fetch_func = fetch_func self._fetch_func = fetch_func
self._on_fetching = on_fetching self._on_fetching = on_fetching
self._on_result = on_result self._on_result = on_result
@@ -56,7 +55,7 @@ class LyricFetcher:
async def _debounce_then_fetch(self) -> None: async def _debounce_then_fetch(self) -> None:
"""Wait debounce window then start a fresh fetch task for latest pending track.""" """Wait debounce window then start a fresh fetch task for latest pending track."""
await asyncio.sleep(self._config.watch.debounce_ms / 1000.0) await asyncio.sleep(self._watch_debounce_ms / 1000.0)
track = self._pending_track track = self._pending_track
if track is None: if track is None:
return return
+7 -13
View File
@@ -9,7 +9,6 @@ from dbus_next.constants import BusType
from dbus_next.message import Message from dbus_next.message import Message
from loguru import logger from loguru import logger
from ..config import AppConfig
from ..models import TrackMeta from ..models import TrackMeta
@@ -75,7 +74,7 @@ def _keyword_match(text: str, keyword: str) -> bool:
class PlayerMonitor: class PlayerMonitor:
"""Tracks MPRIS players and forwards signal-driven state updates to session callbacks.""" """Tracks MPRIS players and forwards signal-driven state updates to session callbacks."""
_config: AppConfig _player_blacklist: tuple[str, ...]
_on_players_changed: Callable[[], None] _on_players_changed: Callable[[], None]
_on_seeked: Callable[[str, int], None] _on_seeked: Callable[[str, int], None]
_on_playback_status: Callable[[str, str], None] _on_playback_status: Callable[[str, str], None]
@@ -89,17 +88,15 @@ class PlayerMonitor:
on_players_changed: Callable[[], None], on_players_changed: Callable[[], None],
on_seeked: Callable[[str, int], None], on_seeked: Callable[[str, int], None],
on_playback_status: Callable[[str, str], None], on_playback_status: Callable[[str, str], None],
config: AppConfig, player_blacklist: tuple[str, ...],
target: Optional[PlayerTarget] = None, target: Optional[PlayerTarget] = None,
) -> None: ) -> None:
"""Initialize monitor callbacks, runtime options, and player target filter.""" """Initialize monitor callbacks, runtime options, and player target filter."""
self._config = config self._player_blacklist = player_blacklist
self._on_players_changed = on_players_changed self._on_players_changed = on_players_changed
self._on_seeked = on_seeked self._on_seeked = on_seeked
self._on_playback_status = on_playback_status self._on_playback_status = on_playback_status
self._target = target or PlayerTarget( self._target = target or PlayerTarget(player_blacklist=self._player_blacklist)
player_blacklist=self._config.general.player_blacklist
)
self.players: dict[str, PlayerState] = {} self.players: dict[str, PlayerState] = {}
self._bus: MessageBus | None = None self._bus: MessageBus | None = None
self._props_cache: dict[str, object] = {} self._props_cache: dict[str, object] = {}
@@ -183,10 +180,7 @@ class PlayerMonitor:
for name in reply.body[0]: for name in reply.body[0]:
if not name.startswith("org.mpris.MediaPlayer2."): if not name.startswith("org.mpris.MediaPlayer2."):
continue continue
if any( if any(x.lower() in name.lower() for x in self._player_blacklist):
x.lower() in name.lower()
for x in self._config.general.player_blacklist
):
continue continue
if not self._target.allows(name): if not self._target.allows(name):
continue continue
@@ -389,7 +383,7 @@ class ActivePlayerSelector:
def select( def select(
players: dict[str, PlayerState], players: dict[str, PlayerState],
last_active: str | None, last_active: str | None,
config: AppConfig, preferred_player: str,
) -> str | None: ) -> str | None:
"""Select active player by playing state, preferred keyword, and continuity.""" """Select active player by playing state, preferred keyword, and continuity."""
if not players: if not players:
@@ -399,7 +393,7 @@ class ActivePlayerSelector:
if len(playing) == 1: if len(playing) == 1:
return playing[0] return playing[0]
preferred = config.general.preferred_player.lower().strip() preferred = preferred_player.lower().strip()
candidates = playing if playing else list(players.keys()) candidates = playing if playing else list(players.keys())
if preferred: if preferred:
for name in candidates: for name in candidates:
+4 -4
View File
@@ -126,12 +126,12 @@ class WatchCoordinator:
player_blacklist=self._config.general.player_blacklist, player_blacklist=self._config.general.player_blacklist,
) )
self._control = ControlServer(config=self._config) self._control = ControlServer(socket_path=config.watch.socket_path)
self._player_monitor = PlayerMonitor( self._player_monitor = PlayerMonitor(
on_players_changed=self._on_player_change, on_players_changed=self._on_player_change,
on_seeked=self._on_seeked, on_seeked=self._on_seeked,
on_playback_status=self._on_playback_status, on_playback_status=self._on_playback_status,
config=self._config, player_blacklist=self._config.general.player_blacklist,
target=self._target, target=self._target,
) )
self._tracker = PositionTracker( self._tracker = PositionTracker(
@@ -143,7 +143,7 @@ class WatchCoordinator:
fetch_func=self._fetch_lyrics, fetch_func=self._fetch_lyrics,
on_fetching=self._on_fetching, on_fetching=self._on_fetching,
on_result=self._on_lyrics_update, on_result=self._on_lyrics_update,
config=self._config, watch_debounce_ms=self._config.watch.debounce_ms,
) )
async def run(self) -> bool: async def run(self) -> bool:
@@ -234,7 +234,7 @@ class WatchCoordinator:
selected = ActivePlayerSelector.select( selected = ActivePlayerSelector.select(
self._player_monitor.players, self._player_monitor.players,
self._model.active_player, self._model.active_player,
self._config, self._config.general.preferred_player,
) )
self._model.active_player = selected self._model.active_player = selected
+7 -5
View File
@@ -57,7 +57,7 @@ def test_active_player_selector_prefers_single_playing() -> None:
), ),
} }
assert ( assert (
ActivePlayerSelector.select(players, None, TEST_CONFIG) ActivePlayerSelector.select(players, None, TEST_CONFIG.general.preferred_player)
== "org.mpris.MediaPlayer2.bar" == "org.mpris.MediaPlayer2.bar"
) )
@@ -80,7 +80,7 @@ def test_active_player_selector_uses_last_active_when_no_playing() -> None:
ActivePlayerSelector.select( ActivePlayerSelector.select(
players, players,
"org.mpris.MediaPlayer2.bar", "org.mpris.MediaPlayer2.bar",
TEST_CONFIG, TEST_CONFIG.general.preferred_player,
) )
== "org.mpris.MediaPlayer2.bar" == "org.mpris.MediaPlayer2.bar"
) )
@@ -182,11 +182,11 @@ def test_control_server_and_client_roundtrip(tmp_path: Path) -> None:
return {"ok": True, "offset_ms": self.offset, "lyrics_status": "idle"} return {"ok": True, "offset_ms": self.offset, "lyrics_status": "idle"}
socket_path = tmp_path / "watch.sock" socket_path = tmp_path / "watch.sock"
server = ControlServer(socket_path=socket_path, config=TEST_CONFIG) server = ControlServer(socket_path=str(socket_path))
session = _Session() session = _Session()
await server.start(session) # type: ignore await server.start(session) # type: ignore
client = ControlClient(socket_path=socket_path, config=TEST_CONFIG) client = ControlClient(socket_path=str(socket_path))
r1 = await client._send_async({"cmd": "offset", "delta": 200}) r1 = await client._send_async({"cmd": "offset", "delta": 200})
r2 = await client._send_async({"cmd": "status"}) r2 = await client._send_async({"cmd": "status"})
await server.stop() await server.stop()
@@ -320,7 +320,9 @@ def test_session_fetches_on_resume_playing_without_lyrics() -> None:
async def _on_result(_lyrics) -> None: async def _on_result(_lyrics) -> None:
return None return None
super().__init__(_fetch, _on_fetching, _on_result, TEST_CONFIG) super().__init__(
_fetch, _on_fetching, _on_result, TEST_CONFIG.watch.debounce_ms
)
self.requested = [] self.requested = []
def request(self, track: TrackMeta) -> None: def request(self, track: TrackMeta) -> None: