Fix video thumbnails (ffmpeg with placeholder fallback), fix right-click restart

- Video thumbnails: try ffmpeg, fall back to play icon placeholder
- Right-click no longer restarts video playback on same post
- Reset activated index on new search
This commit is contained in:
pax 2026-04-05 04:21:48 -05:00
parent 85ec13bf7c
commit fad6ab65af
2 changed files with 41 additions and 39 deletions

View File

@ -671,6 +671,7 @@ class BooruApp(QMainWindow):
self._run_async(_search)
def _on_search_done(self, posts: list) -> None:
self._last_activated_index = -1
self._posts = posts
self._status.showMessage(f"{len(posts)} results")
thumbs = self._grid.set_posts(len(posts))
@ -768,6 +769,8 @@ class BooruApp(QMainWindow):
# -- Post selection / preview --
_last_activated_index = -1
def _on_post_selected(self, index: int) -> None:
multi = self._grid.selected_indices
if len(multi) > 1:
@ -780,7 +783,9 @@ class BooruApp(QMainWindow):
)
if self._info_panel.isVisible():
self._info_panel.set_post(post)
self._on_post_activated(index)
if index != self._last_activated_index:
self._last_activated_index = index
self._on_post_activated(index)
def _on_post_activated(self, index: int) -> None:
if 0 <= index < len(self._posts):

View File

@ -7,10 +7,8 @@ import os
import threading
from pathlib import Path
from PySide6.QtCore import Qt, Signal, QObject, QUrl, QTimer
from PySide6.QtCore import Qt, Signal, QObject
from PySide6.QtGui import QPixmap
from PySide6.QtMultimedia import QMediaPlayer, QVideoSink, QVideoFrame
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
@ -241,43 +239,42 @@ 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 using Qt's QMediaPlayer + QVideoSink."""
from PySide6.QtMultimedia import QAudioOutput
player = QMediaPlayer()
audio = QAudioOutput()
audio.setVolume(0)
player.setAudioOutput(audio)
sink = QVideoSink()
player.setVideoSink(sink)
captured = [False]
def _on_frame(frame: QVideoFrame):
if captured[0]:
return
if frame.isValid():
img = frame.toImage()
if not img.isNull():
captured[0] = True
scaled = img.scaled(
LIBRARY_THUMB_SIZE, LIBRARY_THUMB_SIZE,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
scaled.save(dest, "JPEG", 85)
"""Grab first frame from video. Tries ffmpeg, falls back to placeholder."""
def _work():
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,
)
if Path(dest).exists():
self._signals.thumb_ready.emit(index, dest)
player.stop()
player.deleteLater()
return
except (FileNotFoundError, Exception):
pass
# Fallback: generate a placeholder
from PySide6.QtGui import QPainter, QColor, QFont
from PySide6.QtGui import QPolygon
from PySide6.QtCore import QPoint as QP
pix = QPixmap(LIBRARY_THUMB_SIZE - 4, LIBRARY_THUMB_SIZE - 4)
pix.fill(QColor(40, 40, 40))
painter = QPainter(pix)
painter.setPen(QColor(180, 180, 180))
painter.setFont(QFont(painter.font().family(), 9))
ext = Path(source).suffix.upper().lstrip(".")
painter.drawText(pix.rect(), Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, ext)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QColor(180, 180, 180, 150))
cx, cy = pix.width() // 2, pix.height() // 2 - 10
painter.drawPolygon(QPolygon([QP(cx - 15, cy - 20), QP(cx - 15, cy + 20), QP(cx + 20, cy)]))
painter.end()
pix.save(dest, "JPEG", 85)
if Path(dest).exists():
self._signals.thumb_ready.emit(index, dest)
def _cleanup():
if not captured[0]:
player.stop()
player.deleteLater()
sink.videoFrameChanged.connect(_on_frame)
player.setSource(QUrl.fromLocalFile(source))
player.play()
# Timeout cleanup if no frame arrives
QTimer.singleShot(5000, _cleanup)
threading.Thread(target=_work, daemon=True).start()
def _on_thumb_ready(self, index: int, path: str) -> None:
thumbs = self._grid._thumbs