177 lines
5.4 KiB
Python
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()
|