home · contact · privacy
Display/estimate current in-file playback position in player interface. master
authorChristian Heller <c.heller@plomlompom.de>
Tue, 11 Mar 2025 08:26:21 +0000 (09:26 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Tue, 11 Mar 2025 08:26:21 +0000 (09:26 +0100)
src/templates/_base.tmpl
src/ytplom/http.py
src/ytplom/misc.py

index 56e5df543c16f0641bf277636e15b6d4be22cf14..8764a5ea5b9721ebebaad219cdb0197c4614ab71 100644 (file)
@@ -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);
+
     }
 };
 
index ae645dcdaba6e80ad778d9fdfc225906190278ef..0c98d2b12a4ac02d756766a882101c595f3f44f9 100644 (file)
@@ -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
index b16cceadf20b0b30ea39ef7f180669194c013b6f..cf9bc96453f12414819484cf5d58c7981b065e17 100644 (file)
@@ -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']