""" 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 subprocess import sys from pathlib import Path from typing import ClassVar, cast from PySide6.QtCore import QRegularExpression, Qt, QTimer from PySide6.QtGui import QAction, QKeySequence, QRegularExpressionValidator from PySide6.QtWidgets import ( QAbstractItemView, QApplication, QButtonGroup, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QMenu, QMessageBox, QProgressDialog, QPushButton, QRadioButton, QSpinBox, QSplitter, QTableWidget, QTableWidgetItem, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, ) from src.constants import APP_TITLE from src.core import series from src.core.file import File from src.core.file_manager import FileManager from src.core.hardlink_manager import HardlinkManager from src.core.movie import ( DEFAULT_VERSION_LABEL, normalize_3d_label, normalize_part_label, ) from src.core.naming import ( THREE_D_FORMATS, canonical_pool_stem, is_valid_year, parse_3d_format, parse_pool_stem, parse_tmdb_id, three_d_format_from_filename, year_from_filename, ) from src.core.tag import Tag from src.core.tag_manager import TagManager # Special auto-generated Filmotéka folders. The "- " prefix makes DLNA/TV # browsers sort them ahead of the genre folders at the output root. TIPS_SUBFOLDER = "- Tipy dne" # random selection, refreshed each run RECENT_SUBFOLDER = "- Nově přidané" # most recently added movies TIPS_COUNT = 15 # random tips RECENT_COUNT = 10 # newest by date added # Series found inside a "mixed" library (movies + series in one folder) have no # genre tags to run through the tag tree — they're hardcopied here instead, # same as a dedicated series library's copy-as-is mirror. MIXED_SERIES_SUBFOLDER = "Seriály" class ImportMoviesDialog(QDialog): """Collect a Title + Year + TMDb id per file for a batch import into the pool. One row per source file (filename shown; Title, Year and TMDb id editable). "Dohledat z TMDb" fills them from TMDb — by the given id, or by searching the filename. A single toggle decides whether the files are copied (default) or moved into the pool. """ def __init__( self, parent: QWidget, sources: list[Path], existing_names: set[str] | None = None, libraries: list[str] | None = None, ) -> None: super().__init__(parent) self.setWindowTitle("Importovat filmy do poolu") self.setMinimumSize(760, 360) self._libraries = libraries or ["Filmy"] # Lower-cased canonical stems already present in the pool (collision check) self.existing_names = {n.lower() for n in (existing_names or set())} # (source path, title field, year field, tmdb-id field, ČSFD-link field) per row self._rows: list[tuple[Path, QLineEdit, QLineEdit, QLineEdit, QLineEdit]] = [] # A row set here isn't imported on its own — it becomes another # playable version of the row it points to (e.g. a 3D + a 2D file of # the same film), added via FileManager.add_movie_version right after # its target is imported. {version source: (target source, popisek)}. self._version_links: dict[Path, tuple[Path, str]] = {} self._version_buttons: dict[Path, QPushButton] = {} # 3D formát for a row that stays a flat, single-file movie (the only # file for that film, itself entirely 3D) — disabled whenever the row # is a version of another one or another row is a version of it, since # a grouped movie's 3D-ness lives on the specific version's popisek. self._format3d_combos: dict[Path, QComboBox] = {} layout = QVBoxLayout(self) info = QLabel( "Filmy se uloží jako „Název (rok) [tmdbid-N].ext“ — přesně jak je " "páruje Jellyfin. Rok je povinný; TMDb ID zajistí správné spárování " "(nepovinné). Modře = navrženo z TMDb (ověř); červeně = název už v poolu je. " "Je jen jeden soubor a je celý 3D → nastav mu rovnou „3D formát“. Je-li " "soubor jen další verzí filmu z jiného řádku (2D+3D, jiná kvalita, …), " "použij „Přiřadit jako verzi…“ místo vyplňování vlastního názvu/roku." ) info.setWordWrap(True) layout.addWidget(info) self.table = QTableWidget(0, 8) self.table.setHorizontalHeaderLabels( [ "Soubor", "Název", "Rok", "TMDb ID", "ČSFD odkaz", "3D formát", "Verze", "", ] ) self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(7, QHeaderView.ResizeMode.ResizeToContents) layout.addWidget(self.table) add_row = QHBoxLayout() add_btn = QPushButton("➕ Přidat soubory…") add_btn.clicked.connect(self._add_files) add_row.addWidget(add_btn) find_btn = QPushButton("🔎 Dohledat z TMDb") find_btn.setToolTip( "Podle TMDb ID doplní název + rok; bez ID vyhledá podle názvu souboru." ) find_btn.clicked.connect(self._autofill_tmdb) add_row.addWidget(find_btn) csfd_btn = QPushButton("🎬 Dohledat přes ČSFD") csfd_btn.setToolTip( "Najde film na ČSFD (podle vyplněného odkazu, jinak podle názvu " "souboru), vezme z něj původní název + rok a podle nich dohledá " "TMDb ID. Vhodné, když přímé hledání na TMDb vrací špatný film." ) csfd_btn.clicked.connect(self._autofill_via_csfd) add_row.addWidget(csfd_btn) add_row.addStretch(1) layout.addLayout(add_row) opts_row = QHBoxLayout() opts_row.addWidget(QLabel("Knihovna:")) self.library_combo = QComboBox() self.library_combo.addItems(self._libraries) opts_row.addWidget(self.library_combo) self.move_check = QCheckBox("Přesunout soubory do poolu (jinak zkopírovat)") opts_row.addWidget(self.move_check) self.skip_no_tmdb_check = QCheckBox("Vynechat řádky bez TMDb ID") self.skip_no_tmdb_check.setToolTip( "Při importu přeskočí soubory bez vyplněného TMDb ID " "(pak ani nevadí chybějící rok). Přeskočené řádky jsou přeškrtnuté." ) self.skip_no_tmdb_check.toggled.connect(self._refresh_skip_marks) opts_row.addWidget(self.skip_no_tmdb_check) opts_row.addStretch(1) layout.addLayout(opts_row) # sources dropped by "Vynechat řádky bez TMDb ID" on accept self._skipped_no_tmdb: list[Path] = [] buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) for source in sources: self._append_row(source) def _append_row(self, source: Path) -> None: row = self.table.rowCount() self.table.insertRow(row) name_item = QTableWidgetItem(source.name) name_item.setFlags(Qt.ItemFlag.ItemIsEnabled) self.table.setItem(row, 0, name_item) title_edit = QLineEdit(source.stem) title_edit.textChanged.connect(lambda _t, e=title_edit: self._mark_collision(e)) year_edit = QLineEdit(year_from_filename(source.name) or "") year_edit.setPlaceholderText("rok") year_edit.setMaximumWidth(64) year_edit.setValidator( QRegularExpressionValidator(QRegularExpression(r"\d{0,4}"), year_edit) ) year_edit.textChanged.connect(lambda _t, e=title_edit: self._mark_collision(e)) # Manual edits clear the "suggested" (blue) marker; TMDb autofill re-adds it. year_edit.textEdited.connect( lambda _t, e=year_edit: self._set_suggested(e, False) ) tmdb_edit = QLineEdit() tmdb_edit.setPlaceholderText("TMDb ID") tmdb_edit.setMaximumWidth(96) tmdb_edit.setValidator( QRegularExpressionValidator(QRegularExpression(r"\d{0,9}"), tmdb_edit) ) tmdb_edit.textChanged.connect(lambda _t, e=title_edit: self._mark_collision(e)) tmdb_edit.textChanged.connect(lambda _t: self._refresh_skip_marks()) tmdb_edit.textEdited.connect( lambda _t, e=tmdb_edit: self._set_suggested(e, False) ) csfd_edit = QLineEdit() csfd_edit.setPlaceholderText("ČSFD odkaz (volitelné; uloží se + pro dohledání)") csfd_edit.textEdited.connect( lambda _t, e=csfd_edit: self._set_suggested(e, False) ) # Pre-fill the row from metadata already sitting next to the source file: # Curator's own pre-index sidecar (`.{name}.!tag`) / an old `.Curator.!index` # first (authoritative for the ČSFD link), then a Kodi/Jellyfin `.nfo`. # Everything filled this way is blue (suggested — verify). self._prefill_row(source, title_edit, year_edit, tmdb_edit, csfd_edit) format3d_combo = QComboBox() format3d_combo.addItem("— žádný —", None) for fmt in THREE_D_FORMATS: format3d_combo.addItem(f"3D {fmt}", fmt) detected = three_d_format_from_filename(source.name) if detected: format3d_combo.setCurrentIndex(format3d_combo.findData(detected)) format3d_combo.setToolTip( "Je-li tenhle soubor jediná (a celá 3D) verze filmu, nastav mu tu " "3D formát — uloží se rovnou jako „Title (rok) [tmdbid].3D." "