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]
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"
+2 -2
View File
@@ -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)
+4 -14
View File
@@ -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."""
+4 -5
View File
@@ -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
+7 -13
View File
@@ -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:
+4 -4
View File
@@ -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
+7 -5
View File
@@ -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: