Files
Curator/PROJECT.md
T

20 KiB
Raw Blame History

PROJECT.md

This file is project-specific. Only include information directly related to the concrete project — goals, current state, architecture decisions, known issues, and tasks.

Origin

Curator is a fork of the former Tagger project. The tagging, filtering and hardlink-tree parts are inherited and keep working as before. On top of that, Curator becomes a full movie library manager (Filmotéka).

Core idea

Curator manages a personal movie library based on two folders:

  • Pool — the managed repository of video files. This is the single source of truth. Curator manages the pool itself (insert/remove file), so files are never moved by hand. The pool has exactly two top-level folders: Filmy (movies — tag-based tree) and Seriály (series — a "copy-as-is" folder mirrored 1:1 into the output; see Design decisions). Every file lives here exactly once.
  • Filmotéka (output) — a generated, browsable directory tree made only of hardlinks into the pool (the same mechanism as today's hardlink manager). It is fully disposable: deleting the Filmotéka folder loses nothing, because it can always be regenerated from the pool.

Workflow

  1. The user configures two folders: the pool and the Filmotéka output.
  2. The user picks a video file via "Open file".
  3. Curator opens a dialog to fill in basic info — at minimum the title/name and a ČSFD link.
  4. Curator renames the file and moves it into the managed pool, and writes a metadata file describing it.
  5. From the pool, Curator generates the Filmotéka — a complex tree of hardlinks built from each file's tags/metadata (like the current hardlink manager, but driven by the pool).
  6. Deleting the Filmotéka has no effect on the pool; the tree is regenerated on demand.

Current state

  • Inherited from Tagger: Tag, TagManager, File (sidecar metadata), FileManager (folder scan, filtering, ignore patterns), 3-level config, HardlinkManager (create/sync/cleanup), pytest suite.
  • Rename Tagger → Curator done across code, spec, config filenames (.Curator.!gtag / .Curator.!ftag) and tests.
  • PySide6 GUI (src/ui/qt_app.py) reframed around the Filmotéka workflow is the entry point; the old tkinter src/ui/gui.py is retained for reference.
  • Pool + Filmotéka wired up: global config holds pool_dir / filmoteka_dir; FileManager creates Filmy/Seriály, imports movies (copy → Title.ext), loads the pool, and the GUI generates the Filmotéka tree via HardlinkManager.
  • File carries title + csfd_link. Pool metadata lives in a unified index (<pool>/.Curator.!index, see pool_index.py); File writes there when an index is injected. Without an index a File is an in-memory object only — the old per-file .!tag sidecars and the per-folder .!ftag config have been removed.

GUI decision

The GUI was reframed around the Filmotéka (not kept as a generic tagger) and rewritten in PySide6: Pool/Filmotéka setup, Import movie, tag-filter sidebar, movie table, and one-click Filmotéka generation.

Design decisions

  • Metadata storage: one unified metadata file for the whole pool (a central index), not per-file sidecars. Justified because Curator owns the pool and files are never moved manually, so it is not exposed to path drift.
  • Config split by scope: the global config (.Curator.!gtag) holds app data only — window state, MRU folders, and the pool_dir / filmoteka_dir pointers (which must stay global: the index is found through them). Everything that describes a specific library — tag_schema and copyasis_folders — lives in the pool index under a settings section, so it travels with the pool. FileManager migrates any pre-existing global values into the index on first open. The old per-folder .!ftag config and per-file .!tag sidecars were removed (they were unused Tagger leftovers), and the orphaned tkinter src/ui/gui.py went with them.
  • Import dialog: multi-file — pick several videos at once and give each its own Title + ČSFD link (one row per file, more can be added from the dialog), or auto-filled with "Najít ČSFD odkazy" (cleans each filename into a query and fills the first ČSFD search hit; existing links are kept). A single copy/move toggle decides whether the sources are copied (default) or moved into the pool. Each file is renamed to Title.ext. When a ČSFD link is given, Curator fetches the movie and assigns Žánr / Rok / Země původu / Hodnocení (ten-point band) tags automatically; further tags can be added via the UI. Directors and the first 10 actors are fetched and cached too, but deliberately not turned into tags/folders (there would be too many).
  • Genres / countries: a movie can have multiple genres and, for a co-production, multiple countries of origin (ČSFD writes them slash-separated, e.g. "USA / Velká Británie"). Each becomes its own tag, so the film appears under every matching genre and country branch in the Filmotéka (multiple hardlinks).
  • Pool layout: two top-level folders — Filmy and Seriály. Movies are the first target; the Seriály branch follows the "copy-as-is" rule below.
  • Copy-as-is folders (Seriály): a subfolder inside the pool can be marked as copy / as-is. For such a folder Curator does not build a tag-based tree; instead it mirrors the exact directory hierarchy from the pool into the Filmotéka output, with the files materialized as hardlinks into the pool. So pool/Seriály/... is cloned 1:1 into output/Seriály/... (same structure, hardlinked files). This is how Seriály work.
  • File naming: pooled movies follow the convention Title (YYYY).ext — a clean title plus a four-digit year in parentheses (naming.py: canonical_pool_stem / parse_pool_stem). The year lets two same-named films coexist and makes the filename self-describing (File.name_context recovers the year from the filename when no ČSFD/tag year is present). The GUI import requires a four-digit year (per-row Rok field, pre-filled from the source filename and fillable from ČSFD — a ČSFD-suggested year is tinted blue to be verified, a name colliding with the pool is tinted red). Filmy → "Přejmenovat dle ČSFD (rok)…" (FileManager.rename_all_to_canonical) brings older Title.ext files up to the convention from their ČSFD metadata; files without a known year are skipped. File.title always stays the clean title (no year).
  • Import copy vs move: by default the original file is copied into the pool (non-destructive); the import dialog also offers a move option that relocates the source into the pool instead.
  • Filmotéka tree layout: driven by a category → root-folder map (FILMOTEKA_CATEGORY_ROOTS). At the output root sit the genre folders directly (output/Akční/film, …), next to the copy-as-is mirrors (Seriály), plus two grouping folders: Dle roku (output/Dle roku/<rok>/film) and Dle země původu (output/Dle země původu/<země>/film), plus Dle hodnocení. Each is a hardlink. HardlinkManager supports an empty root (tag folders placed directly at the output root) and restricts obsolete cleanup to the tag-tree's own top-level folders so mirrors are never touched.
  • Tag schema (config-driven, not hard-coded): the categories, their ČSFD source field + transform, and their Filmotéka folder mapping all live in tag_schema in the global config (default config.DEFAULT_TAG_SCHEMA, edited via Nastavení → Tag schéma…). Both apply_csfd_tags (which fields → tags) and the Filmotéka layout (FileManager.filmoteka_category_roots) read from it, so adding a category or changing a folder rule needs no code change. A category can be made filter-only (no folders) by setting its filmoteka_root to null. The transform (e.g. decade_band) shapes only the folder name — tags keep the exact value (rating → tag Hodnocení/90, folder Dle hodnocení/90–100 %); it is applied at Filmotéka generation via filmoteka_category_transforms.
  • Filmotéka link naming (clean title, year on collision): the default hardlink name in the tag tree is the movie's clean title — Title.ext, not the pooled Title (YYYY).ext. When two different films would collide on that name within one folder, the colliding ones keep the disambiguating year (Title (YYYY).ext); a residual same-title-and-year clash gets a stable numeric suffix. HardlinkManager._plan_links resolves the whole tag tree to a collision-free (file, path) plan that both link creation and obsolete-detection consume, so they can never disagree on a name (the previous churn source). Copy-as-is mirrors (Seriály) are unaffected — they mirror 1:1.
  • Per-category filename template (filename_template in a schema entry): overrides the default naming inside that category's folders only, rendered from the movie's metadata (File.name_context: title/year/rating/ext/stem/ filename plus any free-form attributes), e.g. a Kolekce with "{collection_sort} - {title}{ext}". Applied via filmoteka_category_filename_templates.
  • Free-form per-movie attributes (File.attributes, set in the GUI): arbitrary key → value metadata stored in the index and merged into name_context, so custom fields like collection_sort can drive filename templates.
  • Tag provenance (ČSFD vs user): each file records which tags came from ČSFD (csfd_tags). Re-fetching regenerates only those; user-added tags are kept, so changing a movie's ČSFD link refreshes ČSFD tags without losing manual ones.
  • Special folders + "- " prefix: besides the tag tree, generation produces - Tipy dne (15 random) and - Nově přidané (10 newest by File.added, mtime fallback — ctime is bumped by hardlinking). All grouping folders and these specials are prefixed with "- " so DLNA/TV browsers sort them before the genre folders at the output root. Specials are reserved_subfolders so cleanup never touches them; renaming old unprefixed folders is handled automatically (they become stale root-level folders and are swept on the next generation).
  • Date added: import_movie stamps File.added (ISO); older items without it fall back to the file's mtime for the "recently added" ordering (ctime is unreliable — creating the Filmotéka hardlinks bumps it).

Direction — Jellyfin pivot (in progress)

Curator is being refocused from a ČSFD-tagging tool into a Jellyfin-first library manager. Agreed shape:

  • Pool = the Jellyfin library set. Files are named Title (rok) [tmdbid-N].ext — the form Jellyfin matches on. Libraries are physical top-level pool folders, each with a kind (libraries setting; managed in Pool → Knihovny…):
    • movie — single-file movies, TMDb naming (done)
    • series — series/season/episode tree, mirrored 1:1 (done)
    • mixed — movies and series in one folder (Jellyfin "Mixed" type; supported-but-discouraged by Jellyfin, but low-risk here thanks to the strict structure, and fully reversible — see decisions). Scanning TBD (phase 9).
    • homevideo — files with no TMDb (Jellyfin "Home videos" type). TBD (ph. 8).
  • Target library layout (the user's plan): Hlavní (mixed, incl. kids), Dokumenty (mixed, mostly no-TMDb YT series), Anime (series), Chajda (homevideo). Kids are not a separate folder — see decisions.
  • Naming convention:
    • Movie: {CZ název} (rok) [tmdbid-N].ext — the exact Jellyfin form.
    • Series: {název série} (rok) [tmdbid]/Season NN/S{ss}E{ee} - {filename}.ext (season 0 → Specials); the episode keeps its original filename after the SxxEyy code. Episode numbers are recognised from the filename; editable in the series GUI.
    • CZ title source: TMDb (cs-CZ) primarily — reliable when matched by tmdbid (yearless title search picks the wrong film, hence the earlier "weird names"); ČSFD as fallback/override (a ČSFD link at import, or when TMDb has no cs title); last fallback = the original filename. The filename is always editable in the GUI.
  • Metadata is Jellyfin's job: Jellyfin fetches from TMDb and (configured to) writes .nfo + artwork into the folders → survives a Jellyfin reinstall. Curator resolves a TMDb id → title + year for naming (tmdb.py). ČSFD is kept only as an optional CZ-title source for the filename (not for tags/ matching — it exposes no TMDb/IMDb id, verified).
  • Import: mandatory TMDb id (matching + CZ title), optional ČSFD link (CZ-title source/override); filename hand-editable before/after import.
  • miniDLNA: keep a genre-split hardlink tree for a dumb DLNA browser, but drive it from the NFO Jellyfin wrote (repurposing HardlinkManager + naming.py), not from ČSFD tags.
  • Jellyfin API (thin, hybrid): trigger scans, verify, manage collections; key + URL in .env. NFO on disk stays authoritative.

Decisions (locked)

  • Kids = Jellyfin parental rating, not a folder (variant B). Kids content stays in the normal libraries; a child Jellyfin account gets a max-rating limit with "block unrated" ON. Ratings come from TMDb via Jellyfin. Caveat: Czech certifications are sparse — set the library metadata country to one with fuller ratings (US) and fix odd cases in Jellyfin (Identify + lock).
  • Mixed libraries are OK and reversible. Jellyfin discourages them (unreliable metadata) but our disciplined structure (series = Season/SxxEyy, movie = single file + [tmdbid]) gives the scanner clear signals. Because metadata is NFO on disk + tmdbid in the name, switching mixed↔separate later is just moving folders (Jellyfin re-reads NFO, no re-fetch, no loss).
  • Home videos (Chajda) use Jellyfin's "Home videos" content type — no online lookup, just serves the files.
  • Always match by tmdbid, not by yearless title. TMDb cs-CZ titles are good when the right film is matched (lookup by id, or search with a year); a bare title search trips on remakes/same-name films (verified: Home Alone → 2021 reboot, Vejška → 2006). Import supplies the id; migration searches with a year.

Phasing

(1) TMDb identity + tmdbid naming + import rework [done]; (2) movie libraries as physical partitions + import-into / move-between [done]; (3) Jellyfin API — scan + verify + status [done]; (4) miniDLNA genre tree from NFO + ČSFD removed [done]; (5) series model — SxxEyy layout, import, rename [done]; (6) migrate the existing pool — title+year → TMDb search → tmdbid [done]; (7) consolidated edit dialogs + library organizer (unified libraries model with kinds) [done]. Remaining: (8) import without TMDb/year (homevideo, no-TMDb docs); (9) mixed-library scanning + generation (movies + series in one folder).

Note: libraries hold flat files for now (pool/<Library>/Title (rok) [tmdbid].ext); per-movie folders are a later refinement.

Tasks

Phase 8 — import without TMDb / without year (homevideo, no-TMDb docs)

  • Relax the import dialog: allow a row with no year and no tmdbid (currently a 4-digit year is mandatory). Pool name then falls back to Title.ext (movies) / Series/Season NN/SxxEyy.ext (series), no [tmdbid].
  • Drive this by library kind: homevideo (and no-TMDb rows in mixed) skip the TMDb requirement; movie keeps requiring a year (as today).
  • homevideo library scanning: load its files as items (flat), no TMDb naming, no genre tags. Consider a dedicated import path or a per-import "bez TMDb" toggle.
  • Tests: import with no year/id lands correctly; homevideo library loads.

Phase 9 — mixed-library scanning + generation

  • Scan a mixed library by structure: a top-level single video file = movie; a subfolder containing Season NN/ (or SxxEyy files) = series. Reuse series.scan_series for the series part and the flat movie scan for the rest.
  • load_pool_movies / load_series become kind-aware (iterate libraries, dispatch by kind); today they read movie_libraries / copyasis_folders.
  • miniDLNA generation + NFO refresh must cover mixed libraries too.
  • The library filter / table should surface mixed libraries once loadable.
  • Tests: a mixed folder with both a movie and a series loads/generates correctly.

Later / optional

  • ČSFD CZ-title fetch (deferred). Live test showed TMDb cs-CZ titles are good when matched by tmdbid, so ČSFD title-fetch was not rebuilt (would need the Anubis PoW solver removed in phase 4). If TMDb gets a title wrong, edit the filename in the GUI. The ČSFD link is already stored per movie (File.csfd_url, set at import / in the edit dialog) as a pointer for later — revisit fetching only if TMDb proves insufficient in practice.
  • Per-movie folders (pool/<Lib>/Title (rok) [tmdbid]/…) instead of flat files.
  • Collections / BoxSets (Bond, Marvel) via Jellyfin API or <set> in NFO.
  • Optional common umbrella folder above libraries (pool/Média/<Lib>) — needs path-aware library_of (currently single-segment).

Done

  • Jellyfin pivot phases 1–7 (see Phasing): TMDb identity, tmdbid naming, movie libraries + move, Jellyfin API (scan/verify), NFO-driven miniDLNA + ČSFD removed, series model (SxxEyy), pool→TMDb migration, consolidated edit dialogs
  • Library organizer (libraries unified model with kinds movie/series/mixed/ homevideo; Pool → Knihovny…); movie_libraries/copyasis_folders derived
  • Pool naming convention Title (YYYY).ext (naming.py): year required at import (per-row Rok field, blue = ČSFD-suggested, red = pool collision), plus "Přejmenovat dle ČSFD (rok)" to bring older files up to the convention
  • Video integrity check (integrity.py, Testy → "Kontrola integrity videa"): FFmpeg-based scan for corrupted/unreadable video data, deep (full decode) or quick (ffprobe), over selected movies or the whole pool
  • Pool-root and Filmotéka-output folder settings in the global config
  • Filmy / Seriály top-level folder handling in the pool
  • "Import movie" dialog (Title + ČSFD link), copy into pool/Filmy as Title.ext
  • Rename a pooled movie from the app (FileManager.rename_movie): renames the file in pool/Filmy and moves its metadata to the new index key
  • Remove-from-pool (delete file + its metadata)
  • Generate the Filmotéka hardlink tree from the pool (Rok / Žánr / Země původu / Hodnocení)
  • Filmotéka fully regenerable from the pool alone (delete output = no loss)
  • GUI reframed around the Filmotéka and rewritten in PySide6
  • Seriály "copy-as-is" mirror: pool/Seriály cloned 1:1 into the output as hardlinks (HardlinkManager.mirror_as_is), wired into Filmotéka generation
  • Fixed media_utils missing subprocess import
  • Unified pool metadata index (pool_index.py): one .Curator.!index per pool; File reads/writes it when injected, FileManager uses it for the pool
  • Configurable copy-as-is folders (copyasis_folders in global config, editable from the GUI); each is mirrored 1:1 during Filmotéka generation (Seriály default)
  • README.md written (overview, concepts, workflow, run/build instructions)
  • ČSFD scraping (csfd.py, ported from Tagger devel): File.apply_csfd_tags fetches a movie and assigns Žánr / Rok / Země původu tags (cached in metadata); wired into the GUI (auto-fetch on import with a ČSFD link, plus "Načíst tagy z ČSFD"). Parsing updated for current ČSFD HTML and verified live against Matrix (film/9499); HTTPS uses the OS cert store via truststore (corporate SSL)
  • ČSFD Anubis anti-bot wall handled: csfd.py detects the proof-of-work challenge page, solves it (SHA-256 PoW matching the bundled worker JS) and replays via a shared requests.Session, so Žánr / Rok / Země původu tags load again (the "nalezeno 1 film, načteno 0 tagů" symptom). Verified live (Matrix 1999)
  • Removed the inherited Tagger predefined tags: DEFAULT_TAGS is now empty (no Hodnocení ⭐ / Barva categories) and new files no longer get an automatic Stav/Nové tag. Tags now come from ČSFD (Žánr / Rok / Země původu) and manual edits. Note: Hodnocení is still listed in FILMOTEKA_CATEGORIES, so that branch is simply empty until something assigns a Hodnocení tag again
  • Fixed template cruft: src/constants.py made consistent (Curator values, get_version/get_debug_mode API) and test_constants.py aligned; removed the imported tagger/ devel dump