Store playback info
This commit is contained in:
parent
27e754b3f7
commit
18c1e6c4a7
1 changed files with 120 additions and 10 deletions
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from typing import Any, TypeVar, cast, overload
|
||||
|
||||
import gi
|
||||
|
||||
|
@ -15,23 +15,28 @@ gi.require_version("Gst", "1.0")
|
|||
gi.require_version("Pango", "1.0")
|
||||
from gi.repository import Gdk, Gio, Gst, Gtk, Pango # NOQA: E402
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class MainWindow(Gtk.ApplicationWindow):
|
||||
file_info_label: Gtk.Label
|
||||
stack: Gtk.Stack
|
||||
list_view: Gtk.ListView
|
||||
list_store: Gio.ListStore
|
||||
selection_model: Gtk.SingleSelection
|
||||
video_widget: Gtk.Picture
|
||||
pipeline: Gst.Pipeline
|
||||
overlay_tick_callback_id: int
|
||||
overlay_label: Gtk.Label
|
||||
overlay_hide_time: float
|
||||
last_position_save: float
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# For overlay text timeout
|
||||
self.overlay_hide_time = 0.0
|
||||
self.last_position_save = 0.0
|
||||
self.overlay_label = Gtk.Label()
|
||||
self.overlay_label.set_name("overlay-text")
|
||||
self.overlay_label.set_valign(Gtk.Align.CENTER)
|
||||
|
@ -131,13 +136,13 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
# Create list store and view
|
||||
self.list_store = Gio.ListStore(item_type=FileItem)
|
||||
self.list_view = Gtk.ListView()
|
||||
selection_model = Gtk.SingleSelection.new(self.list_store)
|
||||
selection_model.connect("selection-changed", self._on_selection_changed)
|
||||
self.list_view.set_model(selection_model)
|
||||
self.selection_model = Gtk.SingleSelection.new(self.list_store)
|
||||
self.selection_model.connect("selection-changed", self._on_selection_changed)
|
||||
self.list_view.set_model(self.selection_model)
|
||||
self.list_view.set_vexpand(True)
|
||||
|
||||
def on_activate(widget: Gtk.ListView, index: int):
|
||||
selected_item = selection_model.get_item(index)
|
||||
selected_item = self.selection_model.get_item(index)
|
||||
if selected_item:
|
||||
file_item = cast(FileItem, selected_item)
|
||||
|
||||
|
@ -146,13 +151,45 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
self._populate_file_list()
|
||||
return
|
||||
|
||||
position = self._load_attribute("position", 0)
|
||||
|
||||
# Start playing the video
|
||||
playbin = self.pipeline.get_by_name("playbin")
|
||||
if playbin:
|
||||
playbin.set_property("uri", f"file://{os.path.abspath(file_item.full_path)}")
|
||||
self.pipeline.set_state(Gst.State.PLAYING)
|
||||
self.stack.set_visible_child_name("overlay")
|
||||
self.show_overlay_text(f"Playing: {file_item.name}")
|
||||
if not playbin:
|
||||
return
|
||||
|
||||
full_path = os.path.abspath(file_item.full_path)
|
||||
playbin.set_property("uri", f"file://{full_path}")
|
||||
|
||||
track = self._load_attribute("subtitle_track", -2)
|
||||
|
||||
if track >= 0:
|
||||
flags = playbin.get_property("flags")
|
||||
flags |= 0x00000004 # TEXT flag
|
||||
playbin.set_property("flags", flags)
|
||||
playbin.set_property("current-text", track)
|
||||
elif track == -1:
|
||||
flags = playbin.get_property("flags")
|
||||
flags &= ~0x00000004 # TEXT flag
|
||||
playbin.set_property("flags", flags)
|
||||
|
||||
if position:
|
||||
# Pause and wait for it to complete.
|
||||
self.pipeline.set_state(Gst.State.PAUSED)
|
||||
self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
|
||||
|
||||
# Seek to saved position.
|
||||
self.pipeline.seek_simple(
|
||||
Gst.Format.TIME,
|
||||
Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
|
||||
position,
|
||||
)
|
||||
|
||||
# Now start playing
|
||||
self.pipeline.set_state(Gst.State.PLAYING)
|
||||
|
||||
self.stack.set_visible_child_name("overlay")
|
||||
self.show_overlay_text(f"Playing: {file_item.name}")
|
||||
|
||||
self.list_view.connect("activate", on_activate)
|
||||
|
||||
|
@ -172,6 +209,20 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
|
||||
self.set_child(self.stack)
|
||||
|
||||
@property
|
||||
def currently_playing(self):
|
||||
selected_item = self.selection_model.get_selected_item()
|
||||
|
||||
if not selected_item:
|
||||
return None
|
||||
|
||||
file_item = cast(FileItem, selected_item)
|
||||
|
||||
if file_item.file_type == FileType.DIRECTORY:
|
||||
return None
|
||||
|
||||
return file_item.full_path
|
||||
|
||||
def _setup_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
|
||||
# Create horizontal box to hold icon and label
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
@ -232,6 +283,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
return True
|
||||
|
||||
elif keyval == Gdk.keyval_from_name("Escape"):
|
||||
self._save_position()
|
||||
self.pipeline.set_state(Gst.State.NULL)
|
||||
self.stack.set_visible_child_name("menu")
|
||||
self.list_view.grab_focus()
|
||||
|
@ -273,6 +325,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
|
||||
def _seek_relative(self, offset: int) -> None:
|
||||
"""Seek relative to current position by offset seconds"""
|
||||
|
||||
playbin = self.pipeline.get_by_name("playbin")
|
||||
if not playbin:
|
||||
return
|
||||
|
@ -296,6 +349,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
|
||||
def _get_subtitle_info(self, track_index: int) -> str:
|
||||
"""Get subtitle track info including language if available"""
|
||||
|
||||
playbin = self.pipeline.get_by_name("playbin")
|
||||
if not playbin:
|
||||
return str(track_index)
|
||||
|
@ -308,8 +362,56 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
found, lang = caps.get_string("language-code")
|
||||
return f"{track_index} ({lang})" if found else str(track_index)
|
||||
|
||||
def _save_position(self) -> None:
|
||||
"""Save current playback position as xattr"""
|
||||
|
||||
playbin = self.pipeline.get_by_name("playbin")
|
||||
if not playbin:
|
||||
return
|
||||
|
||||
success, position = self.pipeline.query_position(Gst.Format.TIME)
|
||||
success2, duration = self.pipeline.query_duration(Gst.Format.TIME)
|
||||
|
||||
if success and success2:
|
||||
self._save_attribute("position", position)
|
||||
self._save_attribute("duration", duration)
|
||||
|
||||
def _save_attribute(self, name: str, value: str | float | int | None):
|
||||
path = self.currently_playing
|
||||
|
||||
if path is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if value is None:
|
||||
os.removexattr(path, f"user.lazy_player.{name}")
|
||||
else:
|
||||
os.setxattr(path, f"user.lazy_player.{name}", str(value).encode("utf8"))
|
||||
except OSError as err:
|
||||
print(err, file=sys.stderr)
|
||||
|
||||
@overload
|
||||
def _load_attribute(self, name: str, dfl: str) -> str: ...
|
||||
|
||||
@overload
|
||||
def _load_attribute(self, name: str, dfl: int) -> int: ...
|
||||
|
||||
def _load_attribute(self, name: str, dfl: str | int) -> str | int:
|
||||
path = self.currently_playing
|
||||
|
||||
if path is None:
|
||||
return dfl
|
||||
|
||||
try:
|
||||
strval = os.getxattr(path, f"user.lazy_player.{name}")
|
||||
return type(dfl)(strval)
|
||||
except OSError as err:
|
||||
print(err, file=sys.stderr)
|
||||
return dfl
|
||||
|
||||
def _cycle_subtitles(self) -> None:
|
||||
"""Cycle through available subtitle tracks, including off state"""
|
||||
|
||||
playbin = self.pipeline.get_by_name("playbin")
|
||||
if not playbin:
|
||||
return
|
||||
|
@ -330,6 +432,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
playbin.set_property("current-text", 0)
|
||||
track_info = self._get_subtitle_info(0)
|
||||
self.show_overlay_text(f"Subtitle track: {track_info}")
|
||||
self._save_attribute("subtitle_track", 0)
|
||||
return
|
||||
|
||||
# If we're on the last track, disable subtitles
|
||||
|
@ -337,6 +440,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
flags &= ~0x00000004 # TEXT flag
|
||||
playbin.set_property("flags", flags)
|
||||
self.show_overlay_text("Subtitles: Off")
|
||||
self._save_attribute("subtitle_track", -1)
|
||||
return
|
||||
|
||||
# Otherwise cycle to next track
|
||||
|
@ -344,6 +448,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
playbin.set_property("current-text", next_track)
|
||||
track_info = self._get_subtitle_info(next_track)
|
||||
self.show_overlay_text(f"Subtitle track: {track_info}")
|
||||
self._save_attribute("subtitle_track", next_track)
|
||||
|
||||
def show_overlay_text(self, text: str, timeout_seconds: float = 1.0) -> None:
|
||||
"""Show text in a centered overlay that disappears after timeout"""
|
||||
|
@ -358,6 +463,11 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||
frame_time = frame_clock.get_frame_time() / 1_000_000 # Convert to seconds
|
||||
self.overlay_hide_time = frame_time + timeout_seconds
|
||||
|
||||
# Save position every 60 seconds
|
||||
if frame_time - self.last_position_save >= 60.0:
|
||||
self._save_position()
|
||||
self.last_position_save = frame_time
|
||||
|
||||
def _populate_file_list(self) -> None:
|
||||
# TODO: Implement proper version sort (strverscmp equivalent)
|
||||
|
||||
|
|
Loading…
Reference in a new issue