CSFD integration

This commit is contained in:
2025-12-30 07:54:30 +01:00
parent 028c6606e0
commit 47b39aadfe
20 changed files with 2597 additions and 129 deletions

View File

@@ -9,12 +9,12 @@ from tkinter import ttk, simpledialog, messagebox, filedialog
from pathlib import Path
from typing import List
from src.core.media_utils import load_icon
from src.ui.utils import load_icon
from src.core.file_manager import FileManager
from src.core.tag_manager import TagManager, DEFAULT_TAG_ORDER
from src.core.file import File
from src.core.tag import Tag
from src.core.list_manager import ListManager
# ListManager removed - sorting implemented directly in GUI
from src.core.constants import APP_NAME, VERSION, APP_VIEWPORT
from src.core.config import save_global_config
from src.core.hardlink_manager import HardlinkManager
@@ -219,8 +219,6 @@ class App:
def __init__(self, filehandler: FileManager, tagmanager: TagManager):
self.filehandler = filehandler
self.tagmanager = tagmanager
self.list_manager = ListManager()
# State
self.states = {}
self.file_items = {} # Treeview item_id -> File object mapping
@@ -231,6 +229,7 @@ class App:
self.sort_mode = "name"
self.sort_order = "asc"
self.category_colors = {} # category -> color mapping
self.show_csfd_column = True # CSFD column visibility
self.filehandler.on_files_changed = self.update_files_from_manager
@@ -312,6 +311,7 @@ class App:
# File menu
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label="Open Folder... (Ctrl+O)", command=self.open_folder_dialog)
file_menu.add_command(label="Zavřít složku (Ctrl+W)", command=self.close_folder)
file_menu.add_command(label="Nastavit ignorované vzory", command=self.set_ignore_patterns)
file_menu.add_separator()
file_menu.add_command(label="Exit (Ctrl+Q)", command=self.root.quit)
@@ -323,6 +323,12 @@ class App:
variable=self.hide_ignored_var,
command=self.toggle_hide_ignored
)
self.show_csfd_var = tk.BooleanVar(value=True, master=self.root)
view_menu.add_checkbutton(
label="Zobrazit CSFD sloupec",
variable=self.show_csfd_var,
command=self.toggle_csfd_column
)
view_menu.add_command(label="Refresh (F5)", command=self.refresh_all)
# Tools menu
@@ -331,6 +337,9 @@ class App:
tools_menu.add_command(label="Detekovat rozlišení videí", command=self.detect_video_resolution)
tools_menu.add_command(label="Přiřadit tagy (Ctrl+T)", command=self.assign_tag_to_selected_bulk)
tools_menu.add_separator()
tools_menu.add_command(label="Nastavit CSFD URL...", command=self.set_csfd_url_for_selected)
tools_menu.add_command(label="Načíst tagy z CSFD", command=self.apply_csfd_tags_for_selected)
tools_menu.add_separator()
tools_menu.add_command(label="Nastavit hardlink složku...", command=self.configure_hardlink_folder)
tools_menu.add_command(label="Aktualizovat hardlink strukturu", command=self.update_hardlink_structure)
tools_menu.add_command(label="Vytvořit hardlink strukturu...", command=self.create_hardlink_structure)
@@ -428,22 +437,27 @@ class App:
table_frame = tk.Frame(file_frame)
table_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Define columns
columns = ("name", "date", "tags", "size")
# Define columns (including CSFD)
columns = ("name", "date", "tags", "csfd", "size")
self.file_table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="extended")
# Column headers with sort commands
self.file_table.heading("name", text="📄 Název ▲", command=lambda: self.sort_by_column("name"))
self.file_table.heading("date", text="📅 Datum", command=lambda: self.sort_by_column("date"))
self.file_table.heading("tags", text="🏷️ Štítky")
self.file_table.heading("csfd", text="🎬 CSFD")
self.file_table.heading("size", text="💾 Velikost", command=lambda: self.sort_by_column("size"))
# Column widths
self.file_table.column("name", width=300)
self.file_table.column("date", width=100)
self.file_table.column("tags", width=200)
self.file_table.column("csfd", width=50)
self.file_table.column("size", width=80)
# Load CSFD column visibility from folder config
self._update_csfd_column_visibility()
# Scrollbars
vsb = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.file_table.yview)
hsb = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.file_table.xview)
@@ -493,6 +507,7 @@ class App:
# Tag context menu
self.tag_menu = tk.Menu(self.root, tearoff=0)
self.tag_menu.add_command(label="Nový štítek", command=self.tree_add_tag)
self.tag_menu.add_command(label="Přejmenovat štítek", command=self.tree_rename_tag)
self.tag_menu.add_command(label="Smazat štítek", command=self.tree_delete_tag)
# File context menu
@@ -501,11 +516,15 @@ class App:
self.file_menu.add_command(label="Přiřadit štítky (Ctrl+T)", command=self.assign_tag_to_selected_bulk)
self.file_menu.add_command(label="Nastavit datum (Ctrl+D)", command=self.set_date_for_selected)
self.file_menu.add_separator()
self.file_menu.add_command(label="Nastavit CSFD URL...", command=self.set_csfd_url_for_selected)
self.file_menu.add_command(label="Načíst tagy z CSFD", command=self.apply_csfd_tags_for_selected)
self.file_menu.add_separator()
self.file_menu.add_command(label="Smazat z indexu (Del)", command=self.remove_selected_files)
def _bind_shortcuts(self):
"""Bind keyboard shortcuts"""
self.root.bind("<Control-o>", lambda e: self.open_folder_dialog())
self.root.bind("<Control-w>", lambda e: self.close_folder())
self.root.bind("<Control-q>", lambda e: self.root.quit())
self.root.bind("<Control-t>", lambda e: self.assign_tag_to_selected_bulk())
self.root.bind("<Control-d>", lambda e: self.set_date_for_selected())
@@ -570,6 +589,9 @@ class App:
self.tag_tree.tag_configure(f"cat_{category}", foreground=color)
self.tag_tree.tag_configure(f"tag_{category}", foreground=color)
# Force tree update
self.tag_tree.update_idletasks()
def update_tag_counts(self, filtered_files):
"""Update tag counts in sidebar based on filtered files"""
if not hasattr(self, 'tag_tree_items'):
@@ -669,6 +691,134 @@ class App:
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(text=f"Smazán tag: {name}")
def tree_rename_tag(self):
"""Rename selected tag or category"""
item = self.selected_tree_item_for_context
if not item:
return
# Don't allow renaming root
if item == self.root_tag_id:
return
parent_id = self.tag_tree.parent(item)
current_text = self.tag_tree.item(item, "text").strip()
# Check if this is a category (parent is root) or a tag
is_category = (parent_id == self.root_tag_id)
if is_category:
# Renaming a category
current_name = current_text.replace("📁 ", "")
new_name = simpledialog.askstring(
"Přejmenovat kategorii",
f"Nový název kategorie '{current_name}':",
initialvalue=current_name
)
if not new_name or new_name == current_name:
return
# Check if new name already exists - offer merge
if new_name in self.tagmanager.get_categories():
merge = messagebox.askyesno(
"Kategorie existuje",
f"Kategorie '{new_name}' již existuje.\n\n"
f"Chcete sloučit kategorii '{current_name}' do '{new_name}'?\n\n"
f"Všechny štítky z '{current_name}' budou přesunuty do '{new_name}'.",
icon="question"
)
if not merge:
return
# Merge category in all files
updated_count = self.filehandler.merge_category_in_files(current_name, new_name)
# Refresh sidebar
self.refresh_sidebar()
self.root.update_idletasks()
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(
text=f"Kategorie sloučena: {current_name}{new_name} ({updated_count} souborů)"
)
return
# Rename category in all files
updated_count = self.filehandler.rename_category_in_files(current_name, new_name)
# Refresh sidebar
self.refresh_sidebar()
self.root.update_idletasks()
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(
text=f"Kategorie přejmenována: {current_name}{new_name} ({updated_count} souborů)"
)
else:
# Renaming a tag
# Get tag name (without count suffix)
# Find the tag name from the mapping
tag_name = None
for full_path, (item_id, name) in self.tag_tree_items.items():
if item_id == item:
tag_name = name
break
if tag_name is None:
# Fallback: parse from text (remove leading spaces and count)
tag_name = current_text.lstrip()
# Remove count suffix like " (5)"
import re
tag_name = re.sub(r'\s*\(\d+\)\s*$', '', tag_name)
category = self.tag_tree.item(parent_id, "text").replace("📁 ", "")
new_name = simpledialog.askstring(
"Přejmenovat štítek",
f"Nový název štítku '{tag_name}':",
initialvalue=tag_name
)
if not new_name or new_name == tag_name:
return
# Check if new name already exists in this category - offer merge
existing_tags = [t.name for t in self.tagmanager.get_tags_in_category(category)]
if new_name in existing_tags:
merge = messagebox.askyesno(
"Štítek existuje",
f"Štítek '{new_name}' v kategorii '{category}' již existuje.\n\n"
f"Chcete sloučit '{tag_name}' do '{new_name}'?\n\n"
f"Všechny soubory s '{tag_name}' budou mít tento štítek nahrazen za '{new_name}'.",
icon="question"
)
if not merge:
return
# Merge tag in all files
updated_count = self.filehandler.merge_tag_in_files(category, tag_name, new_name)
# Refresh sidebar
self.refresh_sidebar()
self.root.update_idletasks()
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(
text=f"Štítek sloučen: {category}/{tag_name}{category}/{new_name} ({updated_count} souborů)"
)
return
# Rename tag in all files
updated_count = self.filehandler.rename_tag_in_files(category, tag_name, new_name)
# Refresh sidebar
self.refresh_sidebar()
self.root.update_idletasks()
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(
text=f"Štítek přejmenován: {category}/{tag_name}{category}/{new_name} ({updated_count} souborů)"
)
def get_checked_tags(self) -> List[Tag]:
"""Get list of checked tags"""
tags = []
@@ -740,13 +890,16 @@ class App:
if len(f.tags) > 3:
tags += f" +{len(f.tags) - 3}"
# CSFD indicator
csfd = "" if f.csfd_url else ""
try:
size = f.file_path.stat().st_size
size_str = self._format_size(size)
except:
size_str = "?"
item_id = self.file_table.insert("", "end", values=(name, date, tags, size_str))
item_id = self.file_table.insert("", "end", values=(name, date, tags, csfd, size_str))
self.file_items[item_id] = f
# Update status
@@ -838,11 +991,27 @@ class App:
f.tagmanager.add_tag(t.category, t.name)
self.status_label.config(text=f"Přidána složka: {folder_path}")
self._update_csfd_column_visibility() # Load CSFD column setting for new folder
self.refresh_sidebar()
self.update_files_from_manager(self.filehandler.filelist)
except Exception as e:
messagebox.showerror("Chyba", f"Nelze přidat složku {folder}: {e}")
def close_folder(self):
"""Close current folder safely"""
if not self.filehandler.current_folder:
self.status_label.config(text="Žádná složka není otevřena")
return
folder_name = self.filehandler.current_folder.name
# Close folder (saves metadata and clears state)
self.filehandler.close_folder()
# Refresh UI
self.refresh_sidebar()
self.status_label.config(text=f"Složka zavřena: {folder_name}")
def open_selected_files(self):
"""Open selected files"""
files = self.get_selected_files()
@@ -974,6 +1143,96 @@ class App:
self.show_full_path = not self.show_full_path
self.update_files_from_manager(self.filehandler.filelist)
def toggle_csfd_column(self):
"""Toggle CSFD column visibility"""
self.show_csfd_column = self.show_csfd_var.get()
self._update_csfd_column_visibility()
# Save to folder config
if self.filehandler.current_folder:
folder_config = self.filehandler.get_folder_config()
folder_config["show_csfd_column"] = self.show_csfd_column
self.filehandler.save_folder_config(config=folder_config)
def _update_csfd_column_visibility(self):
"""Update CSFD column width based on visibility setting"""
# Load from folder config if available
if self.filehandler.current_folder:
folder_config = self.filehandler.get_folder_config()
self.show_csfd_column = folder_config.get("show_csfd_column", True)
if hasattr(self, 'show_csfd_var'):
self.show_csfd_var.set(self.show_csfd_column)
# Update column width
if hasattr(self, 'file_table'):
if self.show_csfd_column:
self.file_table.column("csfd", width=50)
else:
self.file_table.column("csfd", width=0)
def set_csfd_url_for_selected(self):
"""Set CSFD URL for selected files"""
files = self.get_selected_files()
if not files:
self.status_label.config(text="Nebyly vybrány žádné soubory")
return
# Get current URL from first file
current_url = files[0].csfd_url or ""
prompt = "Zadej CSFD URL (např. https://www.csfd.cz/film/9423-pane-vy-jste-vdova/):"
url = simpledialog.askstring("Nastavit CSFD URL", prompt, initialvalue=current_url, parent=self.root)
if url is None:
return
for f in files:
f.set_csfd_url(url if url != "" else None)
self.update_files_from_manager(self.filehandler.filelist)
self.status_label.config(text=f"CSFD URL nastaveno pro {len(files)} soubor(ů)")
def apply_csfd_tags_for_selected(self):
"""Load tags from CSFD for selected files"""
files = self.get_selected_files()
if not files:
self.status_label.config(text="Nebyly vybrány žádné soubory")
return
# Filter files with CSFD URL
files_with_url = [f for f in files if f.csfd_url]
if not files_with_url:
messagebox.showwarning("Upozornění", "Žádný z vybraných souborů nemá nastavenou CSFD URL")
return
self.status_label.config(text=f"Načítám tagy z CSFD pro {len(files_with_url)} souborů...")
self.root.update()
success_count = 0
error_count = 0
all_tags_added = []
for f in files_with_url:
result = f.apply_csfd_tags()
if result["success"]:
success_count += 1
all_tags_added.extend(result["tags_added"])
else:
error_count += 1
# Refresh sidebar to show new categories
self.refresh_sidebar()
self.root.update_idletasks() # Force UI refresh
self.update_files_from_manager(self.filehandler.filelist)
# Show result
if error_count > 0:
messagebox.showwarning("Dokončeno s chybami",
f"Úspěšně: {success_count}, Chyby: {error_count}\n"
f"Přidáno {len(all_tags_added)} tagů")
else:
self.status_label.config(
text=f"Načteno z CSFD: {success_count} souborů, přidáno {len(all_tags_added)} tagů")
def sort_by_column(self, column: str):
"""Sort by column header click"""
if self.sort_mode == column: