From 82df48e33682ae88f5b33dd0cafd0611e6caeb68 Mon Sep 17 00:00:00 2001 From: Christian Heller <c.heller@plomlompom.de> Date: Tue, 11 Mar 2025 09:26:21 +0100 Subject: [PATCH] Display/estimate current in-file playback position in player interface. --- src/templates/_base.tmpl | 30 +++++++++++++++++++++++++++--- src/ytplom/http.py | 6 +++++- src/ytplom/misc.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/templates/_base.tmpl b/src/templates/_base.tmpl index 56e5df5..8764a5e 100644 --- a/src/templates/_base.tmpl +++ b/src/templates/_base.tmpl @@ -4,6 +4,7 @@ <head> <meta charset="UTF-8"> <script> +const MS_IN_S = 1000; const RETRY_INTERVAL_S = 5; const PATH_EVENTS = "/{{page_names.events}}"; const PATH_PLAYER = "/{{page_names.player}}"; @@ -59,7 +60,7 @@ function connect_events() { events_stream.close(); if (while_connecting) { console.log("Error seemed connection-related, trying reconnect."); - setTimeout(connect_events, RETRY_INTERVAL_S * 1000); + setTimeout(connect_events, RETRY_INTERVAL_S * MS_IN_S); } else { console.log("Error does not seem connection-related, therefore aborting."); } @@ -103,6 +104,8 @@ function add_tag_links_to(parent_element, tags) { }); } +var timestamp_interval = null; + event_handlers.player = function(data) { const div = document.getElementById("player_controls"); div.innerHTML = ""; @@ -110,12 +113,33 @@ event_handlers.player = function(data) { add_player_btn_to(div, "next", "next", !data.can_play); add_player_btn_to(div, data.is_playing ? "pause" : "play", "play", !data.can_play); add_text_to(div, " · "); - add_text_to(div, data.is_running ? (data.is_playing ? "playing:" : "paused:") - : "stopped" + (data.title ? ':' : '')); + add_text_to(div, data.is_running ? (data.is_playing ? "playing" : "paused") + : "stopped"); if (data.title_digest) { + function format_seconds(total_seconds) { + if (total_seconds < 0) { + return "?"; + } + const seconds = total_seconds % 60; + const minutes = Math.floor(total_seconds / 60); + return `${minutes}:` + (seconds < 10 ? '0' : '') + `${seconds}` + } + add_text_to(div, " ("); + const timestamp_span = add_child_to("span", div); + clearInterval(timestamp_interval); + let timestamp = data.timestamp; + timestamp_span.textContent = format_seconds(timestamp); + if (data.is_playing) { + timestamp_interval = setInterval(function() { + timestamp += 1; + timestamp_span.textContent = format_seconds(timestamp); + }, MS_IN_S / data.speed); + } + add_text_to(div, "/" + format_seconds(data.duration) + "): "); add_a_to(div, data.title, `${PATH_PREFIX_FILE}${data.title_digest}`); add_text_to(div, " · "); add_tag_links_to(div, data.title_tags); + } }; diff --git a/src/ytplom/http.py b/src/ytplom/http.py index ae645dc..0c98d2b 100644 --- a/src/ytplom/http.py +++ b/src/ytplom/http.py @@ -164,7 +164,8 @@ class _TaskHandler(PlomHttpHandler): self.server.player.move_entry(int(command.split('_')[1]), False) elif command.startswith('rm_'): self.server.player.remove_by_idx(int(command.split('_')[1])) - elif command.startswith('inject_') or command.startswith('injectplay_'): + elif (command.startswith('inject_') + or command.startswith('injectplay_')): command, digest = command.split('_', 1) with DbConn() as conn: file = VideoFile.get_one(conn, Hash.from_b64(digest)) @@ -294,6 +295,9 @@ class _TaskHandler(PlomHttpHandler): 'is_running': self.server.player.is_running, 'is_playing': self.server.player.is_playing, 'can_play': self.server.player.can_play, + 'timestamp': self.server.player.timestamp, + 'duration': self.server.player.duration, + 'speed': self.server.player.speed, 'title_tags': tags, 'title_digest': digest, 'title': title diff --git a/src/ytplom/misc.py b/src/ytplom/misc.py index b16ccea..cf9bc96 100644 --- a/src/ytplom/misc.py +++ b/src/ytplom/misc.py @@ -589,11 +589,20 @@ class Player: self._monitoring_kill: bool = False self._kill_queue: Queue = Queue() self.playlist: list[VideoFile] = [] + self.speed = -1.0 + self.timestamp = -1 + self.duration = -1 self.load_files_and_mpv() def _signal_update(self) -> None: - """Update .last_update as signal player state has changed relevantly""" + """Update .last_update as signal player state has changed relevantly. + + If possible, also updates current player timestamp. + """ self.last_update = _now_string() + if self._mpv: + self.timestamp = (int(self._mpv.time_pos) if self._mpv.time_pos + else -1) def _monitor_kill(self) -> None: """Properly enforce mpv shutdown from direct interaction with mpv @@ -637,6 +646,8 @@ class Player: - bind starting of files to ._signal_update and setting ._idx to MPV's own playlist position index - bind ending last file to re-starting at playlist start + - bind what affects currently played duration, timestamp to respective + updates - start playing """ self._mpv = MPV(input_default_bindings=True, @@ -662,7 +673,21 @@ class Player: def on_shutdown(_) -> None: self._kill_queue.put(True) + @self._mpv.event_callback('seek') + def on_seek(*_) -> None: + self._signal_update() + + def on_duration_change(_, duration_in_s): + self.duration = int(duration_in_s) if duration_in_s else -1 + self._signal_update() + + def on_speed_change(_, speed): + self.speed = speed + self._signal_update() + self._mpv.observe_property('pause', lambda a, b: self._signal_update()) + self._mpv.observe_property('duration', on_duration_change) + self._mpv.observe_property('speed', on_speed_change) for path in [f.full_path for f in self.playlist]: self._mpv.command('loadfile', path, 'append') self._idx = 0 @@ -894,7 +919,7 @@ class DownloadsManager: video_id = self._to_download.pop(0) url = f'{YOUTUBE_URL_PREFIX}{video_id}' with YoutubeDL(YT_DL_PARAMS | {'progress_hooks': [hook]}) as ydl: - self._update_status(video_id, f'preparing download') + self._update_status(video_id, 'preparing download') info = ydl.sanitize_info(ydl.extract_info(url, download=False)) for requested in info['requested_formats']: estimated_total += requested['filesize_approx'] -- 2.30.2