Add statistics and data-consistency menus, recently-added folder

This commit is contained in:
2026-07-02 06:10:14 +02:00
parent b5c2023212
commit f0f38d4257
17 changed files with 498 additions and 78 deletions
+111 -7
View File
@@ -30,6 +30,12 @@ from src.core.tag import Tag
from src.constants import APP_TITLE
from src.core.hardlink_manager import HardlinkManager
# 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
class ImportMoviesDialog(QDialog):
@@ -408,6 +414,44 @@ class TagSchemaDialog(QDialog):
self.accept()
class StatsDialog(QDialog):
"""Read-only overview of the pool: totals + per-category tag breakdown."""
def __init__(self, parent: QWidget, stats: dict, size_text: str) -> None:
super().__init__(parent)
self.setWindowTitle("Statistiky knihovny")
self.setMinimumSize(460, 560)
layout = QVBoxLayout(self)
avg = stats["avg_rating"]
summary = (
f"<b>Filmů v poolu:</b> {stats['count']}<br>"
f"<b>Celková velikost:</b> {size_text}<br>"
f"<b>Průměrné hodnocení ČSFD:</b> "
f"{str(avg) + ' %' if avg is not None else ''}<br>"
f"<b>S ČSFD odkazem:</b> {stats['with_csfd']} &nbsp;·&nbsp; "
f"<b>bez:</b> {stats['without_csfd']}<br>"
f"<b>Bez štítků:</b> {stats['untagged']}"
)
layout.addWidget(QLabel(summary))
tree = QTreeWidget()
tree.setHeaderLabels(["Kategorie / štítek", "Počet"])
tree.setColumnWidth(0, 300)
for category in sorted(stats["categories"]):
entries = stats["categories"][category]
cat_item = QTreeWidgetItem([f"{category} ({len(entries)})", ""])
tree.addTopLevelItem(cat_item)
for name, count in entries:
cat_item.addChild(QTreeWidgetItem([name, str(count)]))
layout.addWidget(tree)
buttons = QDialogButtonBox(QDialogButtonBox.Close)
buttons.rejected.connect(self.reject)
buttons.accepted.connect(self.accept)
layout.addWidget(buttons)
class QtApp(QMainWindow):
def __init__(self, filehandler: FileManager, tagmanager: TagManager) -> None:
super().__init__()
@@ -470,6 +514,11 @@ class QtApp(QMainWindow):
settings_menu = bar.addMenu("&Nastavení")
self._add_action(settings_menu, "Tag schéma…", self.edit_tag_schema)
self._add_action(settings_menu, "Statistiky…", self.show_statistics)
tests_menu = bar.addMenu("&Testy")
self._add_action(tests_menu, "Kontrola konzistence dat…", self.check_consistency)
def _add_action(self, menu: QMenu, text: str, slot, shortcut: str | None = None) -> QAction:
action = QAction(text, self)
if shortcut:
@@ -957,7 +1006,8 @@ class QtApp(QMainWindow):
QMessageBox.information(self, "Filmotéka", "Pool je prázdný.")
return
manager = HardlinkManager(out)
reserved = set(self.filehandler.copyasis_folders) | {"Tipy dne"}
reserved = (set(self.filehandler.copyasis_folders)
| {TIPS_SUBFOLDER, RECENT_SUBFOLDER})
created, create_fail, removed, remove_fail = manager.sync_structure(
files,
category_roots=self.filehandler.filmoteka_category_roots(),
@@ -978,22 +1028,26 @@ class QtApp(QMainWindow):
mirrored += m_created
mirror_fail += m_failed
# "Tipy dne": 10 random pool movies (copy-as-is folders excluded)
tips, tips_fail = manager.generate_random_tips(files, count=10)
# Special folders (pool only; copy-as-is excluded): random tips + newest
tips, tips_fail = manager.generate_random_tips(
files, count=TIPS_COUNT, subfolder=TIPS_SUBFOLDER)
recent, recent_fail = manager.generate_recently_added(
files, count=RECENT_COUNT, subfolder=RECENT_SUBFOLDER)
msg = (
f"Filmy — vytvořeno: {created}, odebráno zastaralých: {removed}\n"
f"Copy-as-is — zrcadleno: {mirrored}\n"
f"Tipy dne — náhodně: {tips}"
f"Tipy dne — náhodně: {tips}, Nově přidané: {recent}"
)
if create_fail or remove_fail or mirror_fail or tips_fail:
msg += f"\nSelhalo: {create_fail + remove_fail + mirror_fail + tips_fail}"
failed = create_fail + remove_fail + mirror_fail + tips_fail + recent_fail
if failed:
msg += f"\nSelhalo: {failed}"
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}, "
f"tipy +{tips}", 5000
f"tipy +{tips}, nově +{recent}", 5000
)
def edit_copyasis_folders(self) -> None:
@@ -1021,6 +1075,56 @@ class QtApp(QMainWindow):
8000,
)
def show_statistics(self) -> None:
if not self.filehandler.filelist:
QMessageBox.information(self, "Statistiky", "Pool je prázdný.")
return
stats = self.filehandler.statistics()
StatsDialog(self, stats, self._format_size(stats["total_size"])).exec()
def check_consistency(self) -> None:
"""Test: does the pool index match the actual files in pool/Filmy?"""
if not self.filehandler.movies_dir:
QMessageBox.information(self, "Testy", "Nejprve nastavte pool.")
return
result = self.filehandler.check_data_consistency()
if not result["ok"]:
QMessageBox.warning(self, "Kontrola konzistence", result["error"])
return
missing, untracked = result["missing"], result["untracked"]
if not missing and not untracked:
QMessageBox.information(
self, "Kontrola konzistence dat",
f"✅ Vše v pořádku — index a pool si odpovídají.\n\n"
f"Záznamů v indexu: {result['index_count']}\n"
f"Souborů v poolu: {result['disk_count']}",
)
return
detail: list[str] = []
if missing:
detail.append(
f"■ Metadata bez souboru ({len(missing)}) — soubor byl smazán "
"přímo, záznam v indexu zůstal:")
detail += [f"{k}" for k in missing]
detail.append("")
if untracked:
detail.append(
f"■ Soubory bez metadat ({len(untracked)}) — přidané do poolu "
"mimo import:")
detail += [f"{k}" for k in untracked]
box = QMessageBox(self)
box.setIcon(QMessageBox.Warning)
box.setWindowTitle("Kontrola konzistence — nalezeny nesrovnalosti")
box.setText(
f"Metadata bez souboru: {len(missing)}\n"
f"Soubory bez metadat: {len(untracked)}\n\n"
"Podrobnosti zobrazíš přes „Show Details…\".")
box.setDetailedText("\n".join(detail))
box.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