27933dd495
PlayerPanel.keyPressEvent handled Space, F and Escape and was dead code: nothing in the player called setFocusPolicy, so focus went to whichever child button, slider or combo box was first in the tab order, and that child ate the keys. Fixing focus alone would not have been enough -- clicking play parks focus on the play button, which then swallows Space -- so the control bar is explicitly NoFocus and the bindings are QShortcuts with WidgetWithChildrenShortcut context on the panel. That fires whichever descendant holds focus, and does not reach the Search box or the URL field on other tabs the way an application shortcut would. mpv's own input stays disabled. With vo=libmpv there is no mpv-owned window, so its input layer receives nothing; enabling default bindings would mean hand-forwarding events through a Qt-to-mpv key-name table, handing mpv the OSD and OSC that are switched off here, and letting `q` quit the core out from under the Qt UI. One declarative action table now drives the shortcuts, the context menu and the buttons. There was no mouse handling at all. Click pauses, double-click goes fullscreen -- via a doubleClickInterval timer, so a double-click does not also pause on the way -- the wheel changes volume and Ctrl+wheel seeks, with sub-notch deltas accumulated so a trackpad is not inert. The control bar gained previous, next and mute. Mute and volume are driven by observed mpv properties rather than assumed, so the icon follows a change made by key, menu or mpv itself. Buffering was completely invisible: a stalled stream showed a frozen frame for 25 seconds before the retry with nothing on screen, so paused-for-cache now surfaces a label over the video. WatchPage owns next/previous because it owns the queue; the panel only asks. Previous restarts the current item when it is more than five seconds in, as every other player does, and pushes the interrupted item back onto the head of the queue rather than dropping it. Verified: all 38 bindings install, every action is a no-op rather than an exception with nothing loaded, focus policies are as intended, and a fullscreen round trip drives the player state correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
235 lines
9.1 KiB
Python
235 lines
9.1 KiB
Python
"""
|
|
Watch tab - player plus play queue
|
|
==================================
|
|
|
|
Hosts the embedded mpv PlayerPanel (or the libmpv-missing hint) and a simple
|
|
play queue. Entries arrive via AppRouter.playVideo / queueVideo.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from PySide6.QtCore import Qt, QTimer
|
|
from PySide6.QtWidgets import (
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QListWidget,
|
|
QListWidgetItem,
|
|
QPushButton,
|
|
QSplitter,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from .ytsage_gui_player import PlayerPanel, create_player_panel
|
|
from ..utils.ytsage_config_manager import ConfigManager
|
|
from ..utils.ytsage_library_manager import LibraryManager
|
|
from ..utils.ytsage_localization import _
|
|
from ..utils.ytsage_logger import logger
|
|
|
|
POSITION_SAVE_INTERVAL_MS = 5000
|
|
|
|
|
|
class WatchPage(QWidget):
|
|
def __init__(self, router, parent: Optional[QWidget] = None) -> None:
|
|
super().__init__(parent)
|
|
self._router = router
|
|
self._queue: List[Dict[str, Any]] = []
|
|
# What has already played this session, so "previous" has somewhere
|
|
# to go. Not persisted: it is session history, not the queue.
|
|
self._played: List[Dict[str, Any]] = []
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
splitter = QSplitter(Qt.Orientation.Horizontal, self)
|
|
layout.addWidget(splitter)
|
|
|
|
self.player = create_player_panel(self)
|
|
splitter.addWidget(self.player)
|
|
|
|
# Kept as an attribute: FullscreenController hides it rather than
|
|
# reparenting the player, which is what used to lose the GL context.
|
|
self.queue_panel = queue_panel = QWidget(self)
|
|
queue_layout = QVBoxLayout(queue_panel)
|
|
queue_layout.setContentsMargins(4, 0, 0, 0)
|
|
|
|
queue_header = QHBoxLayout()
|
|
queue_label = QLabel(_("player.queue"))
|
|
queue_label.setStyleSheet("font-weight: bold;")
|
|
queue_header.addWidget(queue_label)
|
|
queue_header.addStretch()
|
|
self.clear_queue_btn = QPushButton("✕")
|
|
self.clear_queue_btn.setFixedWidth(28)
|
|
self.clear_queue_btn.setToolTip(_("watch.clear_queue"))
|
|
self.clear_queue_btn.clicked.connect(self.clear_queue)
|
|
queue_header.addWidget(self.clear_queue_btn)
|
|
queue_layout.addLayout(queue_header)
|
|
|
|
self.queue_list = QListWidget()
|
|
self.queue_list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
|
|
self.queue_list.itemDoubleClicked.connect(self._on_queue_item_activated)
|
|
self.queue_list.model().rowsMoved.connect(self._on_rows_moved)
|
|
queue_layout.addWidget(self.queue_list)
|
|
|
|
splitter.addWidget(queue_panel)
|
|
splitter.setStretchFactor(0, 4)
|
|
splitter.setStretchFactor(1, 1)
|
|
splitter.setSizes([900, 240])
|
|
|
|
if isinstance(self.player, PlayerPanel):
|
|
self.player.playbackEnded.connect(self._on_playback_ended)
|
|
self._duration = 0.0
|
|
self.player.durationChanged.connect(self._on_duration_changed)
|
|
self._position_timer = QTimer(self)
|
|
self._position_timer.setInterval(POSITION_SAVE_INTERVAL_MS)
|
|
self._position_timer.timeout.connect(self._save_position)
|
|
self.player.nowPlayingChanged.connect(self._on_now_playing)
|
|
# The panel asks; the queue lives here, so the queue answers.
|
|
self.player.nextRequested.connect(self.play_next)
|
|
self.player.previousRequested.connect(self.play_previous)
|
|
|
|
self._restore_queue()
|
|
|
|
# ------------------------------------------------------------ public API
|
|
|
|
def play_entry(self, entry: Dict[str, Any], resume_pos: float = 0.0) -> None:
|
|
if isinstance(self.player, PlayerPanel):
|
|
if resume_pos <= 0 and (ConfigManager.get("player.resume") or "auto") == "auto":
|
|
video_id = entry.get("id") or entry.get("url")
|
|
if video_id:
|
|
resume_pos = LibraryManager.get_resume_position(str(video_id))
|
|
self.player.play(entry, resume_pos=resume_pos)
|
|
else:
|
|
logger.warning("Play requested but libmpv is unavailable")
|
|
|
|
def enqueue(self, entry: Dict[str, Any]) -> None:
|
|
self._queue.append(dict(entry))
|
|
item = QListWidgetItem(entry.get("title") or entry.get("url") or "?")
|
|
item.setData(Qt.ItemDataRole.UserRole, dict(entry))
|
|
self.queue_list.addItem(item)
|
|
self._persist_queue()
|
|
# Start playing right away when nothing is on and this is the first item
|
|
if isinstance(self.player, PlayerPanel) and not self.player.current_entry() and len(self._queue) == 1:
|
|
self._play_next_from_queue()
|
|
|
|
def clear_queue(self) -> None:
|
|
self._queue.clear()
|
|
self.queue_list.clear()
|
|
self._persist_queue()
|
|
|
|
def queue_entries(self) -> List[Dict[str, Any]]:
|
|
return [self.queue_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.queue_list.count())]
|
|
|
|
def play_next(self) -> None:
|
|
"""Next in the queue. Bound to N and the player's next button."""
|
|
self._play_next_from_queue()
|
|
|
|
def play_previous(self) -> None:
|
|
"""
|
|
Back to what was playing before.
|
|
|
|
Restarts the current item first if it is more than a few seconds in,
|
|
which is what every other player does with a "previous" press.
|
|
"""
|
|
if not isinstance(self.player, PlayerPanel):
|
|
return
|
|
if self.player.current_position() > 5.0:
|
|
self.player.seek_absolute(0.0)
|
|
return
|
|
# _played ends with what is playing now, so going back needs two.
|
|
if len(self._played) < 2:
|
|
return
|
|
current = self._played.pop()
|
|
previous = self._played[-1]
|
|
# Put the current item back at the head of the queue rather than
|
|
# dropping it on the floor.
|
|
if current:
|
|
item = QListWidgetItem(current.get("title") or current.get("url") or "?")
|
|
item.setData(Qt.ItemDataRole.UserRole, dict(current))
|
|
self.queue_list.insertItem(0, item)
|
|
self._queue.insert(0, dict(current))
|
|
self._persist_queue()
|
|
# play_entry re-pushes `previous`, so drop it here to avoid a double.
|
|
self._played.pop()
|
|
self.play_entry(previous)
|
|
|
|
# --------------------------------------------------------------- internal
|
|
|
|
def _play_next_from_queue(self) -> None:
|
|
if self.queue_list.count() == 0:
|
|
return
|
|
item = self.queue_list.takeItem(0)
|
|
entry = item.data(Qt.ItemDataRole.UserRole)
|
|
if entry in self._queue:
|
|
self._queue.remove(entry)
|
|
self._persist_queue()
|
|
self.play_entry(entry)
|
|
|
|
def _on_playback_ended(self, reason: str) -> None:
|
|
self._save_position(final=True)
|
|
if reason in ("eof", "") and self.queue_list.count() > 0:
|
|
self._play_next_from_queue()
|
|
|
|
def _on_queue_item_activated(self, item: QListWidgetItem) -> None:
|
|
entry = item.data(Qt.ItemDataRole.UserRole)
|
|
row = self.queue_list.row(item)
|
|
self.queue_list.takeItem(row)
|
|
if entry in self._queue:
|
|
self._queue.remove(entry)
|
|
self._persist_queue()
|
|
self.play_entry(entry)
|
|
|
|
def _on_rows_moved(self, *args) -> None:
|
|
self._queue = self.queue_entries()
|
|
self._persist_queue()
|
|
|
|
# ------------------------------------------------- history & persistence
|
|
|
|
def _on_now_playing(self, entry: Dict[str, Any]) -> None:
|
|
LibraryManager.upsert_watch(entry)
|
|
# Record what we were on before this, so "previous" can return to it.
|
|
# The retry path replays the same entry; don't stack duplicates.
|
|
if self._played[-1:] != [entry] and entry:
|
|
self._played.append(dict(entry))
|
|
del self._played[:-50]
|
|
self._duration = 0.0
|
|
self._position_timer.start()
|
|
|
|
def _on_duration_changed(self, duration: float) -> None:
|
|
self._duration = duration
|
|
|
|
def _save_position(self, final: bool = False) -> None:
|
|
if not isinstance(self.player, PlayerPanel):
|
|
return
|
|
entry = self.player.current_entry()
|
|
video_id = entry.get("id") or entry.get("url")
|
|
if not video_id:
|
|
return
|
|
pos = self.player.current_position()
|
|
if pos > 0:
|
|
LibraryManager.update_position(str(video_id), pos, self._duration or entry.get("duration"))
|
|
if final:
|
|
self._position_timer.stop()
|
|
|
|
def _persist_queue(self) -> None:
|
|
try:
|
|
LibraryManager.save_queue(self.queue_entries())
|
|
except Exception as e:
|
|
logger.debug(f"Could not persist queue: {e}")
|
|
|
|
def _restore_queue(self) -> None:
|
|
try:
|
|
for entry in LibraryManager.load_queue():
|
|
self._queue.append(entry)
|
|
item = QListWidgetItem(entry.get("title") or entry.get("url") or "?")
|
|
item.setData(Qt.ItemDataRole.UserRole, entry)
|
|
self.queue_list.addItem(item)
|
|
except Exception as e:
|
|
logger.debug(f"Could not restore queue: {e}")
|
|
|
|
def shutdown(self) -> None:
|
|
if isinstance(self.player, PlayerPanel):
|
|
self._save_position(final=True)
|
|
self._persist_queue()
|
|
self.player.shutdown()
|