From 6d9cfaf8bec55525fae9342be1f49e24546eca61 Mon Sep 17 00:00:00 2001 From: Uyanide Date: Thu, 9 Apr 2026 23:00:10 +0200 Subject: [PATCH] refactor: modules only need to know the config values they need to know --- pyproject.toml | 2 +- src/lrx_cli/cli.py | 4 ++-- src/lrx_cli/watch/control.py | 18 ++++-------------- src/lrx_cli/watch/fetcher.py | 9 ++++----- src/lrx_cli/watch/player.py | 20 +++++++------------- src/lrx_cli/watch/session.py | 8 ++++---- tests/test_watch.py | 12 +++++++----- 7 files changed, 29 insertions(+), 44 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 504b004..46192a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lrx-cli" -version = "0.6.5" +version = "0.7.0" description = "Fetch line-synced lyrics for your music player." readme = "README.md" requires-python = ">=3.13" diff --git a/src/lrx_cli/cli.py b/src/lrx_cli/cli.py index 74ec017..95c2f45 100644 --- a/src/lrx_cli/cli.py +++ b/src/lrx_cli/cli.py @@ -430,7 +430,7 @@ def offset(delta: str) -> None: logger.error(parse_error or "Invalid offset delta") sys.exit(1) - response = ControlClient(config=_app_config).send( + response = ControlClient(_app_config.watch.socket_path).send( {"cmd": "offset", "delta": parsed_delta} ) if not response.get("ok"): @@ -442,7 +442,7 @@ def offset(delta: str) -> None: @ctl_app.command def status() -> None: """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"): logger.error(response.get("error", "Unknown error")) sys.exit(1) diff --git a/src/lrx_cli/watch/control.py b/src/lrx_cli/watch/control.py index 63119c3..f9c4a0a 100644 --- a/src/lrx_cli/watch/control.py +++ b/src/lrx_cli/watch/control.py @@ -7,8 +7,6 @@ from typing import TYPE_CHECKING from loguru import logger -from ..config import AppConfig - if TYPE_CHECKING: from .session import WatchCoordinator @@ -19,13 +17,9 @@ class ControlServer: _socket_path: Path _server: asyncio.AbstractServer | None - def __init__( - self, - config: AppConfig, - socket_path: Path | None = None, - ) -> None: + def __init__(self, socket_path: str) -> None: """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 async def start(self, session: "WatchCoordinator") -> bool: @@ -107,13 +101,9 @@ class ControlClient: _socket_path: Path - def __init__( - self, - config: AppConfig, - socket_path: Path | None = None, - ) -> None: + def __init__(self, socket_path: str) -> None: """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]: """Send one JSON command to control server and return JSON response.""" diff --git a/src/lrx_cli/watch/fetcher.py b/src/lrx_cli/watch/fetcher.py index a3e1e3b..db6d3ed 100644 --- a/src/lrx_cli/watch/fetcher.py +++ b/src/lrx_cli/watch/fetcher.py @@ -3,7 +3,6 @@ import asyncio from typing import Awaitable, Callable, Optional -from ..config import AppConfig from ..lrc import LRCData from ..models import TrackMeta @@ -11,7 +10,7 @@ from ..models import TrackMeta class LyricFetcher: """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]]] _on_fetching: Callable[[], Awaitable[None] | None] _on_result: Callable[[Optional[LRCData]], Awaitable[None] | None] @@ -24,10 +23,10 @@ class LyricFetcher: fetch_func: Callable[[TrackMeta], Awaitable[Optional[LRCData]]], on_fetching: Callable[[], Awaitable[None] | None], on_result: Callable[[Optional[LRCData]], Awaitable[None] | None], - config: AppConfig, + watch_debounce_ms: int, ) -> None: """Initialize fetch callbacks and runtime options.""" - self._config = config + self._watch_debounce_ms = watch_debounce_ms self._fetch_func = fetch_func self._on_fetching = on_fetching self._on_result = on_result @@ -56,7 +55,7 @@ class LyricFetcher: async def _debounce_then_fetch(self) -> None: """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 if track is None: return diff --git a/src/lrx_cli/watch/player.py b/src/lrx_cli/watch/player.py index 7db79e3..85f9f12 100644 --- a/src/lrx_cli/watch/player.py +++ b/src/lrx_cli/watch/player.py @@ -9,7 +9,6 @@ from dbus_next.constants import BusType from dbus_next.message import Message from loguru import logger -from ..config import AppConfig from ..models import TrackMeta @@ -75,7 +74,7 @@ def _keyword_match(text: str, keyword: str) -> bool: class PlayerMonitor: """Tracks MPRIS players and forwards signal-driven state updates to session callbacks.""" - _config: AppConfig + _player_blacklist: tuple[str, ...] _on_players_changed: Callable[[], None] _on_seeked: Callable[[str, int], None] _on_playback_status: Callable[[str, str], None] @@ -89,17 +88,15 @@ class PlayerMonitor: on_players_changed: Callable[[], None], on_seeked: Callable[[str, int], None], on_playback_status: Callable[[str, str], None], - config: AppConfig, + player_blacklist: tuple[str, ...], target: Optional[PlayerTarget] = None, ) -> None: """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_seeked = on_seeked self._on_playback_status = on_playback_status - self._target = target or PlayerTarget( - player_blacklist=self._config.general.player_blacklist - ) + self._target = target or PlayerTarget(player_blacklist=self._player_blacklist) self.players: dict[str, PlayerState] = {} self._bus: MessageBus | None = None self._props_cache: dict[str, object] = {} @@ -183,10 +180,7 @@ class PlayerMonitor: for name in reply.body[0]: if not name.startswith("org.mpris.MediaPlayer2."): continue - if any( - x.lower() in name.lower() - for x in self._config.general.player_blacklist - ): + if any(x.lower() in name.lower() for x in self._player_blacklist): continue if not self._target.allows(name): continue @@ -389,7 +383,7 @@ class ActivePlayerSelector: def select( players: dict[str, PlayerState], last_active: str | None, - config: AppConfig, + preferred_player: str, ) -> str | None: """Select active player by playing state, preferred keyword, and continuity.""" if not players: @@ -399,7 +393,7 @@ class ActivePlayerSelector: if len(playing) == 1: return playing[0] - preferred = config.general.preferred_player.lower().strip() + preferred = preferred_player.lower().strip() candidates = playing if playing else list(players.keys()) if preferred: for name in candidates: diff --git a/src/lrx_cli/watch/session.py b/src/lrx_cli/watch/session.py index 7e0febe..ec0e0fc 100644 --- a/src/lrx_cli/watch/session.py +++ b/src/lrx_cli/watch/session.py @@ -126,12 +126,12 @@ class WatchCoordinator: 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( on_players_changed=self._on_player_change, on_seeked=self._on_seeked, on_playback_status=self._on_playback_status, - config=self._config, + player_blacklist=self._config.general.player_blacklist, target=self._target, ) self._tracker = PositionTracker( @@ -143,7 +143,7 @@ class WatchCoordinator: fetch_func=self._fetch_lyrics, on_fetching=self._on_fetching, on_result=self._on_lyrics_update, - config=self._config, + watch_debounce_ms=self._config.watch.debounce_ms, ) async def run(self) -> bool: @@ -234,7 +234,7 @@ class WatchCoordinator: selected = ActivePlayerSelector.select( self._player_monitor.players, self._model.active_player, - self._config, + self._config.general.preferred_player, ) self._model.active_player = selected diff --git a/tests/test_watch.py b/tests/test_watch.py index b639dea..8ff9e48 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -57,7 +57,7 @@ def test_active_player_selector_prefers_single_playing() -> None: ), } assert ( - ActivePlayerSelector.select(players, None, TEST_CONFIG) + ActivePlayerSelector.select(players, None, TEST_CONFIG.general.preferred_player) == "org.mpris.MediaPlayer2.bar" ) @@ -80,7 +80,7 @@ def test_active_player_selector_uses_last_active_when_no_playing() -> None: ActivePlayerSelector.select( players, "org.mpris.MediaPlayer2.bar", - TEST_CONFIG, + TEST_CONFIG.general.preferred_player, ) == "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"} socket_path = tmp_path / "watch.sock" - server = ControlServer(socket_path=socket_path, config=TEST_CONFIG) + server = ControlServer(socket_path=str(socket_path)) session = _Session() 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}) r2 = await client._send_async({"cmd": "status"}) await server.stop() @@ -320,7 +320,9 @@ def test_session_fetches_on_resume_playing_without_lyrics() -> None: async def _on_result(_lyrics) -> 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 = [] def request(self, track: TrackMeta) -> None: