From 14c81484c99b5a1d63761ad2d6119d0d08ba4244 Mon Sep 17 00:00:00 2001 From: pax Date: Sun, 12 Apr 2026 14:31:30 -0500 Subject: [PATCH] replace ffmpeg with mpv for library video thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit behavior change: video thumbnails in the Library tab are now generated by a headless mpv instance (vo=null, pause=True, screenshot-to-file) instead of shelling out to ffmpeg. Resized to LIBRARY_THUMB_SIZE with PIL. Falls back to the same placeholder on failure. ffmpeg removed from README install commands — no longer a dependency. --- README.md | 4 +-- booru_viewer/gui/library.py | 52 ++++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 898a7ba..ccbc53c 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,12 @@ AUR: [/packages/booru-viewer-git](https://aur.archlinux.org/packages/booru-viewe Ubuntu / Debian (24.04+): ```sh -sudo apt install python3 python3-pip python3-venv mpv libmpv-dev ffmpeg +sudo apt install python3 python3-pip python3-venv mpv libmpv-dev ``` Fedora: ```sh -sudo dnf install python3 python3-pip qt6-qtbase mpv mpv-libs-devel ffmpeg +sudo dnf install python3 python3-pip qt6-qtbase mpv mpv-libs-devel ``` Then clone and install: diff --git a/booru_viewer/gui/library.py b/booru_viewer/gui/library.py index 45ebb92..e5413f8 100644 --- a/booru_viewer/gui/library.py +++ b/booru_viewer/gui/library.py @@ -326,21 +326,49 @@ class LibraryView(QWidget): threading.Thread(target=_work, daemon=True).start() def _capture_video_thumb(self, index: int, source: str, dest: str) -> None: - """Grab first frame from video. Tries ffmpeg, falls back to placeholder.""" + """Grab first frame from video using mpv, falls back to placeholder.""" def _work(): + extracted = False try: - import subprocess - result = subprocess.run( - ["ffmpeg", "-y", "-i", source, "-vframes", "1", - "-vf", f"scale={LIBRARY_THUMB_SIZE}:{LIBRARY_THUMB_SIZE}:force_original_aspect_ratio=decrease", - "-q:v", "5", dest], - capture_output=True, timeout=10, + import threading as _threading + import mpv as mpvlib + + frame_ready = _threading.Event() + m = mpvlib.MPV( + vo='null', ao='null', aid='no', + pause=True, keep_open='yes', + terminal=False, config=False, ) - if Path(dest).exists(): - self._signals.thumb_ready.emit(index, dest) - return - except (FileNotFoundError, Exception): - pass + try: + @m.property_observer('video-params') + def _on_params(_name, value): + if isinstance(value, dict) and value.get('w'): + frame_ready.set() + + m.loadfile(source) + if frame_ready.wait(timeout=10): + m.command('screenshot-to-file', dest, 'video') + finally: + m.terminate() + + if Path(dest).exists() and Path(dest).stat().st_size > 0: + from PIL import Image + with Image.open(dest) as img: + img.thumbnail( + (LIBRARY_THUMB_SIZE, LIBRARY_THUMB_SIZE), + Image.LANCZOS, + ) + if img.mode in ("RGBA", "P"): + img = img.convert("RGB") + img.save(dest, "JPEG", quality=85) + extracted = True + except Exception as e: + log.debug("mpv thumb extraction failed for %s: %s", source, e) + + if extracted and Path(dest).exists(): + self._signals.thumb_ready.emit(index, dest) + return + # Fallback: generate a placeholder from PySide6.QtGui import QPainter, QColor, QFont from PySide6.QtGui import QPolygon