Add FFmpeg-based video integrity check

This commit is contained in:
2026-07-11 11:32:07 +02:00
parent 97a39709e8
commit 4c4180f28c
8 changed files with 422 additions and 4 deletions
+85 -1
View File
@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QSplitter, QTreeWidget, QTreeWidgetItem,
QTableWidget, QTableWidgetItem, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QMessageBox, QInputDialog, QDialog, QDialogButtonBox,
QHeaderView, QMenu, QAbstractItemView, QCheckBox, QComboBox,
QHeaderView, QMenu, QAbstractItemView, QCheckBox, QComboBox, QProgressDialog,
)
from src.core.file_manager import FileManager
@@ -518,6 +518,7 @@ class QtApp(QMainWindow):
tests_menu = bar.addMenu("&Testy")
self._add_action(tests_menu, "Kontrola konzistence dat…", self.check_consistency)
self._add_action(tests_menu, "Kontrola integrity videa…", self.check_video_integrity)
def _add_action(self, menu: QMenu, text: str, slot, shortcut: str | None = None) -> QAction:
action = QAction(text, self)
@@ -1130,6 +1131,89 @@ class QtApp(QMainWindow):
box.setDetailedText("\n".join(detail))
box.exec()
def check_video_integrity(self) -> None:
"""Test: scan movies for corrupted / unreadable video data (FFmpeg)."""
if not self.filehandler.movies_dir:
QMessageBox.information(self, "Testy", "Nejprve nastavte pool.")
return
# Scope: selected movies, or the whole pool when nothing is selected.
files = self._selected_movies() or self.filehandler.filelist
if not files:
QMessageBox.information(self, "Kontrola integrity", "Pool je prázdný.")
return
scope = "vybrané filmy" if self._selected_movies() else "celý pool"
# Depth: deep = full decode (thorough, slow), quick = ffprobe (fast).
box = QMessageBox(self)
box.setIcon(QMessageBox.Question)
box.setWindowTitle("Kontrola integrity videa")
box.setText(
f"Zkontrolovat {scope} ({len(files)}).\n\n"
"Důkladná: plné dekódování přes ffmpeg — odhalí i poškozená data "
"uvnitř souboru, ale čte celý soubor (pomalé).\n"
"Rychlá: ffprobe — odhalí nečitelné/uříznuté soubory (rychlé)."
)
deep_btn = box.addButton("Důkladná", QMessageBox.AcceptRole)
quick_btn = box.addButton("Rychlá", QMessageBox.AcceptRole)
box.addButton("Zrušit", QMessageBox.RejectRole)
box.exec()
clicked = box.clickedButton()
if clicked not in (deep_btn, quick_btn):
return
deep = clicked is deep_btn
progress = QProgressDialog(
"Kontroluji integritu videa…", "Zrušit", 0, len(files), self)
progress.setWindowTitle("Kontrola integrity videa")
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
def on_progress(done: int, total: int, f: File) -> None:
progress.setValue(done)
progress.setLabelText(f"[{done}/{total}] {f.filename}")
QApplication.processEvents()
result = self.filehandler.scan_video_integrity(
files, deep=deep, on_progress=on_progress,
should_cancel=progress.wasCanceled)
progress.setValue(len(files))
if not result["ok"]:
QMessageBox.warning(self, "Kontrola integrity", result["error"])
return
problems = result["problems"]
checked = result["checked"]
suffix = " (zrušeno)" if result["cancelled"] else ""
if not problems:
QMessageBox.information(
self, "Kontrola integrity videa",
f"✅ Bez problémů — zkontrolováno {checked} souborů{suffix}.")
return
bad = [r for r in problems if r.checked]
unchecked = [r for r in problems if not r.checked]
detail: list[str] = []
if bad:
detail.append(f"■ Poškozené / vadné ({len(bad)}):")
detail += [f"{r.path.name}{r.error}" for r in bad]
detail.append("")
if unchecked:
detail.append(f"■ Nešlo zkontrolovat ({len(unchecked)}):")
detail += [f"{r.path.name}{r.error}" for r in unchecked]
msg = QMessageBox(self)
msg.setIcon(QMessageBox.Warning)
msg.setWindowTitle("Kontrola integrity — nalezeny problémy")
msg.setText(
f"Zkontrolováno: {checked}{suffix}\n"
f"Poškozené / vadné: {len(bad)}\n"
f"Nešlo zkontrolovat: {len(unchecked)}\n\n"
"Podrobnosti zobrazíš přes „Show Details…\".")
msg.setDetailedText("\n".join(detail))
msg.exec()
def closeEvent(self, event) -> None: # noqa: N802 — Qt override
self.filehandler.global_config["window_geometry"] = f"{self.width()}x{self.height()}"
from src.core.config import save_global_config