diff --git a/booru_viewer/gui/media/video_player.py b/booru_viewer/gui/media/video_player.py index fb13756..da538dc 100644 --- a/booru_viewer/gui/media/video_player.py +++ b/booru_viewer/gui/media/video_player.py @@ -3,8 +3,6 @@ from __future__ import annotations import logging -import os -from pathlib import Path from PySide6.QtCore import Qt, QTimer, Signal, Property, QPoint from PySide6.QtGui import QColor, QIcon, QPixmap, QPainter, QPen, QBrush, QPolygon, QPainterPath, QFont @@ -330,14 +328,6 @@ class VideoPlayer(QWidget): # spawn unmuted by default. _ensure_mpv replays this on creation. self._pending_mute: bool = False - # Stream-record state: mpv's stream-record option tees its - # network stream into a .part file that gets promoted to the - # real cache path on clean EOF. Eliminates the parallel httpx - # download that used to race with mpv for the same bytes. - self._stream_record_tmp: Path | None = None - self._stream_record_target: Path | None = None - self._seeked_during_record: bool = False - def _ensure_mpv(self) -> mpvlib.MPV: """Set up mpv callbacks on first use. MPV instance is pre-created.""" if self._mpv is not None: @@ -421,8 +411,6 @@ class VideoPlayer(QWidget): def seek_to_ms(self, ms: int) -> None: if self._mpv: self._mpv.seek(ms / 1000.0, 'absolute+exact') - if self._stream_record_target is not None: - self._seeked_during_record = True def play_file(self, path: str, info: str = "") -> None: """Play a file from a local path OR a remote http(s) URL. @@ -458,23 +446,11 @@ class VideoPlayer(QWidget): self._last_video_size = None # reset dedupe so new file fires a fit self._apply_loop_to_mpv() - # Clean up any leftover .part from a previous play_file that - # didn't finish (rapid clicks, popout closed mid-stream, etc). - self._discard_stream_record() - if path.startswith(("http://", "https://")): from urllib.parse import urlparse - from ...core.cache import _referer_for, cached_path_for + from ...core.cache import _referer_for referer = _referer_for(urlparse(path)) - target = cached_path_for(path) - target.parent.mkdir(parents=True, exist_ok=True) - tmp = target.with_suffix(target.suffix + ".part") - m.loadfile(path, "replace", - referrer=referer, - stream_record=tmp.as_posix(), - demuxer_max_bytes="150MiB") - self._stream_record_tmp = tmp - self._stream_record_target = target + m.loadfile(path, "replace", referrer=referer) else: m.loadfile(path) if self._autoplay: @@ -485,7 +461,6 @@ class VideoPlayer(QWidget): self._poll_timer.start() def stop(self) -> None: - self._discard_stream_record() self._poll_timer.stop() if self._mpv: self._mpv.command('stop') @@ -570,8 +545,6 @@ class VideoPlayer(QWidget): """ if self._mpv: self._mpv.seek(pos / 1000.0, 'absolute+exact') - if self._stream_record_target is not None: - self._seeked_during_record = True def _seek_relative(self, ms: int) -> None: if self._mpv: @@ -669,61 +642,12 @@ class VideoPlayer(QWidget): if not self._eof_pending: return self._eof_pending = False - self._finalize_stream_record() if self._loop_state == 1: # Once self.pause() elif self._loop_state == 2: # Next self.pause() self.play_next.emit() - # -- Stream-record helpers -- - - def _discard_stream_record(self) -> None: - """Remove any pending stream-record temp file without promoting.""" - tmp = self._stream_record_tmp - self._stream_record_tmp = None - self._stream_record_target = None - self._seeked_during_record = False - if tmp is not None: - try: - tmp.unlink(missing_ok=True) - except OSError: - pass - - def _finalize_stream_record(self) -> None: - """Promote the stream-record .part file to its final cache path. - - Only promotes if: (a) there is a pending stream-record, (b) the - user did not seek during playback (seeking invalidates the file - because mpv may have skipped byte ranges), and (c) the .part - file exists and is non-empty. - """ - tmp = self._stream_record_tmp - target = self._stream_record_target - self._stream_record_tmp = None - self._stream_record_target = None - if tmp is None or target is None: - return - if self._seeked_during_record: - log.debug("Stream-record discarded (seek during playback): %s", tmp.name) - try: - tmp.unlink(missing_ok=True) - except OSError: - pass - return - if not tmp.exists() or tmp.stat().st_size == 0: - log.debug("Stream-record .part missing or empty: %s", tmp.name) - return - try: - os.replace(tmp, target) - log.debug("Stream-record promoted: %s -> %s", tmp.name, target.name) - except OSError as e: - log.warning("Stream-record promote failed: %s", e) - try: - tmp.unlink(missing_ok=True) - except OSError: - pass - @staticmethod def _fmt(ms: int) -> str: s = ms // 1000 diff --git a/booru_viewer/gui/media_controller.py b/booru_viewer/gui/media_controller.py index 6ae33a6..874391b 100644 --- a/booru_viewer/gui/media_controller.py +++ b/booru_viewer/gui/media_controller.py @@ -133,8 +133,6 @@ class MediaController: async def _load(): self._prefetch_pause.clear() try: - if streaming: - return path = await download_image(post.file_url, progress_callback=_progress) self._app._signals.image_done.emit(str(path), info) except Exception as e: @@ -154,6 +152,25 @@ class MediaController: def on_image_done(self, path: str, info: str) -> None: self._app._dl_progress.hide() + # If the preview is already streaming this video from URL, + # just update path references so copy/paste works — don't + # restart playback. + current = self._app._preview._current_path + if current and current.startswith(("http://", "https://")): + from ..core.cache import cached_path_for + if Path(path) == cached_path_for(current): + self._app._preview._current_path = path + idx = self._app._grid.selected_index + if 0 <= idx < len(self._app._grid._thumbs): + self._app._grid._thumbs[idx]._cached_path = path + cn = self._app._search_ctrl._cached_names + if cn is not None: + cn.add(Path(path).name) + self._app._status.showMessage( + f"{len(self._app._posts)} results — Loaded" + ) + self.auto_evict_cache() + return if self._app._popout_ctrl.window and self._app._popout_ctrl.window.isVisible(): self._app._preview._info_label.setText(info) self._app._preview._current_path = path @@ -182,10 +199,10 @@ class MediaController: else: self._app._preview._video_player.stop() self._app._preview.set_media(url, info) - # Set the expected cache path on the thumbnail so drag-to-copy - # works once the stream-record promotes the .part file on EOF. - # Streaming videos skip on_image_done (which normally sets this), - # so without this the thumbnail never gets a _cached_path. + # Pre-set the expected cache path on the thumbnail immediately. + # The parallel httpx download will also set it via on_image_done + # when it completes, but this makes it available for drag-to-copy + # from the moment streaming starts. from ..core.cache import cached_path_for idx = self._app._grid.selected_index if 0 <= idx < len(self._app._grid._thumbs):