Add statistics and data-consistency menus, recently-added folder
This commit is contained in:
+111
-7
@@ -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']} · "
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user