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:
parent
85ec13bf7c
commit
fad6ab65af
@ -671,6 +671,7 @@ class BooruApp(QMainWindow):
|
|||||||
self._run_async(_search)
|
self._run_async(_search)
|
||||||
|
|
||||||
def _on_search_done(self, posts: list) -> None:
|
def _on_search_done(self, posts: list) -> None:
|
||||||
|
self._last_activated_index = -1
|
||||||
self._posts = posts
|
self._posts = posts
|
||||||
self._status.showMessage(f"{len(posts)} results")
|
self._status.showMessage(f"{len(posts)} results")
|
||||||
thumbs = self._grid.set_posts(len(posts))
|
thumbs = self._grid.set_posts(len(posts))
|
||||||
@ -768,6 +769,8 @@ class BooruApp(QMainWindow):
|
|||||||
|
|
||||||
# -- Post selection / preview --
|
# -- Post selection / preview --
|
||||||
|
|
||||||
|
_last_activated_index = -1
|
||||||
|
|
||||||
def _on_post_selected(self, index: int) -> None:
|
def _on_post_selected(self, index: int) -> None:
|
||||||
multi = self._grid.selected_indices
|
multi = self._grid.selected_indices
|
||||||
if len(multi) > 1:
|
if len(multi) > 1:
|
||||||
@ -780,6 +783,8 @@ class BooruApp(QMainWindow):
|
|||||||
)
|
)
|
||||||
if self._info_panel.isVisible():
|
if self._info_panel.isVisible():
|
||||||
self._info_panel.set_post(post)
|
self._info_panel.set_post(post)
|
||||||
|
if index != self._last_activated_index:
|
||||||
|
self._last_activated_index = index
|
||||||
self._on_post_activated(index)
|
self._on_post_activated(index)
|
||||||
|
|
||||||
def _on_post_activated(self, index: int) -> None:
|
def _on_post_activated(self, index: int) -> None:
|
||||||
|
|||||||
@ -7,10 +7,8 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
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.QtGui import QPixmap
|
||||||
from PySide6.QtMultimedia import QMediaPlayer, QVideoSink, QVideoFrame
|
|
||||||
from PySide6.QtMultimediaWidgets import QVideoWidget
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@ -241,43 +239,42 @@ 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 using Qt's QMediaPlayer + QVideoSink."""
|
"""Grab first frame from video. Tries ffmpeg, falls back to placeholder."""
|
||||||
from PySide6.QtMultimedia import QAudioOutput
|
def _work():
|
||||||
player = QMediaPlayer()
|
try:
|
||||||
audio = QAudioOutput()
|
import subprocess
|
||||||
audio.setVolume(0)
|
result = subprocess.run(
|
||||||
player.setAudioOutput(audio)
|
["ffmpeg", "-y", "-i", source, "-vframes", "1",
|
||||||
sink = QVideoSink()
|
"-vf", f"scale={LIBRARY_THUMB_SIZE}:{LIBRARY_THUMB_SIZE}:force_original_aspect_ratio=decrease",
|
||||||
player.setVideoSink(sink)
|
"-q:v", "5", dest],
|
||||||
captured = [False]
|
capture_output=True, timeout=10,
|
||||||
|
|
||||||
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)
|
if 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
|
||||||
|
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)
|
self._signals.thumb_ready.emit(index, dest)
|
||||||
player.stop()
|
|
||||||
player.deleteLater()
|
|
||||||
|
|
||||||
def _cleanup():
|
threading.Thread(target=_work, daemon=True).start()
|
||||||
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)
|
|
||||||
|
|
||||||
def _on_thumb_ready(self, index: int, path: str) -> None:
|
def _on_thumb_ready(self, index: int, path: str) -> None:
|
||||||
thumbs = self._grid._thumbs
|
thumbs = self._grid._thumbs
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user