refactor: modules only need to know the config values they need to know
This commit is contained in:
+1
-1
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user