From: Christian Heller Date: Mon, 16 Dec 2024 13:52:49 +0000 (+0100) Subject: Re-organize Player code. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/%7B%7Bdb.prefix%7D%7D/%7B%7B%20web_path%20%7D%7D/decks/processes?a=commitdiff_plain;h=4f84183acf8a73bd7e9c4ae57af2d1f508efa4bd;p=ytplom Re-organize Player code. --- diff --git a/src/ytplom/http.py b/src/ytplom/http.py index 4dbe651..165e5eb 100644 --- a/src/ytplom/http.py +++ b/src/ytplom/http.py @@ -161,7 +161,7 @@ class _TaskHandler(BaseHTTPRequestHandler): elif 'stop' == command: self.server.player.toggle_run() elif 'reload' == command: - self.server.player.reload() + self.server.player.load_files_and_start() elif command.startswith('jump_'): self.server.player.jump_to(int(command.split('_')[1])) elif command.startswith('up_'): @@ -416,7 +416,7 @@ class _TaskHandler(BaseHTTPRequestHandler): if 'playlist' in params.as_dict: data['playlist_files'] = [ {'rel_path': str(f.rel_path), 'digest': f.digest.b64} - for f in self.server.player.files] + for f in self.server.player.playlist] try: self.wfile.write( f'data: {json_dumps(data)}\n\n'.encode()) diff --git a/src/ytplom/misc.py b/src/ytplom/misc.py index 6cadf49..4f30cf5 100644 --- a/src/ytplom/misc.py +++ b/src/ytplom/misc.py @@ -501,25 +501,32 @@ class QuotaLog(DbData): 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 @@ -540,37 +547,33 @@ class Player: 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: @@ -581,35 +584,45 @@ class Player: @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: @@ -645,7 +658,7 @@ class Player: 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() @@ -654,35 +667,30 @@ class Player: 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) diff --git a/src/ytplom/primitives.py b/src/ytplom/primitives.py index ddc64d5..50de4ea 100644 --- a/src/ytplom/primitives.py +++ b/src/ytplom/primitives.py @@ -1,3 +1,4 @@ +"""Basic depended-ons not depending on anything else.""" from pathlib import Path