Add per-movie attributes and per-category filename templates
This commit is contained in:
+336
-44
@@ -20,25 +20,16 @@ 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,
|
||||
QHeaderView, QMenu, QAbstractItemView, QCheckBox, QComboBox,
|
||||
)
|
||||
|
||||
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.constants import APP_TITLE
|
||||
from src.core.hardlink_manager import HardlinkManager
|
||||
|
||||
# Layout of the generated Filmotéka tree: category → root folder under the
|
||||
# output (see PROJECT.md). Genres sit directly at the output root (next to the
|
||||
# copy-as-is Seriály mirror); Rok and Země původu get their own grouping folder.
|
||||
FILMOTEKA_CATEGORY_ROOTS = {
|
||||
"Žánr": "",
|
||||
"Rok": "Dle roku",
|
||||
"Země původu": "Dle země původu",
|
||||
"Hodnocení": "Dle hodnocení",
|
||||
}
|
||||
|
||||
|
||||
class ImportMoviesDialog(QDialog):
|
||||
@@ -49,23 +40,29 @@ class ImportMoviesDialog(QDialog):
|
||||
the files are copied (default, non-destructive) or moved into the pool.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget, sources: List[Path]) -> None:
|
||||
def __init__(
|
||||
self, parent: QWidget, sources: List[Path],
|
||||
existing_names: set[str] | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Importovat filmy do poolu")
|
||||
self.setMinimumSize(680, 360)
|
||||
self.setMinimumSize(720, 360)
|
||||
|
||||
# Lower-cased names already present in the pool (for collision highlight)
|
||||
self.existing_names = {n.lower() for n in (existing_names or set())}
|
||||
# (source path, title field, ČSFD field) per row
|
||||
self._rows: list[tuple[Path, QLineEdit, QLineEdit]] = []
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.table = QTableWidget(0, 3)
|
||||
self.table.setHorizontalHeaderLabels(["Soubor", "Název", "ČSFD odkaz"])
|
||||
self.table = QTableWidget(0, 4)
|
||||
self.table.setHorizontalHeaderLabels(["Soubor", "Název", "ČSFD odkaz", ""])
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
layout.addWidget(self.table)
|
||||
|
||||
add_row = QHBoxLayout()
|
||||
@@ -97,11 +94,35 @@ class ImportMoviesDialog(QDialog):
|
||||
name_item.setFlags(Qt.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))
|
||||
csfd_edit = QLineEdit()
|
||||
csfd_edit.setPlaceholderText("https://www.csfd.cz/film/...")
|
||||
remove_btn = QPushButton("✕")
|
||||
remove_btn.setFixedWidth(28)
|
||||
remove_btn.setToolTip("Odebrat tento soubor z importu")
|
||||
remove_btn.clicked.connect(lambda _c, e=title_edit: self._remove_row(e))
|
||||
self.table.setCellWidget(row, 1, title_edit)
|
||||
self.table.setCellWidget(row, 2, csfd_edit)
|
||||
self.table.setCellWidget(row, 3, remove_btn)
|
||||
self._rows.append((source, title_edit, csfd_edit))
|
||||
self._mark_collision(title_edit)
|
||||
|
||||
def _mark_collision(self, title_edit: QLineEdit) -> None:
|
||||
"""Highlight the title in red when that name already exists in the pool."""
|
||||
clashes = title_edit.text().strip().lower() in self.existing_names
|
||||
title_edit.setStyleSheet(
|
||||
"QLineEdit { color: #c0392b; font-weight: bold; }" if clashes else "")
|
||||
title_edit.setToolTip(
|
||||
"V poolu už existuje film s tímto názvem — při importu dostane "
|
||||
"číselnou příponu." if clashes else "")
|
||||
|
||||
def _remove_row(self, title_edit: QLineEdit) -> None:
|
||||
"""Drop a single file from the import list without restarting the dialog."""
|
||||
for index, (_src, edit, _csfd) in enumerate(self._rows):
|
||||
if edit is title_edit:
|
||||
self.table.removeRow(index)
|
||||
self._rows.pop(index)
|
||||
return
|
||||
|
||||
def _add_files(self) -> None:
|
||||
paths, _ = QFileDialog.getOpenFileNames(self, "Vyber video soubory")
|
||||
@@ -157,13 +178,19 @@ class AssignTagsDialog(QDialog):
|
||||
Result maps full_path -> 1 (assign), 0 (remove), 2 (leave mixed/unchanged).
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget, tagmanager: TagManager, files: List[File]) -> None:
|
||||
def __init__(
|
||||
self, parent: QWidget, tagmanager: TagManager, files: List[File],
|
||||
categories: List[str] | None = None,
|
||||
) -> 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]
|
||||
self.tagmanager = tagmanager
|
||||
self._file_tag_sets = [{t.full_path for t in f.tags} for f in files]
|
||||
self._file_count = len(files)
|
||||
self._items: list[tuple[str, QTreeWidgetItem]] = []
|
||||
self._cat_items: dict[str, QTreeWidgetItem] = {}
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(QLabel(f"Vybráno filmů: {len(files)}"))
|
||||
@@ -172,25 +199,17 @@ class AssignTagsDialog(QDialog):
|
||||
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)
|
||||
# Show schema categories (incl. empty ones) ∪ categories that have tags
|
||||
all_categories = list(dict.fromkeys(
|
||||
(categories or []) + self.tagmanager_categories(tagmanager)))
|
||||
for category in all_categories:
|
||||
self._category_item(category)
|
||||
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.ItemIsAutoTristate)
|
||||
item.setCheckState(0, state)
|
||||
cat_item.addChild(item)
|
||||
self._items.append((tag.full_path, item))
|
||||
self._add_tag_item(category, tag.name)
|
||||
|
||||
add_btn = QPushButton("➕ Nový štítek…")
|
||||
add_btn.clicked.connect(self._add_new_tag)
|
||||
layout.addWidget(add_btn)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
@@ -201,12 +220,194 @@ class AssignTagsDialog(QDialog):
|
||||
def tagmanager_categories(tagmanager: TagManager) -> List[str]:
|
||||
return sorted(tagmanager.get_categories())
|
||||
|
||||
def _category_item(self, category: str) -> QTreeWidgetItem:
|
||||
"""Return (creating if needed) the header item for a category."""
|
||||
if category not in self._cat_items:
|
||||
cat_item = QTreeWidgetItem([category])
|
||||
cat_item.setFlags(Qt.ItemIsEnabled)
|
||||
self.tree.addTopLevelItem(cat_item)
|
||||
cat_item.setExpanded(True)
|
||||
self._cat_items[category] = cat_item
|
||||
return self._cat_items[category]
|
||||
|
||||
def _add_tag_item(self, category: str, name: str, force_checked: bool = False):
|
||||
"""Add a checkable tag row under its category (deduplicated)."""
|
||||
full_path = f"{category}/{name}"
|
||||
for fp, item in self._items:
|
||||
if fp == full_path: # already listed → optionally (re)check it
|
||||
if force_checked:
|
||||
item.setCheckState(0, Qt.Checked)
|
||||
return item
|
||||
have = sum(1 for s in self._file_tag_sets if full_path in s)
|
||||
if force_checked or have == self._file_count and self._file_count:
|
||||
state = Qt.Checked
|
||||
elif have == 0:
|
||||
state = Qt.Unchecked
|
||||
else:
|
||||
state = Qt.PartiallyChecked
|
||||
item = QTreeWidgetItem([name])
|
||||
item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsAutoTristate)
|
||||
item.setCheckState(0, state)
|
||||
self._category_item(category).addChild(item)
|
||||
self._items.append((full_path, item))
|
||||
return item
|
||||
|
||||
def _add_new_tag(self) -> None:
|
||||
"""Prompt for a category + value and add it as a checked tag."""
|
||||
known = list(self._cat_items.keys())
|
||||
category, ok = QInputDialog.getItem(
|
||||
self, "Nový štítek", "Kategorie:", known, editable=True)
|
||||
category = category.strip()
|
||||
if not ok or not category:
|
||||
return
|
||||
name, ok = QInputDialog.getText(
|
||||
self, "Nový štítek", f"Název štítku v kategorii „{category}\":")
|
||||
name = name.strip()
|
||||
if not ok or not name:
|
||||
return
|
||||
item = self._add_tag_item(category, name, force_checked=True)
|
||||
self.tree.scrollToItem(item)
|
||||
|
||||
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 TagSchemaDialog(QDialog):
|
||||
"""Editor for the tag schema: categories, their ČSFD source and folder rules.
|
||||
|
||||
Each row defines a tag category, optionally which ČSFD field feeds it (with a
|
||||
value transform), and how it maps into the Filmotéka tree (a grouping folder,
|
||||
the output root, or no folders at all).
|
||||
"""
|
||||
|
||||
# (label, csfd_field) — the CSFDMovie attributes a category can draw from
|
||||
FIELD_CHOICES = [
|
||||
("(žádné — uživatelská)", None),
|
||||
("Žánry (genres)", "genres"),
|
||||
("Rok (year)", "year"),
|
||||
("Země (countries)", "countries"),
|
||||
("Hodnocení (rating)", "rating"),
|
||||
("Režie (directors)", "directors"),
|
||||
("Herci (actors)", "actors"),
|
||||
("Délka (duration)", "duration"),
|
||||
]
|
||||
TRANSFORM_CHOICES = [
|
||||
("(žádný)", None),
|
||||
("Pásmo po 10 (decade_band)", "decade_band"),
|
||||
]
|
||||
|
||||
def __init__(self, parent: QWidget, schema: list[dict]) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Tag schéma — kategorie a pravidla")
|
||||
self.setMinimumSize(760, 420)
|
||||
self.result_schema: list[dict] | None = None
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(QLabel(
|
||||
"Kategorie tagů, jejich zdroj z ČSFD a jak se z nich tvoří složky "
|
||||
"Filmotéky.\nSložka: prázdné = v kořeni výstupu, „Dle…\" = seskupit, "
|
||||
"vypnuté „Tvořit složky\" = jen filtr bez složek.\n"
|
||||
"Šablona názvu: jak pojmenovat hardlink jen v této kategorii, např. "
|
||||
"„{year} - {title}{ext}\" (pole: title/year/rating/ext/stem/filename)."))
|
||||
|
||||
self.table = QTableWidget(0, 6)
|
||||
self.table.setHorizontalHeaderLabels(
|
||||
["Kategorie", "ČSFD pole", "Transform", "Tvořit složky", "Složka",
|
||||
"Šablona názvu"])
|
||||
hdr = self.table.horizontalHeader()
|
||||
hdr.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
hdr.setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
hdr.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
||||
hdr.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
hdr.setSectionResizeMode(4, QHeaderView.Stretch)
|
||||
hdr.setSectionResizeMode(5, QHeaderView.Stretch)
|
||||
layout.addWidget(self.table)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
add_btn = QPushButton("➕ Přidat kategorii")
|
||||
add_btn.clicked.connect(lambda: self._append_row({}))
|
||||
del_btn = QPushButton("➖ Odebrat vybranou")
|
||||
del_btn.clicked.connect(self._remove_selected)
|
||||
btn_row.addWidget(add_btn)
|
||||
btn_row.addWidget(del_btn)
|
||||
btn_row.addStretch(1)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self._save)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
for entry in schema:
|
||||
self._append_row(entry)
|
||||
|
||||
def _append_row(self, entry: dict) -> None:
|
||||
row = self.table.rowCount()
|
||||
self.table.insertRow(row)
|
||||
|
||||
self.table.setCellWidget(row, 0, QLineEdit(entry.get("category", "")))
|
||||
|
||||
field_combo = QComboBox()
|
||||
for label, value in self.FIELD_CHOICES:
|
||||
field_combo.addItem(label, value)
|
||||
field_combo.setCurrentIndex(
|
||||
self._combo_index(self.FIELD_CHOICES, entry.get("csfd_field")))
|
||||
self.table.setCellWidget(row, 1, field_combo)
|
||||
|
||||
transform_combo = QComboBox()
|
||||
for label, value in self.TRANSFORM_CHOICES:
|
||||
transform_combo.addItem(label, value)
|
||||
transform_combo.setCurrentIndex(
|
||||
self._combo_index(self.TRANSFORM_CHOICES, entry.get("transform")))
|
||||
self.table.setCellWidget(row, 2, transform_combo)
|
||||
|
||||
root = entry.get("filmoteka_root", "")
|
||||
make_folders = QCheckBox()
|
||||
make_folders.setChecked(root is not None)
|
||||
self.table.setCellWidget(row, 3, make_folders)
|
||||
|
||||
folder_edit = QLineEdit("" if root is None else root)
|
||||
folder_edit.setPlaceholderText("prázdné = kořen výstupu")
|
||||
self.table.setCellWidget(row, 4, folder_edit)
|
||||
|
||||
template_edit = QLineEdit(entry.get("filename_template") or "")
|
||||
template_edit.setPlaceholderText("výchozí název souboru")
|
||||
self.table.setCellWidget(row, 5, template_edit)
|
||||
|
||||
@staticmethod
|
||||
def _combo_index(choices: list, value) -> int:
|
||||
for i, (_label, v) in enumerate(choices):
|
||||
if v == value:
|
||||
return i
|
||||
return 0
|
||||
|
||||
def _remove_selected(self) -> None:
|
||||
row = self.table.currentRow()
|
||||
if row >= 0:
|
||||
self.table.removeRow(row)
|
||||
|
||||
def _save(self) -> None:
|
||||
schema: list[dict] = []
|
||||
for row in range(self.table.rowCount()):
|
||||
category = self.table.cellWidget(row, 0).text().strip()
|
||||
if not category:
|
||||
continue
|
||||
make_folders = self.table.cellWidget(row, 3).isChecked()
|
||||
folder = self.table.cellWidget(row, 4).text().strip()
|
||||
template = self.table.cellWidget(row, 5).text().strip()
|
||||
schema.append({
|
||||
"category": category,
|
||||
"csfd_field": self.table.cellWidget(row, 1).currentData(),
|
||||
"transform": self.table.cellWidget(row, 2).currentData(),
|
||||
"filmoteka_root": folder if make_folders else None,
|
||||
"filename_template": template or None,
|
||||
})
|
||||
self.result_schema = schema
|
||||
self.accept()
|
||||
|
||||
|
||||
class QtApp(QMainWindow):
|
||||
def __init__(self, filehandler: FileManager, tagmanager: TagManager) -> None:
|
||||
super().__init__()
|
||||
@@ -218,7 +419,7 @@ class QtApp(QMainWindow):
|
||||
self._active_filter: set[str] = set()
|
||||
self.filehandler.on_files_changed = lambda _=None: self.refresh_table()
|
||||
|
||||
self.setWindowTitle(f"{APP_NAME} {VERSION} — Filmotéka")
|
||||
self.setWindowTitle(f"{APP_TITLE} — Filmotéka")
|
||||
geometry = self.filehandler.global_config.get("window_geometry", "1200x800")
|
||||
try:
|
||||
w, h = (int(x) for x in geometry.lower().split("x"))
|
||||
@@ -256,6 +457,7 @@ class QtApp(QMainWindow):
|
||||
self._add_action(movie_menu, "Přejmenovat…", self.rename_movie, "F2")
|
||||
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, "Nastavit atribut…", self.set_attribute)
|
||||
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()
|
||||
@@ -265,6 +467,9 @@ class QtApp(QMainWindow):
|
||||
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)
|
||||
|
||||
settings_menu = bar.addMenu("&Nastavení")
|
||||
self._add_action(settings_menu, "Tag schéma…", self.edit_tag_schema)
|
||||
|
||||
def _add_action(self, menu: QMenu, text: str, slot, shortcut: str | None = None) -> QAction:
|
||||
action = QAction(text, self)
|
||||
if shortcut:
|
||||
@@ -347,13 +552,20 @@ class QtApp(QMainWindow):
|
||||
for t in f.tags:
|
||||
counts[t.full_path] = counts.get(t.full_path, 0) + 1
|
||||
|
||||
# Preserve each category's expanded/collapsed state across the rebuild
|
||||
# (new categories default to expanded).
|
||||
expanded = {}
|
||||
for i in range(self.tag_tree.topLevelItemCount()):
|
||||
cat = self.tag_tree.topLevelItem(i)
|
||||
expanded[cat.text(0)] = cat.isExpanded()
|
||||
|
||||
self.tag_tree.blockSignals(True)
|
||||
self.tag_tree.clear()
|
||||
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)
|
||||
cat_item.setExpanded(expanded.get(category, True))
|
||||
for tag in self.tagmanager.get_tags_in_category(category):
|
||||
count = counts.get(tag.full_path, 0)
|
||||
item = QTreeWidgetItem([f"{tag.name} ({count})"])
|
||||
@@ -433,6 +645,7 @@ class QtApp(QMainWindow):
|
||||
menu.addAction("Přejmenovat…", self.rename_movie)
|
||||
menu.addAction("Přiřadit štítky…", self.assign_tags)
|
||||
menu.addAction("Nastavit datum…", self.set_date)
|
||||
menu.addAction("Nastavit atribut…", self.set_attribute)
|
||||
menu.addAction("Upravit ČSFD odkaz…", self.edit_csfd)
|
||||
menu.addAction("Načíst tagy z ČSFD", self.apply_csfd_tags_for_selected)
|
||||
menu.addSeparator()
|
||||
@@ -495,7 +708,8 @@ class QtApp(QMainWindow):
|
||||
if not paths:
|
||||
return
|
||||
sources = [Path(p) for p in paths]
|
||||
dialog = ImportMoviesDialog(self, sources)
|
||||
existing_names = {Path(f.filename).stem for f in self.filehandler.filelist}
|
||||
dialog = ImportMoviesDialog(self, sources, existing_names)
|
||||
if dialog.exec() != QDialog.Accepted:
|
||||
return
|
||||
entries = dialog.entries()
|
||||
@@ -503,12 +717,25 @@ class QtApp(QMainWindow):
|
||||
return
|
||||
move = dialog.move_files
|
||||
|
||||
# Files whose name already exists in the pool (the red-highlighted rows)
|
||||
colliding = [t for s, t, _ in entries if self.filehandler.pooled_with_stem(t)]
|
||||
on_conflict = "suffix"
|
||||
if colliding:
|
||||
on_conflict = self._resolve_import_conflicts(colliding)
|
||||
if on_conflict is None:
|
||||
return # user cancelled the whole import
|
||||
|
||||
imported: list[File] = []
|
||||
skipped = 0
|
||||
errors: list[str] = []
|
||||
for source, title, csfd_link in entries:
|
||||
try:
|
||||
movie = self.filehandler.import_movie(source, title, csfd_link or None, move=move)
|
||||
imported.append(movie)
|
||||
movie = self.filehandler.import_movie(
|
||||
source, title, csfd_link or None, move=move, on_conflict=on_conflict)
|
||||
if movie is None:
|
||||
skipped += 1
|
||||
else:
|
||||
imported.append(movie)
|
||||
except Exception as exc: # noqa: BLE001 — surface per-file import failures
|
||||
errors.append(f"{source.name}: {exc}")
|
||||
|
||||
@@ -537,6 +764,29 @@ class QtApp(QMainWindow):
|
||||
QMessageBox.information(self, "Import", summary)
|
||||
self.status.showMessage(summary, 5000)
|
||||
|
||||
def _resolve_import_conflicts(self, names: list[str]) -> str | None:
|
||||
"""Ask how to handle names already in the pool. Returns the on_conflict
|
||||
policy ("replace"/"suffix") or None if the user cancels the import."""
|
||||
box = QMessageBox(self)
|
||||
box.setIcon(QMessageBox.Question)
|
||||
box.setWindowTitle("Název už v poolu existuje")
|
||||
listing = "\n".join(f" • {n}" for n in names[:10])
|
||||
more = f"\n … a další ({len(names) - 10})" if len(names) > 10 else ""
|
||||
box.setText(
|
||||
f"V poolu už existují filmy s těmito názvy:\n{listing}{more}\n\n"
|
||||
"Co s nimi?"
|
||||
)
|
||||
replace_btn = box.addButton("Nahradit novými", QMessageBox.AcceptRole)
|
||||
keep_btn = box.addButton("Ponechat oba (přípona)", QMessageBox.ActionRole)
|
||||
box.addButton("Zrušit import", QMessageBox.RejectRole)
|
||||
box.exec()
|
||||
clicked = box.clickedButton()
|
||||
if clicked is replace_btn:
|
||||
return "replace"
|
||||
if clicked is keep_btn:
|
||||
return "suffix"
|
||||
return None
|
||||
|
||||
def open_movies(self) -> None:
|
||||
for f in self._selected_movies():
|
||||
self._open_path(f.file_path)
|
||||
@@ -557,7 +807,8 @@ class QtApp(QMainWindow):
|
||||
if not files:
|
||||
QMessageBox.information(self, "Štítky", "Nejprve vyberte filmy.")
|
||||
return
|
||||
dialog = AssignTagsDialog(self, self.tagmanager, files)
|
||||
schema_categories = [e["category"] for e in self.filehandler.tag_schema]
|
||||
dialog = AssignTagsDialog(self, self.tagmanager, files, schema_categories)
|
||||
if dialog.exec() != QDialog.Accepted:
|
||||
return
|
||||
for full_path, state in dialog.result_map.items():
|
||||
@@ -580,6 +831,30 @@ class QtApp(QMainWindow):
|
||||
f.set_date(text.strip() or None)
|
||||
self.refresh_table()
|
||||
|
||||
def set_attribute(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if not files:
|
||||
QMessageBox.information(self, "Atribut", "Nejprve vyberte filmy.")
|
||||
return
|
||||
known = sorted({k for f in files for k in f.attributes})
|
||||
key, ok = QInputDialog.getItem(
|
||||
self, "Nastavit atribut",
|
||||
"Název atributu (např. collection_sort):", known, editable=True)
|
||||
key = key.strip()
|
||||
if not ok or not key:
|
||||
return
|
||||
# prefill with the current value when editing a single movie
|
||||
current = files[0].attributes.get(key, "") if len(files) == 1 else ""
|
||||
value, ok = QInputDialog.getText(
|
||||
self, "Nastavit atribut", f"Hodnota „{key}\" (prázdné = smazat):",
|
||||
text=current)
|
||||
if not ok:
|
||||
return
|
||||
for f in files:
|
||||
f.set_attribute(key, value.strip() or None)
|
||||
self.refresh_table()
|
||||
self.status.showMessage(f"Atribut „{key}\" nastaven u {len(files)} filmů.", 5000)
|
||||
|
||||
def rename_movie(self) -> None:
|
||||
files = self._selected_movies()
|
||||
if len(files) != 1:
|
||||
@@ -641,8 +916,9 @@ class QtApp(QMainWindow):
|
||||
ok_count = 0
|
||||
tags_total = 0
|
||||
errors: list[str] = []
|
||||
schema = self.filehandler.tag_schema
|
||||
for f in files:
|
||||
result = f.apply_csfd_tags()
|
||||
result = f.apply_csfd_tags(schema)
|
||||
if result["success"]:
|
||||
ok_count += 1
|
||||
tags_total += len(result["tags_added"])
|
||||
@@ -682,7 +958,11 @@ class QtApp(QMainWindow):
|
||||
return
|
||||
manager = HardlinkManager(out)
|
||||
created, create_fail, removed, remove_fail = manager.sync_structure(
|
||||
files, category_roots=FILMOTEKA_CATEGORY_ROOTS
|
||||
files,
|
||||
category_roots=self.filehandler.filmoteka_category_roots(),
|
||||
category_transforms=self.filehandler.filmoteka_category_transforms(),
|
||||
category_filename_templates=(
|
||||
self.filehandler.filmoteka_category_filename_templates()),
|
||||
)
|
||||
|
||||
# Copy-as-is folders (e.g. Seriály): mirror each 1:1 (hardlinked)
|
||||
@@ -722,6 +1002,18 @@ class QtApp(QMainWindow):
|
||||
"Copy-as-is složky: " + ", ".join(self.filehandler.copyasis_folders), 5000
|
||||
)
|
||||
|
||||
def edit_tag_schema(self) -> None:
|
||||
dialog = TagSchemaDialog(self, self.filehandler.tag_schema)
|
||||
if dialog.exec() != QDialog.Accepted or dialog.result_schema is None:
|
||||
return
|
||||
self.filehandler.set_tag_schema(dialog.result_schema)
|
||||
self.refresh_table() # categories/sidebar may have changed
|
||||
self.status.showMessage(
|
||||
f"Tag schéma uloženo ({len(dialog.result_schema)} kategorií). "
|
||||
"Pro nové tagy spusťte „Načíst tagy z ČSFD\" a přegenerujte Filmotéku.",
|
||||
8000,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user