replace ffmpeg with mpv for library video thumbnails

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.
This commit is contained in:
pax 2026-04-12 14:31:30 -05:00
parent 0d72b0ec8a
commit 14c81484c9
2 changed files with 42 additions and 14 deletions

View File

@ -58,12 +58,12 @@ AUR: [/packages/booru-viewer-git](https://aur.archlinux.org/packages/booru-viewe
Ubuntu / Debian (24.04+): Ubuntu / Debian (24.04+):
```sh ```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: Fedora:
```sh ```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: Then clone and install:

View File

@ -326,21 +326,49 @@ class LibraryView(QWidget):
threading.Thread(target=_work, daemon=True).start() threading.Thread(target=_work, daemon=True).start()
def _capture_video_thumb(self, index: int, source: str, dest: str) -> None: 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(): def _work():
extracted = False
try: try:
import subprocess import threading as _threading
result = subprocess.run( import mpv as mpvlib
["ffmpeg", "-y", "-i", source, "-vframes", "1",
"-vf", f"scale={LIBRARY_THUMB_SIZE}:{LIBRARY_THUMB_SIZE}:force_original_aspect_ratio=decrease", frame_ready = _threading.Event()
"-q:v", "5", dest], m = mpvlib.MPV(
capture_output=True, timeout=10, vo='null', ao='null', aid='no',
pause=True, keep_open='yes',
terminal=False, config=False,
) )
if Path(dest).exists(): 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) self._signals.thumb_ready.emit(index, dest)
return return
except (FileNotFoundError, Exception):
pass
# Fallback: generate a placeholder # Fallback: generate a placeholder
from PySide6.QtGui import QPainter, QColor, QFont from PySide6.QtGui import QPainter, QColor, QFont
from PySide6.QtGui import QPolygon from PySide6.QtGui import QPolygon