# Tagger - Project Documentation **Version:** 1.2.0 | **Status:** Stable | **GUI:** PySide6/Qt6 --- ## About Desktop app for organizing files using hierarchical tags (category/name). **Features:** Folder scanning, tag filtering (AND/OR/NOT), rename/merge tags, CSFD.cz integration (with local cache), hardlink structure, 3-level config (global/folder/file), orphaned sidecar detection. --- ## Structure ``` Tagger/ ├── Tagger.py # Entry point ├── src/core/ # Business logic (NO UI imports!) │ ├── tag.py # Tag value object (immutable) │ ├── tag_manager.py # Tag/category management │ ├── file.py # File with metadata + CSFD cache │ ├── file_manager.py # File management, filtering, orphan detection │ ├── config.py # 3-level config system │ ├── hardlink_manager.py │ ├── csfd.py # CSFD scraper + CSFDMovie serialization │ ├── media_utils.py # ffprobe integration │ ├── constants.py # APP_NAME, VERSION │ └── _version.py # Version fallback for PyInstaller ├── src/ui/ │ ├── gui.py # Qt6 GUI (MainWindow) │ └── utils.py # load_icon() └── tests/ # 274 tests ``` --- ## Architecture Rules 1. **UI must not contain business logic** — call FileManager/TagManager 2. **Core must not import UI** — no PySide6 in src/core/ 3. **Dependency injection** — pass via constructor 4. **UTF-8 everywhere** — `encoding='utf-8'`, `ensure_ascii=False` --- ## Config Files | Level | File | Location | Contents | |-------|------|----------|----------| | Global | `.Tagger.!gtag` | `~/.config/Tagger/` | window geometry, last folder, recent folders | | Folder | `.Tagger.!ftag` | project folder | ignore patterns, hardlink settings | | File | `.filename.!tag` | same dir as file | tags, date, csfd_url, csfd_cache | --- ## Key Components **Tag** — immutable, `Tag(category, name)`, `Tag.from_string("cat/name")` **File** — `file_path`, `tags[]`, `date`, `csfd_url`, `csfd_cache`, metadata in `.filename.!tag` - `apply_csfd_tags()` — fetch + cache CSFD data - `get_cached_movie()` — return CSFDMovie from cache (no network) - `set_csfd_url()` — invalidates cache on URL change **TagManager** — `add_tag()`, `get_categories()`, `rename_tag()`, `merge_tag()` **FileManager** — `append(folder)`, `filter_files_by_tags()`, `close_folder()` - `on_orphaned_tags` callback — fires when orphaned `.!tag` sidecars are found - `find_orphaned_tags()` — manual scan for orphaned sidecars **HardlinkManager** — `create_structure_for_files()`, `sync_structure()` **CSFDMovie** — `to_dict()` / `from_dict()` for cache serialization --- ## Filtering ```python # AND (default — all must match) fm.filter_files_by_tags(["Žánr/Drama", "Rok/1990"]) # OR — at least one must match fm.filter_files_by_tags(any_of=["Žánr/Drama", "Žánr/Thriller"]) # NOT — none of these fm.filter_files_by_tags(must_not=["Stav/Nové"]) # Combined fm.filter_files_by_tags( must_have=["Žánr/Drama"], any_of=["Rok/1990", "Rok/1991"], must_not=["Stav/Nové"], ) ``` --- ## Running ```bash poetry run python Tagger.py # GUI poetry run pytest -q # Tests poetry run pyinstaller --onefile Tagger.py # Build ``` --- ## Shortcuts `Ctrl+O` Open | `Ctrl+T` Tags | `Ctrl+D` Date | `Ctrl+W` Close | `F5` Refresh | `Del` Remove --- ## Debugging **Version 0.0.0 in build:** Run app once from source to update `_version.py`, then rebuild. **Cannot import:** Use `poetry run python Tagger.py` **Metadata corrupted:** Auto-recovers with defaults. **Config not saved:** Check `~/.config/Tagger/` exists and is writable. --- --- ## Metrics - **Tests:** 274 ✅ - **Python:** 3.14+ - **Dependencies:** PySide6, requests, beautifulsoup4, loguru, python-dotenv