Rework Tagger fork into Curator movie-library manager (PySide6 GUI, pool index, ČSFD import)
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
"""
|
||||
PySide6 GUI for Curator — reframed around the Filmotéka (movie-library) workflow.
|
||||
|
||||
Layout:
|
||||
- Toolbar / menu: configure the Pool and the Filmotéka output, import a movie,
|
||||
generate the Filmotéka hardlink tree.
|
||||
- Left sidebar: tag tree used as an AND-filter over the movie list.
|
||||
- Center: the movie table (the contents of pool/Filmy).
|
||||
- Status bar: counts and the current pool/output paths.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction, QKeySequence
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QSplitter, QTreeWidget, QTreeWidgetItem,
|
||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QFileDialog, QMessageBox, QInputDialog, QDialog, QDialogButtonBox,
|
||||
QFormLayout, QHeaderView, QMenu, QAbstractItemView,
|
||||
)
|
||||
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file import File
|
||||
from src.core.tag import Tag
|
||||
from src.core.constants import APP_NAME, VERSION
|
||||
from src.core.hardlink_manager import HardlinkManager
|
||||
|
||||
# Categories that drive the generated Filmotéka tree (see PROJECT.md)
|
||||
FILMOTEKA_CATEGORIES = ["Rok", "Žánr", "Hodnocení"]
|
||||
|
||||
|
||||
class ImportMovieDialog(QDialog):
|
||||
"""Collect the Title and ČSFD link for a movie being imported into the pool."""
|
||||
|
||||
def __init__(self, parent: QWidget, default_title: str) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Importovat film do poolu")
|
||||
self.setMinimumWidth(420)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
self.title_edit = QLineEdit(default_title)
|
||||
self.csfd_edit = QLineEdit()
|
||||
self.csfd_edit.setPlaceholderText("https://www.csfd.cz/film/...")
|
||||
form.addRow("Název:", self.title_edit)
|
||||
form.addRow("ČSFD odkaz:", self.csfd_edit)
|
||||
layout.addLayout(form)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return self.title_edit.text().strip()
|
||||
|
||||
@property
|
||||
def csfd_link(self) -> str:
|
||||
return self.csfd_edit.text().strip()
|
||||
|
||||
|
||||
class AssignTagsDialog(QDialog):
|
||||
"""Tri-state bulk tag assignment over the selected movies.
|
||||
|
||||
Result maps full_path -> 1 (assign), 0 (remove), 2 (leave mixed/unchanged).
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget, tagmanager: TagManager, files: List[File]) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Přiřadit štítky")
|
||||
self.setMinimumSize(380, 480)
|
||||
self.result_map: dict[str, int] = {}
|
||||
|
||||
file_tag_sets = [{t.full_path for t in f.tags} for f in files]
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(QLabel(f"Vybráno filmů: {len(files)}"))
|
||||
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderHidden(True)
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
self._items: list[tuple[str, QTreeWidgetItem]] = []
|
||||
for category in self.tagmanager_categories(tagmanager):
|
||||
cat_item = QTreeWidgetItem([category])
|
||||
cat_item.setFlags(Qt.ItemIsEnabled)
|
||||
self.tree.addTopLevelItem(cat_item)
|
||||
cat_item.setExpanded(True)
|
||||
for tag in tagmanager.get_tags_in_category(category):
|
||||
have = sum(1 for s in file_tag_sets if tag.full_path in s)
|
||||
if have == 0:
|
||||
state = Qt.Unchecked
|
||||
elif have == len(files):
|
||||
state = Qt.Checked
|
||||
else:
|
||||
state = Qt.PartiallyChecked
|
||||
item = QTreeWidgetItem([tag.name])
|
||||
item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsTristate)
|
||||
item.setCheckState(0, state)
|
||||
cat_item.addChild(item)
|
||||
self._items.append((tag.full_path, item))
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
@staticmethod
|
||||
def tagmanager_categories(tagmanager: TagManager) -> List[str]:
|
||||
return sorted(tagmanager.get_categories())
|
||||
|
||||
def accept(self) -> None:
|
||||
mapping = {Qt.Checked: 1, Qt.Unchecked: 0, Qt.PartiallyChecked: 2}
|
||||
self.result_map = {fp: mapping[item.checkState(0)] for fp, item in self._items}
|
||||
super().accept()
|
||||
|
||||
|
||||
class QtApp(QMainWindow):
|
||||
def __init__(self, filehandler: FileManager, tagmanager: TagManager) -> None:
|
||||
super().__init__()
|
||||
self.filehandler = filehandler
|
||||
self.tagmanager = tagmanager
|
||||
self.file_rows: dict[int, File] = {} # table row -> File
|
||||
self.filehandler.on_files_changed = lambda _=None: self.refresh_table()
|
||||
|
||||
self.setWindowTitle(f"{APP_NAME} {VERSION} — Filmotéka")
|
||||
geometry = self.filehandler.global_config.get("window_geometry", "1200x800")
|
||||
try:
|
||||
w, h = (int(x) for x in geometry.lower().split("x"))
|
||||
self.resize(w, h)
|
||||
except Exception:
|
||||
self.resize(1200, 800)
|
||||
|
||||
self._build_menu()
|
||||
self._build_central()
|
||||
self._build_statusbar()
|
||||
|
||||
# Load the pool if one is configured
|
||||
if self.filehandler.movies_dir:
|
||||
self.filehandler.load_pool_movies()
|
||||
self.refresh_sidebar()
|
||||
self.refresh_table()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# UI construction
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_menu(self) -> None:
|
||||
bar = self.menuBar()
|
||||
|
||||
pool_menu = bar.addMenu("&Pool")
|
||||
self._add_action(pool_menu, "Nastavit pool…", self.set_pool, "Ctrl+P")
|
||||
self._add_action(pool_menu, "Nastavit Filmotéku (výstup)…", self.set_filmoteka)
|
||||
pool_menu.addSeparator()
|
||||
self._add_action(pool_menu, "Znovu načíst pool", self.reload_pool, "F5")
|
||||
pool_menu.addSeparator()
|
||||
self._add_action(pool_menu, "Konec", self.close, "Ctrl+Q")
|
||||
|
||||
movie_menu = bar.addMenu("&Filmy")
|
||||
self._add_action(movie_menu, "Importovat film…", self.import_movie, "Ctrl+I")
|
||||
self._add_action(movie_menu, "Přiřadit štítky…", self.assign_tags, "Ctrl+T")
|
||||
self._add_action(movie_menu, "Nastavit datum…", self.set_date, "Ctrl+D")
|
||||
self._add_action(movie_menu, "Upravit ČSFD odkaz…", self.edit_csfd)
|
||||
self._add_action(movie_menu, "Načíst tagy z ČSFD", self.apply_csfd_tags_for_selected)
|
||||
movie_menu.addSeparator()
|
||||
self._add_action(movie_menu, "Odebrat z poolu…", self.remove_movies, "Del")
|
||||
|
||||
film_menu = bar.addMenu("Filmo&téka")
|
||||
self._add_action(film_menu, "Vygenerovat Filmotéku", self.generate_filmoteka, "Ctrl+G")
|
||||
self._add_action(film_menu, "Copy-as-is složky…", self.edit_copyasis_folders)
|
||||
|
||||
def _add_action(self, menu: QMenu, text: str, slot, shortcut: str | None = None) -> QAction:
|
||||
action = QAction(text, self)
|
||||
if shortcut:
|
||||
action.setShortcut(QKeySequence(shortcut))
|
||||
action.triggered.connect(slot)
|
||||
menu.addAction(action)
|
||||
return action
|
||||
|
||||
def _build_central(self) -> None:
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Sidebar — tag filter
|
||||
sidebar = QWidget()
|
||||
side_layout = QVBoxLayout(sidebar)
|
||||
side_layout.setContentsMargins(4, 4, 4, 4)
|
||||
side_layout.addWidget(QLabel("📂 Štítky (filtr)"))
|
||||
self.tag_tree = QTreeWidget()
|
||||
self.tag_tree.setHeaderHidden(True)
|
||||
self.tag_tree.itemChanged.connect(self._on_tag_filter_changed)
|
||||
side_layout.addWidget(self.tag_tree)
|
||||
splitter.addWidget(sidebar)
|
||||
|
||||
# Main — search + movie table
|
||||
main = QWidget()
|
||||
main_layout = QVBoxLayout(main)
|
||||
main_layout.setContentsMargins(4, 4, 4, 4)
|
||||
|
||||
search_row = QHBoxLayout()
|
||||
search_row.addWidget(QLabel("🔍"))
|
||||
self.search_edit = QLineEdit()
|
||||
self.search_edit.setPlaceholderText("Hledat film…")
|
||||
self.search_edit.textChanged.connect(self.refresh_table)
|
||||
search_row.addWidget(self.search_edit)
|
||||
import_btn = QPushButton("➕ Importovat film")
|
||||
import_btn.clicked.connect(self.import_movie)
|
||||
search_row.addWidget(import_btn)
|
||||
main_layout.addLayout(search_row)
|
||||
|
||||
self.table = QTableWidget(0, 5)
|
||||
self.table.setHorizontalHeaderLabels(["Název", "Datum", "Štítky", "Velikost", "ČSFD"])
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.table.customContextMenuRequested.connect(self._show_table_menu)
|
||||
self.table.doubleClicked.connect(lambda _: self.open_movies())
|
||||
self.table.itemSelectionChanged.connect(self._update_selection_status)
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
main_layout.addWidget(self.table)
|
||||
|
||||
splitter.addWidget(main)
|
||||
splitter.setStretchFactor(0, 0)
|
||||
splitter.setStretchFactor(1, 1)
|
||||
splitter.setSizes([260, 940])
|
||||
self.setCentralWidget(splitter)
|
||||
|
||||
def _build_statusbar(self) -> None:
|
||||
self.status = self.statusBar()
|
||||
self._update_path_status()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Sidebar (tag filter)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def refresh_sidebar(self) -> None:
|
||||
self.tag_tree.blockSignals(True)
|
||||
self.tag_tree.clear()
|
||||
counts: dict[str, int] = {}
|
||||
for f in self.filehandler.filelist:
|
||||
for t in f.tags:
|
||||
counts[t.full_path] = counts.get(t.full_path, 0) + 1
|
||||
|
||||
for category in self.tagmanager.get_categories():
|
||||
cat_item = QTreeWidgetItem([category])
|
||||
cat_item.setFlags(Qt.ItemIsEnabled)
|
||||
self.tag_tree.addTopLevelItem(cat_item)
|
||||
cat_item.setExpanded(True)
|
||||
for tag in self.tagmanager.get_tags_in_category(category):
|
||||
count = counts.get(tag.full_path, 0)
|
||||
label = f"{tag.name} ({count})" if count else tag.name
|
||||
item = QTreeWidgetItem([label])
|
||||
item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
|
||||
item.setCheckState(0, Qt.Unchecked)
|
||||
item.setData(0, Qt.UserRole, tag.full_path)
|
||||
cat_item.addChild(item)
|
||||
self.tag_tree.blockSignals(False)
|
||||
|
||||
def _on_tag_filter_changed(self, _item, _col) -> None:
|
||||
self.refresh_table()
|
||||
|
||||
def _checked_filter_tags(self) -> List[Tag]:
|
||||
tags: List[Tag] = []
|
||||
for i in range(self.tag_tree.topLevelItemCount()):
|
||||
cat = self.tag_tree.topLevelItem(i)
|
||||
for j in range(cat.childCount()):
|
||||
child = cat.child(j)
|
||||
if child.checkState(0) == Qt.Checked:
|
||||
full_path = child.data(0, Qt.UserRole)
|
||||
category, name = full_path.split("/", 1)
|
||||
tags.append(Tag(category, name))
|
||||
return tags
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Movie table
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def refresh_table(self, *_args) -> None:
|
||||
filtered = self.filehandler.filter_files_by_tags(self._checked_filter_tags())
|
||||
search = self.search_edit.text().lower() if hasattr(self, "search_edit") else ""
|
||||
if search:
|
||||
filtered = [f for f in filtered if search in (f.title or f.filename).lower()]
|
||||
filtered.sort(key=lambda f: (f.title or f.filename).lower())
|
||||
|
||||
self.table.setRowCount(len(filtered))
|
||||
self.file_rows.clear()
|
||||
for row, f in enumerate(filtered):
|
||||
self.file_rows[row] = f
|
||||
name = f.title or f.filename
|
||||
tags = ", ".join(t.name for t in f.tags)
|
||||
try:
|
||||
size = self._format_size(f.file_path.stat().st_size)
|
||||
except OSError:
|
||||
size = "?"
|
||||
csfd = "🔗" if f.csfd_link else ""
|
||||
for col, value in enumerate([name, f.date or "", tags, size, csfd]):
|
||||
self.table.setItem(row, col, QTableWidgetItem(value))
|
||||
|
||||
self.refresh_sidebar()
|
||||
self._update_selection_status()
|
||||
self.status.showMessage(f"Zobrazeno {len(filtered)} filmů", 4000)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size_bytes: float) -> str:
|
||||
for unit in ["B", "KB", "MB", "GB"]:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.1f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.1f} TB"
|
||||
|
||||
def _selected_movies(self) -> List[File]:
|
||||
rows = {idx.row() for idx in self.table.selectionModel().selectedRows()}
|
||||
return [self.file_rows[r] for r in sorted(rows) if r in self.file_rows]
|
||||
|
||||
def _show_table_menu(self, pos) -> None:
|
||||
menu = QMenu(self)
|
||||
menu.addAction("Otevřít", self.open_movies)
|
||||
menu.addAction("Přiřadit štítky…", self.assign_tags)
|
||||
menu.addAction("Nastavit datum…", self.set_date)
|
||||
menu.addAction("Upravit ČSFD odkaz…", self.edit_csfd)
|
||||
menu.addAction("Načíst tagy z ČSFD", self.apply_csfd_tags_for_selected)
|
||||
menu.addSeparator()
|
||||
menu.addAction("Odebrat z poolu…", self.remove_movies)
|
||||
menu.exec(self.table.viewport().mapToGlobal(pos))
|
||||
|
||||
def _update_selection_status(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if not files:
|
||||
self._update_path_status()
|
||||
return
|
||||
total = 0
|
||||
for f in files:
|
||||
try:
|
||||
total += f.file_path.stat().st_size
|
||||
except OSError:
|
||||
pass
|
||||
self.status.showMessage(f"{len(files)} vybráno — {self._format_size(total)}")
|
||||
|
||||
def _update_path_status(self) -> None:
|
||||
pool = self.filehandler.pool_dir
|
||||
out = self.filehandler.filmoteka_dir
|
||||
self.status.showMessage(
|
||||
f"Pool: {pool or '—'} | Filmotéka: {out or '—'} | "
|
||||
f"{len(self.filehandler.filelist)} filmů"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def set_pool(self) -> None:
|
||||
folder = QFileDialog.getExistingDirectory(self, "Vyber složku poolu")
|
||||
if not folder:
|
||||
return
|
||||
self.filehandler.set_pool_dir(Path(folder))
|
||||
self.filehandler.load_pool_movies()
|
||||
self.refresh_table()
|
||||
self._update_path_status()
|
||||
|
||||
def set_filmoteka(self) -> None:
|
||||
folder = QFileDialog.getExistingDirectory(self, "Vyber výstupní složku Filmotéky")
|
||||
if not folder:
|
||||
return
|
||||
self.filehandler.set_filmoteka_dir(Path(folder))
|
||||
self._update_path_status()
|
||||
|
||||
def reload_pool(self) -> None:
|
||||
if not self.filehandler.movies_dir:
|
||||
QMessageBox.information(self, "Pool", "Nejprve nastavte pool.")
|
||||
return
|
||||
self.filehandler.load_pool_movies()
|
||||
self.refresh_table()
|
||||
|
||||
def import_movie(self) -> None:
|
||||
if not self.filehandler.movies_dir:
|
||||
QMessageBox.warning(self, "Pool", "Nejprve nastavte pool (menu Pool → Nastavit pool).")
|
||||
return
|
||||
path, _ = QFileDialog.getOpenFileName(self, "Vyber video soubor")
|
||||
if not path:
|
||||
return
|
||||
source = Path(path)
|
||||
dialog = ImportMovieDialog(self, default_title=source.stem)
|
||||
if dialog.exec() != QDialog.Accepted:
|
||||
return
|
||||
try:
|
||||
movie = self.filehandler.import_movie(source, dialog.title, dialog.csfd_link)
|
||||
except Exception as exc: # noqa: BLE001 — surface any import failure to the user
|
||||
QMessageBox.critical(self, "Chyba importu", str(exc))
|
||||
return
|
||||
|
||||
# If a ČSFD link was given, enrich the movie with tags right away
|
||||
if movie.csfd_link:
|
||||
self.status.showMessage("Načítám z ČSFD…")
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
try:
|
||||
_, tags_total, errors = self._fetch_csfd_for([movie])
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
if errors:
|
||||
QMessageBox.warning(self, "ČSFD", "Tagy se nepodařilo načíst:\n" + errors[0])
|
||||
else:
|
||||
self.status.showMessage(
|
||||
f"Importováno: {movie.title} (+{tags_total} tagů z ČSFD)", 5000
|
||||
)
|
||||
|
||||
self.refresh_table()
|
||||
self.status.showMessage(f"Importováno: {dialog.title}", 5000)
|
||||
|
||||
def open_movies(self) -> None:
|
||||
for f in self._selected_movies():
|
||||
self._open_path(f.file_path)
|
||||
|
||||
def _open_path(self, path: Path) -> None:
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
os.startfile(path) # type: ignore[attr-defined]
|
||||
elif sys.platform.startswith("darwin"):
|
||||
subprocess.call(["open", str(path)])
|
||||
else:
|
||||
subprocess.call(["xdg-open", str(path)])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
QMessageBox.critical(self, "Chyba", f"Nelze otevřít {path}: {exc}")
|
||||
|
||||
def assign_tags(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if not files:
|
||||
QMessageBox.information(self, "Štítky", "Nejprve vyberte filmy.")
|
||||
return
|
||||
dialog = AssignTagsDialog(self, self.tagmanager, files)
|
||||
if dialog.exec() != QDialog.Accepted:
|
||||
return
|
||||
for full_path, state in dialog.result_map.items():
|
||||
category, name = full_path.split("/", 1)
|
||||
if state == 1:
|
||||
self.filehandler.assign_tag_to_file_objects(files, self.tagmanager.add_tag(category, name))
|
||||
elif state == 0:
|
||||
self.filehandler.remove_tag_from_file_objects(files, Tag(category, name))
|
||||
self.refresh_table()
|
||||
|
||||
def set_date(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if not files:
|
||||
QMessageBox.information(self, "Datum", "Nejprve vyberte filmy.")
|
||||
return
|
||||
text, ok = QInputDialog.getText(self, "Nastavit datum", "Datum (YYYY-MM-DD, prázdné = smazat):")
|
||||
if not ok:
|
||||
return
|
||||
for f in files:
|
||||
f.set_date(text.strip() or None)
|
||||
self.refresh_table()
|
||||
|
||||
def edit_csfd(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if len(files) != 1:
|
||||
QMessageBox.information(self, "ČSFD", "Vyberte právě jeden film.")
|
||||
return
|
||||
f = files[0]
|
||||
text, ok = QInputDialog.getText(self, "ČSFD odkaz", "URL:", text=f.csfd_link or "")
|
||||
if not ok:
|
||||
return
|
||||
f.set_csfd_link(text.strip() or None)
|
||||
self.refresh_table()
|
||||
|
||||
def apply_csfd_tags_for_selected(self) -> None:
|
||||
files = [f for f in self._selected_movies() if f.csfd_link]
|
||||
if not files:
|
||||
QMessageBox.information(
|
||||
self, "ČSFD", "Vyberte filmy, které mají nastavený ČSFD odkaz."
|
||||
)
|
||||
return
|
||||
|
||||
self.status.showMessage(f"Načítám z ČSFD ({len(files)})…")
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
try:
|
||||
ok_count, tags_total, errors = self._fetch_csfd_for(files)
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
|
||||
self.refresh_table()
|
||||
msg = f"Načteno: {ok_count}/{len(files)} filmů, přidáno {tags_total} tagů."
|
||||
if errors:
|
||||
msg += "\n\nChyby:\n" + "\n".join(errors[:5])
|
||||
QMessageBox.warning(self, "ČSFD dokončeno s chybami", msg)
|
||||
else:
|
||||
QMessageBox.information(self, "ČSFD", msg)
|
||||
|
||||
def _fetch_csfd_for(self, files) -> tuple[int, int, list[str]]:
|
||||
"""Apply CSFD tags to each file; return (ok_count, tags_added, errors)."""
|
||||
ok_count = 0
|
||||
tags_total = 0
|
||||
errors: list[str] = []
|
||||
for f in files:
|
||||
result = f.apply_csfd_tags()
|
||||
if result["success"]:
|
||||
ok_count += 1
|
||||
tags_total += len(result["tags_added"])
|
||||
else:
|
||||
errors.append(f"{f.title or f.filename}: {result['error']}")
|
||||
return ok_count, tags_total, errors
|
||||
|
||||
def remove_movies(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if not files:
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self, "Odebrat z poolu",
|
||||
f"Smazat {len(files)} film(ů) z poolu včetně souboru a metadat?",
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
return
|
||||
for f in files:
|
||||
try:
|
||||
f.delete_metadata()
|
||||
if f.file_path.exists():
|
||||
f.file_path.unlink()
|
||||
except OSError as exc:
|
||||
QMessageBox.warning(self, "Chyba", f"Nelze smazat {f.filename}: {exc}")
|
||||
if f in self.filehandler.filelist:
|
||||
self.filehandler.filelist.remove(f)
|
||||
self.refresh_table()
|
||||
|
||||
def generate_filmoteka(self) -> None:
|
||||
out = self.filehandler.filmoteka_dir
|
||||
if not out:
|
||||
QMessageBox.warning(self, "Filmotéka", "Nejprve nastavte výstupní složku Filmotéky.")
|
||||
return
|
||||
files = self.filehandler.filelist
|
||||
if not files:
|
||||
QMessageBox.information(self, "Filmotéka", "Pool je prázdný.")
|
||||
return
|
||||
manager = HardlinkManager(out)
|
||||
created, create_fail, removed, remove_fail = manager.sync_structure(files, FILMOTEKA_CATEGORIES)
|
||||
|
||||
# Copy-as-is folders (e.g. Seriály): mirror each 1:1 (hardlinked)
|
||||
pool = self.filehandler.pool_dir
|
||||
mirrored = 0
|
||||
mirror_fail = 0
|
||||
for name in self.filehandler.copyasis_folders:
|
||||
src = pool / name if pool else None
|
||||
if src and src.is_dir() and any(src.iterdir()):
|
||||
m_created, m_failed = manager.mirror_as_is(src, name)
|
||||
mirrored += m_created
|
||||
mirror_fail += m_failed
|
||||
|
||||
msg = (
|
||||
f"Filmy — vytvořeno: {created}, odebráno zastaralých: {removed}\n"
|
||||
f"Copy-as-is — zrcadleno: {mirrored}"
|
||||
)
|
||||
if create_fail or remove_fail or mirror_fail:
|
||||
msg += f"\nSelhalo: {create_fail + remove_fail + mirror_fail}"
|
||||
QMessageBox.warning(self, "Filmotéka dokončena s chybami", msg)
|
||||
else:
|
||||
QMessageBox.information(self, "Filmotéka vygenerována", msg)
|
||||
self.status.showMessage(
|
||||
f"Filmotéka: filmy +{created}/-{removed}, copy-as-is +{mirrored}", 5000
|
||||
)
|
||||
|
||||
def edit_copyasis_folders(self) -> None:
|
||||
current = ", ".join(self.filehandler.copyasis_folders)
|
||||
text, ok = QInputDialog.getText(
|
||||
self, "Copy-as-is složky",
|
||||
"Názvy pool podsložek zrcadlených 1:1 (oddělené čárkou):", text=current
|
||||
)
|
||||
if not ok:
|
||||
return
|
||||
self.filehandler.set_copyasis_folders(text.split(","))
|
||||
self.status.showMessage(
|
||||
"Copy-as-is složky: " + ", ".join(self.filehandler.copyasis_folders), 5000
|
||||
)
|
||||
|
||||
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
|
||||
save_global_config(self.filehandler.global_config)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def run(filehandler: FileManager, tagmanager: TagManager) -> None:
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
window = QtApp(filehandler, tagmanager)
|
||||
window.show()
|
||||
app.exec()
|
||||
Reference in New Issue
Block a user