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+):
```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:

View File

@ -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():
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
except (FileNotFoundError, Exception):
pass
# Fallback: generate a placeholder
from PySide6.QtGui import QPainter, QColor, QFont
from PySide6.QtGui import QPolygon