Files
musicmouse/python-backend/musicmouse/simulator/fake_player.py
2026-08-27 23:46:19 +02:00

177 lines
5.4 KiB
Python

"""A player that behaves like :class:`~musicmouse.devices.player.VlcPlayer` without VLC.
Tracks advance on the injected :class:`~musicmouse.clock.Clock`, so under ``FakeClock``
a three-minute playlist plays out in microseconds and under ``RealClock`` you can watch
it tick along in the interactive simulator.
"""
from __future__ import annotations
import asyncio
import contextlib
import logging
from musicmouse.bus import EventBus
from musicmouse.clock import Clock, RealClock
from musicmouse.devices.player import PlayerBase
from musicmouse.media import Playlist
_log = logging.getLogger(__name__)
__all__ = ["FakePlayer"]
DEFAULT_TRACK_DURATION = 5.0
class FakePlayer(PlayerBase):
def __init__(
self,
bus: EventBus,
*,
min_volume: int = 0,
max_volume: int = 100,
initial_volume: int = 50,
track_duration: float = DEFAULT_TRACK_DURATION,
clock: Clock | None = None,
) -> None:
super().__init__(
bus, min_volume=min_volume, max_volume=max_volume, initial_volume=initial_volume
)
self._clock = clock or RealClock()
self.track_duration = track_duration
self._remaining = track_duration
self._started_at: float | None = None
self._timer: asyncio.Task[None] | None = None
self.closed = False
# -------------------------------------------------------------------- state
@property
def position(self) -> float:
"""Derived from the clock, so it is exact under ``FakeClock`` too."""
remaining = self._remaining
if self._started_at is not None:
remaining = max(0.0, remaining - (self._clock.now() - self._started_at))
return max(0.0, self.track_duration - remaining)
@property
def duration(self) -> float:
return self.track_duration if self._playlist else 0.0
# ------------------------------------------------------------------ actions
def set_playlist(self, playlist: Playlist) -> None:
self._cancel_timer()
self._load_playlist(playlist)
self._remaining = self.track_duration
_log.info("Playlist %r loaded (%d tracks)", playlist.name, len(playlist))
def play(self) -> None:
if self._playlist is None or not self._playlist:
_log.warning("Nothing to play: the playlist is empty")
return
if self._playing:
return
self._set_playing(True)
self._start_timer()
def play_from_start(self) -> None:
self.play_track(0)
def play_track(self, index: int) -> None:
if self._playlist is None or not self._playlist:
_log.warning("Nothing to play: the playlist is empty")
return
self._cancel_timer()
self._set_index(max(0, min(index, len(self._playlist) - 1)))
self._remaining = self.track_duration
self._set_playing(True)
self._start_timer()
def pause(self) -> None:
if not self._playing:
return
self._freeze()
self._set_playing(False)
def stop(self) -> None:
self._cancel_timer()
self._remaining = self.track_duration
self._set_playing(False)
def next_track(self) -> None:
self._skip(1)
def previous_track(self) -> None:
self._skip(-1)
def seek(self, position: float) -> None:
if self._playlist is None:
return
self._remaining = max(0.0, self.track_duration - max(0.0, position))
if self._playing:
self._start_timer()
async def run(self) -> None:
return
def close(self) -> None:
self._cancel_timer()
self.closed = True
# ---------------------------------------------------------------- internals
def _skip(self, offset: int) -> None:
if self._playlist is None or not self._playlist:
return
target = self._index + offset
if target < 0:
target = 0
if target >= len(self._playlist):
self._finish()
return
self._cancel_timer()
self._set_index(target)
self._remaining = self.track_duration
if self._playing:
self._start_timer()
def _finish(self) -> None:
self._cancel_timer()
self._remaining = self.track_duration
self._set_playing(False)
self._announce_playlist_finished()
def _start_timer(self) -> None:
self._cancel_timer()
self._started_at = self._clock.now()
self._timer = asyncio.create_task(self._await_track_end(), name="fake-player-track")
def _cancel_timer(self) -> None:
if self._timer is not None:
self._timer.cancel()
self._timer = None
self._started_at = None
def _freeze(self) -> None:
if self._started_at is not None:
self._remaining = max(0.0, self._remaining - (self._clock.now() - self._started_at))
self._cancel_timer()
async def _await_track_end(self) -> None:
with contextlib.suppress(asyncio.CancelledError):
await self._clock.sleep(self._remaining)
self._timer = None
self._started_at = None
self._on_track_end()
def _on_track_end(self) -> None:
assert self._playlist is not None
if self._index + 1 < len(self._playlist):
self._set_index(self._index + 1)
self._remaining = self.track_duration
self._start_timer()
else:
self._finish()