feat: watch mode fetch immediatly on track changes regardless of player status
This commit is contained in:
+141
-55
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from lrx_cli.lrc import LRCData
|
||||
from lrx_cli.models import TrackMeta
|
||||
from lrx_cli.watch.control import ControlClient, ControlServer, parse_delta
|
||||
from lrx_cli.watch.view import BaseOutput, LyricView, WatchState
|
||||
from lrx_cli.watch.view import BaseOutput, LyricView, WatchState, WatchStatus
|
||||
from lrx_cli.watch.view.pipe import PipeOutput
|
||||
from lrx_cli.watch.player import ActivePlayerSelector, PlayerState, PlayerTarget
|
||||
from lrx_cli.watch.fetcher import LyricFetcher
|
||||
@@ -299,62 +299,148 @@ def test_pipe_output_repeated_text_uses_correct_timed_occurrence(capsys) -> None
|
||||
assert printed == "B\nX\nC\n"
|
||||
|
||||
|
||||
def test_session_fetches_on_resume_playing_without_lyrics() -> None:
|
||||
# ── WatchCoordinator state machine ───────────────────────────────────────────
|
||||
|
||||
|
||||
class _CaptureFetcher:
|
||||
"""Records fetch requests without doing real network calls."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.requested: list[str] = []
|
||||
|
||||
def request(self, track: TrackMeta) -> None:
|
||||
self.requested.append(track.display_name())
|
||||
|
||||
async def stop(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _make_coordinator() -> WatchCoordinator:
|
||||
class _Manager:
|
||||
def fetch_for_track(self, *_a, **_kw):
|
||||
return None
|
||||
|
||||
class _Output(BaseOutput):
|
||||
async def on_state(self, state: WatchState) -> None:
|
||||
pass
|
||||
|
||||
session = WatchCoordinator(
|
||||
_Manager(), # type: ignore
|
||||
_Output(),
|
||||
player_hint=None,
|
||||
config=TEST_CONFIG,
|
||||
)
|
||||
session._tracker = PositionTracker(
|
||||
lambda _bus: asyncio.sleep(0, result=0),
|
||||
TEST_CONFIG,
|
||||
)
|
||||
return session
|
||||
|
||||
|
||||
BUS = "org.mpris.MediaPlayer2.spotify"
|
||||
|
||||
|
||||
def _pstate(status: str = "Playing", title: str = "Song") -> PlayerState:
|
||||
return PlayerState(
|
||||
bus_name=BUS,
|
||||
status=status,
|
||||
track=TrackMeta(title=title, artist="Artist"),
|
||||
)
|
||||
|
||||
|
||||
def test_coordinator_fetches_on_initial_player() -> None:
|
||||
async def _run() -> None:
|
||||
class _Manager:
|
||||
def fetch_for_track(self, *_args, **_kwargs):
|
||||
return None
|
||||
session = _make_coordinator()
|
||||
fetcher = _CaptureFetcher()
|
||||
session._fetcher = fetcher # type: ignore[assignment]
|
||||
|
||||
class _Output(BaseOutput):
|
||||
async def on_state(self, state: WatchState) -> None:
|
||||
return None
|
||||
|
||||
class _Fetcher(LyricFetcher):
|
||||
def __init__(self):
|
||||
async def _fetch(_track: TrackMeta):
|
||||
return None
|
||||
|
||||
async def _on_fetching() -> None:
|
||||
return None
|
||||
|
||||
async def _on_result(_lyrics) -> None:
|
||||
return None
|
||||
|
||||
super().__init__(
|
||||
_fetch, _on_fetching, _on_result, TEST_CONFIG.watch.debounce_ms
|
||||
)
|
||||
self.requested = []
|
||||
|
||||
def request(self, track: TrackMeta) -> None:
|
||||
self.requested.append(track.display_name())
|
||||
|
||||
session = WatchCoordinator(
|
||||
_Manager(), # type: ignore
|
||||
_Output(),
|
||||
player_hint=None,
|
||||
config=TEST_CONFIG,
|
||||
)
|
||||
fake_fetcher = _Fetcher()
|
||||
session._fetcher = fake_fetcher
|
||||
session._tracker = PositionTracker(
|
||||
lambda _bus: asyncio.sleep(0, result=0),
|
||||
TEST_CONFIG,
|
||||
)
|
||||
|
||||
bus_name = "org.mpris.MediaPlayer2.spotify"
|
||||
track = TrackMeta(title="Song", artist="Artist")
|
||||
session._model.active_player = bus_name
|
||||
session._player_monitor.players = {
|
||||
bus_name: PlayerState(bus_name=bus_name, status="Playing", track=track)
|
||||
}
|
||||
session._model.set_lyrics(None)
|
||||
session._model.status = "paused"
|
||||
|
||||
session._on_playback_status(bus_name, "Playing")
|
||||
session._player_monitor.players = {BUS: _pstate("Playing")}
|
||||
session._on_player_change()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert fake_fetcher.requested == ["Artist - Song"]
|
||||
assert session._model.status == "fetching"
|
||||
assert fetcher.requested == ["Artist - Song"]
|
||||
assert session._model.status == WatchStatus.FETCHING
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_coordinator_fetches_while_paused() -> None:
|
||||
"""Fetch is triggered immediately even when player is paused."""
|
||||
|
||||
async def _run() -> None:
|
||||
session = _make_coordinator()
|
||||
fetcher = _CaptureFetcher()
|
||||
session._fetcher = fetcher # type: ignore[assignment]
|
||||
|
||||
session._player_monitor.players = {BUS: _pstate("Paused")}
|
||||
session._on_player_change()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert fetcher.requested == ["Artist - Song"]
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_coordinator_fetches_on_track_change() -> None:
|
||||
async def _run() -> None:
|
||||
session = _make_coordinator()
|
||||
session._model.active_player = BUS
|
||||
session._model.active_track_key = "Artist - Old Song"
|
||||
session._model.set_lyrics(LRCData("[00:01.00]old"))
|
||||
session._model.status = WatchStatus.OK
|
||||
|
||||
fetcher = _CaptureFetcher()
|
||||
session._fetcher = fetcher # type: ignore[assignment]
|
||||
|
||||
session._player_monitor.players = {BUS: _pstate("Playing", title="New Song")}
|
||||
session._on_player_change()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert fetcher.requested == ["Artist - New Song"]
|
||||
assert session._model.lyrics is None # cleared on track change
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_coordinator_no_refetch_on_calibration_no_lyrics() -> None:
|
||||
"""Calibration with same player/track and no_lyrics must NOT trigger a second fetch."""
|
||||
|
||||
async def _run() -> None:
|
||||
session = _make_coordinator()
|
||||
fetcher = _CaptureFetcher()
|
||||
session._fetcher = fetcher # type: ignore[assignment]
|
||||
|
||||
session._player_monitor.players = {BUS: _pstate("Playing")}
|
||||
session._on_player_change() # first call: player appears → fetch
|
||||
await asyncio.sleep(0)
|
||||
assert len(fetcher.requested) == 1
|
||||
|
||||
session._model.status = WatchStatus.NO_LYRICS # simulate fetch returned nothing
|
||||
|
||||
session._on_player_change() # calibration: same player/track
|
||||
await asyncio.sleep(0)
|
||||
assert len(fetcher.requested) == 1 # no second fetch
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_coordinator_no_fetch_when_lyrics_present() -> None:
|
||||
async def _run() -> None:
|
||||
session = _make_coordinator()
|
||||
session._model.active_player = BUS
|
||||
session._model.active_track_key = "Artist - Song"
|
||||
session._model.set_lyrics(LRCData("[00:01.00]line"))
|
||||
session._model.status = WatchStatus.OK
|
||||
|
||||
fetcher = _CaptureFetcher()
|
||||
session._fetcher = fetcher # type: ignore[assignment]
|
||||
|
||||
session._player_monitor.players = {BUS: _pstate("Playing")}
|
||||
session._on_player_change()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert fetcher.requested == []
|
||||
assert session._model.status == WatchStatus.OK
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
@@ -391,7 +477,7 @@ def test_session_emit_state_only_when_lyric_cursor_changes() -> None:
|
||||
bus_name: PlayerState(bus_name=bus_name, status="Playing", track=track)
|
||||
}
|
||||
session._model.set_lyrics(LRCData("[00:01.00]a\n[00:03.00]b"))
|
||||
session._model.status = "ok"
|
||||
session._model.status = WatchStatus.OK
|
||||
await session._tracker.set_active_player(
|
||||
bus_name,
|
||||
"Playing",
|
||||
@@ -441,7 +527,7 @@ def test_session_emits_when_crossing_first_timestamp() -> None:
|
||||
bus_name: PlayerState(bus_name=bus_name, status="Playing", track=track)
|
||||
}
|
||||
session._model.set_lyrics(LRCData("[00:02.00]a\n[00:03.00]b"))
|
||||
session._model.status = "ok"
|
||||
session._model.status = WatchStatus.OK
|
||||
await session._tracker.set_active_player(
|
||||
bus_name,
|
||||
"Playing",
|
||||
|
||||
Reference in New Issue
Block a user