class Player:
- """MPV representation with some additional features."""
+ """Feature-adding wrapper/manager of MPV and its playlist."""
_idx: int
+ last_update: DatetimeStr
def __init__(self,
whitelist_tags_display: TagSet,
whitelist_tags_prefilter: TagSet,
needed_tags_prefilter: TagSet,
) -> None:
- self.last_update = DatetimeStr('')
- self._mpv: Optional[MPV] = None
- self._kill_queue: Queue = Queue()
- self._monitoring_kill = False
+ # filters setup
self.filter_path = FilterStr('')
+ self.needed_tags = TagSet()
self._whitelist_tags_prefilter = whitelist_tags_prefilter
self._whitelist_tags_display = whitelist_tags_display
self._needed_tags_prefilter = needed_tags_prefilter
- self.needed_tags = TagSet()
+ # actual playlist and player setup
+ self._mpv: Optional[MPV] = None
+ self._monitoring_kill: bool = False
+ self._kill_queue: Queue = Queue()
+ self.playlist: list[VideoFile] = []
self.load_files_and_start()
+ def _signal_update(self) -> None:
+ """Update .last_update as signal player state has changed relevantly"""
+ self.last_update = DatetimeStr(datetime.now().strftime(TIMESTAMP_FMT))
+
def _monitor_kill(self) -> None:
"""Properly enforce mpv shutdown from direct interaction with mpv
client, as may happen with the "q" keystroke. If not for the handling
Thread(target=kill_on_queue_get, daemon=True).start()
- def load_files_and_start(self) -> None:
- """Collect filtered files into playlist, start player."""
- with DbConn() as conn:
- known_files = {
- f.full_path: f for f
- in VideoFile.get_filtered(
- conn,
- self.filter_path,
- self._needed_tags_prefilter,
- self.needed_tags,
- self._whitelist_tags_prefilter,
- self._whitelist_tags_display)}
- self.files = [known_files[p] for p in PATH_DOWNLOADS.iterdir()
- if p in known_files
- and p.is_file()
- and p.suffix[1:] in LEGAL_EXTENSIONS]
- shuffle(self.files)
- self._idx = 0
- self._start_mpv()
+ def _kill_mpv(self) -> None:
+ if self._mpv:
+ self._mpv.terminate()
+ self._mpv = None
+ self._signal_update()
- def _signal_update(self) -> None:
- self.last_update = DatetimeStr(datetime.now().strftime(TIMESTAMP_FMT))
+ def _play_at_index(self):
+ self._signal_update()
+ if self._mpv:
+ self._mpv.command('playlist-play-index', self._idx)
def _start_mpv(self) -> None:
+ """Start MPV at ._mpv and add some house-keeping and event handlers.
+
+ In detail:
+ - to properly enforce shutdowns even on direct client interaction, init
+ ._kill_queue and ._monitor_kill (see more thorough explanation there)
+ - bind changes to MPV's 'pause' property to ._signal_update
+ - build MPV's internal playlist from .playlist
+ - bind starting of files to ._signal_update and setting ._idx to MPV's
+ own playlist position index
+ - start playing
+ """
self._mpv = MPV(input_default_bindings=True,
input_vo_keyboard=True,
config=True)
self._monitor_kill()
- self._mpv.observe_property('pause', lambda a, b: self._signal_update())
- for path in [f.full_path for f in self.files]:
- self._mpv.command('loadfile', path, 'append')
@self._mpv.event_callback('start-file')
def on_start_file(_) -> None:
@self._mpv.event_callback('shutdown')
def on_shutdown(_) -> None:
- """To properly enforce shutdown even on direct client interaction,
- see self._monitor_kill for more thorough explanation.
- """
self._kill_queue.put(True)
+ self._mpv.observe_property('pause', lambda a, b: self._signal_update())
+ for path in [f.full_path for f in self.playlist]:
+ self._mpv.command('loadfile', path, 'append')
+ self._idx = 0
self._play_at_index()
- def _kill_mpv(self) -> None:
- if self._mpv:
- self._mpv.terminate()
- self._mpv = None
- self._signal_update()
-
- def _play_at_index(self):
- self._signal_update()
- if self._mpv:
- self._mpv.command('playlist-play-index', self._idx)
+ def load_files_and_start(self) -> None:
+ """Collect filtered files into playlist, shuffle, start player."""
+ with DbConn() as conn:
+ known_files = {
+ f.full_path: f for f
+ in VideoFile.get_filtered(
+ conn,
+ self.filter_path,
+ self._needed_tags_prefilter,
+ self.needed_tags,
+ self._whitelist_tags_prefilter,
+ self._whitelist_tags_display)}
+ self.playlist = [known_files[p] for p in PATH_DOWNLOADS.iterdir()
+ if p in known_files
+ and p.is_file()
+ and p.suffix[1:] in LEGAL_EXTENSIONS]
+ shuffle(self.playlist)
+ self._kill_mpv()
+ self._start_mpv()
@property
def empty(self) -> bool:
"""Return if playlist empty."""
- return 0 == len(self.files)
+ return 0 == len(self.playlist)
@property
def current_digest(self) -> Optional[Hash]:
"""Return hash digest ID of currently playing file."""
- if not self.files:
+ if not self.playlist:
return None
- return self.files[self._idx].digest
+ return self.playlist[self._idx].digest
@property
def is_running(self) -> bool:
def next(self) -> None:
"""Move player to next item in playlist."""
- if self._idx < len(self.files) - 1:
+ if self._idx < len(self.playlist) - 1:
self._idx += 1
self._play_at_index()
self._idx = target_idx
self._play_at_index()
- def move_entry(self, start_idx: int, upwards=True) -> None:
+ def move_entry(self, start_idx: int, upwards: bool = True) -> None:
"""Move playlist entry at start_idx up or down one step."""
if (start_idx == self._idx
or (upwards and start_idx == self._idx + 1)
or ((not upwards) and start_idx == self._idx - 1)
or (upwards and start_idx < 1)
- or ((not upwards) and start_idx > len(self.files) - 2)):
+ or ((not upwards) and start_idx > len(self.playlist) - 2)):
return
i0, i1 = start_idx, start_idx + (-1 if upwards else 1)
if self._mpv:
# NB: a functional playlist-move would do this in a single step,
# but for some reason I don't seem to get it to do anything
- path = self.files[i1].full_path
+ path = self.playlist[i1].full_path
self._mpv.command('playlist-remove', i1)
self._mpv.command('loadfile', path, 'insert-at', i0)
- self.files[i0], self.files[i1] = self.files[i1], self.files[i0]
+ self.playlist[i0], self.playlist[i1] = (self.playlist[i1],
+ self.playlist[i0])
self._signal_update()
- def reload(self) -> None:
- """Close MPV, empty filenames, restart."""
- self._kill_mpv()
- self.files.clear()
- self.load_files_and_start()
-
def inject_and_play(self, file: VideoFile) -> None:
"""Inject file after current title, then jump to it."""
- if self.files:
+ if self.playlist:
self._idx += 1
- self.files.insert(self._idx, file)
+ self.playlist.insert(self._idx, file)
if self._mpv:
self._mpv.command('loadfile', file.full_path,
'insert-at', self._idx)