Added function for media resolution tagging
This commit is contained in:
12
config.json
Normal file
12
config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ignore_patterns": [
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.mp3",
|
||||
"*/M/*",
|
||||
"*/L/*",
|
||||
"*/Ostatní/*",
|
||||
"*.hidden*"
|
||||
],
|
||||
"last_folder": "/media/veracrypt3"
|
||||
}
|
||||
22
src/core/config.py
Normal file
22
src/core/config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_FILE = Path("config.json")
|
||||
|
||||
default_config = {
|
||||
"ignore_patterns": [],
|
||||
"last_folder": None
|
||||
}
|
||||
|
||||
def load_config():
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return default_config.copy()
|
||||
return default_config.copy()
|
||||
|
||||
def save_config(cfg: dict):
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
||||
@@ -21,6 +21,10 @@ class File:
|
||||
self.ignored = False
|
||||
self.tags = []
|
||||
self.date = None
|
||||
if self.tagmanager:
|
||||
tag = self.tagmanager.add_tag("Stav", "Nové")
|
||||
self.tags.append(tag)
|
||||
self.save_metadata()
|
||||
else:
|
||||
self.load_metadata()
|
||||
|
||||
@@ -93,4 +97,4 @@ class File:
|
||||
return
|
||||
if tag_obj in self.tags:
|
||||
self.tags.remove(tag_obj)
|
||||
self.save_metadata()
|
||||
self.save_metadata()
|
||||
@@ -3,6 +3,8 @@ from .file import File
|
||||
from .tag_manager import TagManager
|
||||
from .utils import list_files
|
||||
from typing import Iterable
|
||||
import fnmatch
|
||||
from src.core.config import load_config, save_config
|
||||
|
||||
class FileManager:
|
||||
def __init__(self, tagmanager: TagManager):
|
||||
@@ -10,18 +12,30 @@ class FileManager:
|
||||
self.folders: list[Path] = []
|
||||
self.tagmanager = tagmanager
|
||||
self.on_files_changed = None # callback do GUI
|
||||
self.config = load_config()
|
||||
|
||||
def append(self, folder: Path) -> None:
|
||||
self.folders.append(folder)
|
||||
self.config["last_folder"] = str(folder)
|
||||
save_config(self.config)
|
||||
|
||||
ignore_patterns = self.config.get("ignore_patterns", [])
|
||||
for each in list_files(folder):
|
||||
if each.name.endswith(".!tag"): # ignoruj metadata soubory
|
||||
if each.name.endswith(".!tag"):
|
||||
continue
|
||||
|
||||
full_path = each.as_posix() # celá cesta jako string
|
||||
|
||||
# kontrolujeme jméno i celou cestu
|
||||
if any(
|
||||
fnmatch.fnmatch(each.name, pat) or fnmatch.fnmatch(full_path, pat)
|
||||
for pat in ignore_patterns
|
||||
):
|
||||
continue
|
||||
|
||||
file_obj = File(each, self.tagmanager)
|
||||
self.filelist.append(file_obj)
|
||||
|
||||
if self.on_files_changed:
|
||||
self.on_files_changed(self.filelist)
|
||||
|
||||
def assign_tag_to_file_objects(self, files_objs: list[File], tag):
|
||||
"""Přiřadí tag (Tag nebo 'category/name' string) ke každému souboru v seznamu."""
|
||||
for f in files_objs:
|
||||
@@ -87,4 +101,4 @@ class FileManager:
|
||||
file_tags = {t.full_path for t in f.tags}
|
||||
if all(tag in file_tags for tag in target_full_paths):
|
||||
filtered.append(f)
|
||||
return filtered
|
||||
return filtered
|
||||
@@ -1,14 +0,0 @@
|
||||
# Module header
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit("This module is not intended to be executed as the main program.")
|
||||
|
||||
# Imports
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
# Functions
|
||||
def load_icon(path) -> ImageTk.PhotoImage:
|
||||
img = Image.open(path)
|
||||
img = img.resize((16, 16), Image.Resampling.LANCZOS)
|
||||
return ImageTk.PhotoImage(img)
|
||||
@@ -17,4 +17,4 @@ class ListManager:
|
||||
# sort by date (None last) — nejnovější nahoře? Zde dávám None jako ""
|
||||
def date_key(f):
|
||||
return (f.date is None, f.date or "")
|
||||
return sorted(files, key=date_key)
|
||||
return sorted(files, key=date_key)
|
||||
42
src/core/media_utils.py
Normal file
42
src/core/media_utils.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Module header
|
||||
import sys
|
||||
from .file import File
|
||||
from .tag_manager import TagManager
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit("This module is not intended to be executed as the main program.")
|
||||
|
||||
# Imports
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
# Functions
|
||||
def load_icon(path) -> ImageTk.PhotoImage:
|
||||
img = Image.open(path)
|
||||
img = img.resize((16, 16), Image.Resampling.LANCZOS)
|
||||
return ImageTk.PhotoImage(img)
|
||||
|
||||
def add_video_resolution_tag(file_obj: File, tagmanager: TagManager):
|
||||
"""
|
||||
Zjistí vertikální rozlišení videa a přiřadí tag Rozlišení/{výška}p.
|
||||
Vyžaduje ffprobe (FFmpeg).
|
||||
"""
|
||||
path = str(file_obj.file_path)
|
||||
try:
|
||||
# ffprobe vrátí width a height ve formátu JSON
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
res = result.stdout.strip() # např. "1920x1080"
|
||||
if "x" not in res:
|
||||
return
|
||||
width, height = map(int, res.split("x"))
|
||||
tag_name = f"Rozlišení/{height}p"
|
||||
tag_obj = tagmanager.add_tag("Rozlišení", f"{height}p")
|
||||
file_obj.add_tag(tag_obj)
|
||||
print(f"Přiřazen tag {tag_name} k {file_obj.filename}")
|
||||
except Exception as e:
|
||||
print(f"Chyba při získávání rozlišení videa {file_obj.filename}: {e}")
|
||||
@@ -19,4 +19,4 @@ class Tag:
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.category, self.name))
|
||||
return hash((self.category, self.name))
|
||||
@@ -33,4 +33,4 @@ class TagManager:
|
||||
return list(self.tags_by_category.keys())
|
||||
|
||||
def get_tags_in_category(self, category: str):
|
||||
return list(self.tags_by_category.get(category, []))
|
||||
return list(self.tags_by_category.get(category, []))
|
||||
@@ -4,4 +4,4 @@ def list_files(folder_path: str | Path) -> list[Path]:
|
||||
folder = Path(folder_path)
|
||||
if not folder.is_dir():
|
||||
raise NotADirectoryError(f"{folder} není platná složka.")
|
||||
return [file_path for file_path in folder.rglob("*") if file_path.is_file()]
|
||||
return [file_path for file_path in folder.rglob("*") if file_path.is_file()]
|
||||
107
src/ui/gui.py
107
src/ui/gui.py
@@ -6,13 +6,16 @@ from tkinter import ttk, simpledialog, messagebox, filedialog
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from src.core.image import load_icon
|
||||
from src.core.media_utils import load_icon
|
||||
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.list_manager import ListManager
|
||||
from src.core.constants import APP_NAME, VERSION, APP_VIEWPORT
|
||||
from src.core.config import save_config # <-- doplněno
|
||||
|
||||
|
||||
|
||||
|
||||
class TagSelectionDialog(tk.Toplevel):
|
||||
@@ -40,7 +43,7 @@ class TagSelectionDialog(tk.Toplevel):
|
||||
btn_frame = tk.Frame(self)
|
||||
btn_frame.pack(pady=5)
|
||||
tk.Button(btn_frame, text="OK", command=self.on_ok).pack(side="left", padx=5)
|
||||
tk.Button(btn_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5)
|
||||
tk.Button(btn_frame, text="Zrušit", command=self.destroy).pack(side="left", padx=5)
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
@@ -127,7 +130,7 @@ class MultiFileTagAssignDialog(tk.Toplevel):
|
||||
|
||||
class App:
|
||||
def __init__(self, filehandler: FileManager, tagmanager: TagManager):
|
||||
self.states = {} # tree states (checkboxy) item_id -> bool
|
||||
self.states = {}
|
||||
self.listbox_map: dict[int, list[File]] = {}
|
||||
self.selected_tree_item_for_context = None
|
||||
self.selected_list_index_for_context = None
|
||||
@@ -135,15 +138,48 @@ class App:
|
||||
self.tagmanager = tagmanager
|
||||
self.list_manager = ListManager()
|
||||
|
||||
# nové proměnné
|
||||
# tady jen připravíme proměnnou, ale nevytváříme BooleanVar!
|
||||
self.hide_ignored_var = None
|
||||
|
||||
self.filter_text = ""
|
||||
self.show_full_path = False
|
||||
self.sort_mode = "name"
|
||||
self.sort_order = "asc"
|
||||
|
||||
# callback z FileManageru
|
||||
self.filehandler.on_files_changed = self.update_files_from_manager
|
||||
|
||||
def detect_video_resolution(self):
|
||||
files = self.get_selected_files_objects()
|
||||
if not files:
|
||||
self.status_bar.config(text="Nebyly vybrány žádné soubory")
|
||||
return
|
||||
|
||||
count = 0
|
||||
for f in files:
|
||||
try:
|
||||
path = str(f.file_path)
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=height", "-of", "csv=p=0", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
height_str = result.stdout.strip()
|
||||
if not height_str.isdigit():
|
||||
continue
|
||||
height = int(height_str)
|
||||
tag_name = f"{height}p"
|
||||
tag_obj = self.tagmanager.add_tag("Rozlišení", tag_name)
|
||||
f.add_tag(tag_obj)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f"Chyba u {f.filename}: {e}")
|
||||
|
||||
self.update_files_from_manager(self.filehandler.filelist)
|
||||
self.status_bar.config(text=f"Přiřazeno rozlišení tagů k {count} souborům")
|
||||
|
||||
|
||||
# ==================================================
|
||||
# MAIN GUI
|
||||
# ==================================================
|
||||
@@ -153,6 +189,16 @@ class App:
|
||||
root.geometry(APP_VIEWPORT)
|
||||
self.root = root
|
||||
|
||||
# teď už máme root, takže můžeme vytvořit BooleanVar
|
||||
self.hide_ignored_var = tk.BooleanVar(value=False, master=root)
|
||||
|
||||
last = self.filehandler.config.get("last_folder")
|
||||
if last:
|
||||
try:
|
||||
self.filehandler.append(Path(last))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---- Ikony
|
||||
unchecked = load_icon("src/resources/images/32/32_unchecked.png")
|
||||
checked = load_icon("src/resources/images/32/32_checked.png")
|
||||
@@ -165,12 +211,27 @@ class App:
|
||||
# ---- Layout
|
||||
menu_bar = tk.Menu(root)
|
||||
root.config(menu=menu_bar)
|
||||
|
||||
file_menu = tk.Menu(menu_bar, tearoff=0)
|
||||
file_menu.add_command(label="Open Folder...", command=self.open_folder_dialog)
|
||||
file_menu.add_command(label="Set date for selected...", command=self.set_date_for_selected)
|
||||
file_menu.add_command(label="Nastavit ignorované vzory", command=self.set_ignore_patterns)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_command(label="Exit", command=root.quit)
|
||||
menu_bar.add_cascade(label="File", menu=file_menu)
|
||||
|
||||
view_menu = tk.Menu(menu_bar, tearoff=0)
|
||||
view_menu.add_checkbutton(
|
||||
label="Skrýt ignorované",
|
||||
variable=self.hide_ignored_var,
|
||||
command=self.toggle_hide_ignored
|
||||
)
|
||||
function_menu = tk.Menu(menu_bar, tearoff=0)
|
||||
function_menu.add_command(label="Nastavit datum", command=self.set_date_for_selected)
|
||||
function_menu.add_command(label="Detekovat rozlišení u videí", command=self.detect_video_resolution)
|
||||
|
||||
|
||||
menu_bar.add_cascade(label="Soubor", menu=file_menu)
|
||||
menu_bar.add_cascade(label="Pohled", menu=view_menu)
|
||||
menu_bar.add_cascade(label="Funkce", menu=function_menu)
|
||||
|
||||
main_frame = tk.Frame(root)
|
||||
main_frame.pack(fill="both", expand=True)
|
||||
@@ -192,7 +253,7 @@ class App:
|
||||
|
||||
# Filter + buttons row
|
||||
filter_frame = tk.Frame(right_frame)
|
||||
filter_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0,4))
|
||||
filter_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 4))
|
||||
filter_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.filter_entry = tk.Entry(filter_frame)
|
||||
@@ -224,14 +285,14 @@ class App:
|
||||
|
||||
# ---- Context menus
|
||||
self.tree_menu = tk.Menu(root, tearoff=0)
|
||||
self.tree_menu.add_command(label="Nový tag zde", command=self.tree_add_tag)
|
||||
self.tree_menu.add_command(label="Smazat tag", command=self.tree_delete_tag)
|
||||
self.tree_menu.add_command(label="Nový štítek", command=self.tree_add_tag)
|
||||
self.tree_menu.add_command(label="Smazat štítek", command=self.tree_delete_tag)
|
||||
|
||||
self.list_menu = tk.Menu(root, tearoff=0)
|
||||
self.list_menu.add_command(label="Otevřít soubor", command=self.list_open_file)
|
||||
self.list_menu.add_command(label="Smazat z indexu", command=self.list_remove_file)
|
||||
self.list_menu.add_command(label="Assign Tag", command=self.assign_tag_to_selected)
|
||||
self.list_menu.add_command(label="Assign Tag (advanced)...", command=self.assign_tag_to_selected_bulk)
|
||||
self.list_menu.add_command(label="Nastavit datum", command=self.set_date_for_selected)
|
||||
self.list_menu.add_command(label="Přiřadit štítek", command=self.assign_tag_to_selected_bulk)
|
||||
|
||||
# ---- Root node
|
||||
root_id = self.tree.insert("", "end", text="Štítky", image=self.icons["tag"])
|
||||
@@ -244,9 +305,23 @@ class App:
|
||||
|
||||
root.mainloop()
|
||||
|
||||
|
||||
# ==================================================
|
||||
# FILTER + SORT TOGGLES
|
||||
# ==================================================
|
||||
def set_ignore_patterns(self):
|
||||
current = ", ".join(self.filehandler.config.get("ignore_patterns", []))
|
||||
s = simpledialog.askstring("Ignore patterns", "Zadej patterny oddělené čárkou (např. *.png, *.tmp):", initialvalue=current)
|
||||
if s is None:
|
||||
return
|
||||
patterns = [p.strip() for p in s.split(",") if p.strip()]
|
||||
self.filehandler.config["ignore_patterns"] = patterns
|
||||
save_config(self.filehandler.config)
|
||||
self.update_files_from_manager(self.filehandler.filelist)
|
||||
|
||||
def toggle_hide_ignored(self):
|
||||
self.update_files_from_manager(self.filehandler.filelist)
|
||||
|
||||
def on_filter_changed(self):
|
||||
self.filter_text = self.filter_entry.get().strip().lower()
|
||||
self.update_files_from_manager(self.filehandler.filelist)
|
||||
@@ -285,6 +360,14 @@ class App:
|
||||
(self.show_full_path and self.filter_text in str(f.file_path).lower())
|
||||
]
|
||||
|
||||
if self.hide_ignored_var and self.hide_ignored_var.get():
|
||||
filtered_files = [
|
||||
f for f in filtered_files
|
||||
if "Stav/Ignorované" not in {t.full_path for t in f.tags}
|
||||
]
|
||||
|
||||
|
||||
|
||||
# řazení
|
||||
reverse = (self.sort_order == "desc")
|
||||
if self.sort_mode == "name":
|
||||
|
||||
Reference in New Issue
Block a user