# 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** (`/.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. - **The record is the movie, files are its children** (index **v2**, `movie.py`). Keyed by identity (`tmdb-`, else a `name-` fallback), each record holds the metadata *and* a `media` list of its files with a free `label` per version. Chosen after the v1 path-keyed shape made multi-version movies unworkable: metadata rode on one file, so it had to be rescued when that file went, and retitling a grouped movie had to be forbidden. With identity as the key, `Movie.desired_layout()` is a pure function of the record — **one video → flat, two or more → own folder** — so promote, demote and rename are the same operation, and an import can recognise "this tmdbid is already in the pool" instead of silently creating a duplicate. - **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//film`) and **`Dle země původu`** (`output/Dle země původu//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. A 3D movie carries an extra `.3D.` tag before the extension (Jellyfin's own convention; `FORMAT` ∈ `HSBS`/`FSBS`/`HTAB`/`FTAB`/`MVC`), set via the edit dialog and preserved across every future rename like the tmdbid is. - **Multiple movie versions** (2D+3D, 4K+1080p, …): identity is `(title, rok, tmdbid)`, stays a **flat file** at one version. A second version promotes it to Jellyfin's required per-movie folder — every file prefixed with the exact folder name — `Title (rok) [tmdbid]/Title (rok) [tmdbid] - {popisek}.ext`; a 3D version's popisek doubles as its 3D tag (`3D HSBS`, scanned by Jellyfin the same as the flat `.3D.HSBS` form). Curator does the promotion/demotion itself (*Upravit film → Verze*), and a version's popisek is renamable too (`rename_movie_version`) — a flat movie always shows one placeholder row ("Výchozí") in the list even though it has no on-disk label yet, but renaming only applies once it's grouped. A popisek left empty (a plain "2D" file with nothing else to call it) is `DEFAULT_VERSION_LABEL` ("Výchozí"), not an error — everywhere a popisek is entered (`add_movie_version`, `rename_movie_version`, `merge_as_version`, the import dialog's version link). Promoting/demoting always carries the file's *own* current 3D formát over automatically when no popisek is given for it (`existing_label`/`keep_label` default to `None` → derived via `naming.parse_3d_format`) — a movie can freely gain and lose versions in any order (3D-only → +2D → -3D → 3D-only again, …) without a formát ever getting silently dropped along the way. The default (plain) version of a grouped movie carries **no label**: `Folder/Folder.ext` (`file_manager._version_filename`); "Výchozí" is the UI's name for it. Identity — what decides "is this film already in the pool" — is the folder name for a grouped movie and the stem minus any `.3D.` for a flat one (`FileManager._identity_stem`, `pooled_with_stem`). Importing a film that's already there defaults to adding a version, never a root-level duplicate, and "keep both as separate films" is not offered for same-TMDb-id films (they'd share one index record). **Parts** (one film split across files, Jellyfin "stacking") are versions with a `partN` popisek, written as `Folder-partN.ext` instead of `Folder - popisek.ext` (`file_manager._version_filename`, `movie.normalize_part_label`); a film with parts always lives in its own folder. Parts and versions in one folder are not combined yet. *Upravit film* presents this as one "Média filmu" section: flat ("Samostatný film") until a second medium arrives, then a folder whose mode is switched between versions and parts (`movie_group_mode`, `set_movie_group_mode`); parts are reordered with ↑/↓ (`move_movie_part`). Renames after a relabel go through `File.save_location`, never a plain `save_metadata`, so a stale sibling can't overwrite shared metadata. Custom per-movie attributes are no longer editable in the GUI; stored ones still feed the miniDLNA filename templates. The table always shows one row per movie (`visible_movies`), marked `▣` / `Film · N verze` when it has several; shared metadata lives on the movie record (index v2), so every version reads the same. Accidental duplicates (same tmdbid / title+year / file size as separate movies) are found by *Nástroje → Kontrola duplicit* and folded in via `merge_as_version`. **Not yet supported:** renaming/moving/retitling a grouped movie (remove the version or edit files directly). Works in `mixed` libraries too, disambiguated from a series by **shape**: a movie's version folder and a series folder are both `Title (rok) [tmdbid]/`, but a series always nests episodes in a `Season NN`/`Specials` folder and a movie's version folder never does (`series.looks_like_series_folder`) — `load_pool_movies`/ `is_movie_group_member` read the shape and `scan_series` (`skip_movie_folders=True` for `mixed`) skips what it recognises as a movie, so both kinds can share one identity-named folder pair without either leaking into the other's listing. A `series`-kind library (which never holds movies) keeps reading every subfolder as a series regardless of shape, so a raw not-yet-renamed import can still have loose episode files at the series root before `Season NN` exists. Assignable at **import** time too, not just after the fact: picking several files for the same film in one batch (*Importovat filmy…*), a row's "🔗 Přiřadit jako verzi…" marks it a version of another row instead of importing it as its own movie — the earlier attempt at this (a 3D formát combo per row) was rejected because a lone row has nothing to attach the version *to* and just created a duplicate; picking the target row explicitly is what that was missing. The popisek is pre-filled from the filename (`naming.three_d_format_from_filename`) the same way `year_from_filename` pre-fills the year. Import runs the target rows first, then folds each linked row onto its target via `add_movie_version`. The reverse case — a film whose only file *is* 3D, with nothing to link it to — gets its own **3D formát combo per row** instead (`import_movie(..., format_3d=…)`, same as *Upravit film*); disabled the moment the row is linked as a version or gains one as a follower, since a grouped movie's 3D-ness lives on the version's popisek, not on the row that started the group. - Series: `{název série} (rok) [tmdbid]/Season NN/S{ss}E{ee} - {série}.ext` (season 0 → `Specials`). The episode filename is the code + the series title only — the release string is dropped (identity is the code; Jellyfin fetches episode metadata from TMDb via the code + `[tmdbid]`). Episode numbers are recognised from the filename and editable at import / in the series GUI; *Seriály → "Napravit názvy souborů…"* bulk-fixes the whole pool. - **Episodes split into physical parts** (an old recording cut into several files) get Jellyfin's own `.ptN` stacking suffix — `SxxEyy - {série}.ptN.ext` — recognised from *cd/dvd/part/pt/disc/disk* + a number in the source filename (`Show.S01E02.pt2.mkv`, `Show S01E01-part-1.mkv`, …) or typed by hand at import as `S01E02P3` (`src.core.series.parse_episode_code`/`parse_episode_part`). Distinct from a multi-episode file (`E01-E02`, one file = several episodes) — a part is the opposite: several files = one episode. - **Removing a series is scoped to the tree selection**: *Přehled seriálů → "Odebrat z poolu…"* deletes the series, a single season, or a single episode, whichever is selected (`FileManager.remove_series`/ `remove_season`/`remove_episodes`) — series still carry no index metadata, so this is always just deleting files/folders from disk. - **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 a title source (not for tags/matching — it exposes no TMDb/IMDb id, verified): it feeds the **ČSFD → TMDb bridge** (`csfd.py`, `FileManager.resolve_via_csfd`) — filename → ČSFD search → ČSFD *original* title + year → TMDb search → tmdbid, more reliable than a raw Czech-title TMDb search. Used by the import dialog ("Dohledat přes ČSFD") and by migration for any movie with a stored `csfd_url`. - **Import:** mandatory **TMDb id** (matching + CZ title), optional **ČSFD link** (drives the ČSFD→TMDb bridge, and stored as a pointer); filename hand-editable before/after import. The import row is pre-filled on open (no network) from metadata already next to the source file: Curator's **pre-index** layouts — a `.{filename}.!tag` sidecar or an old `.Curator.!index` up the tree (`legacy_meta.read_legacy_meta`, authoritative for the ČSFD link) — then a Kodi/Jellyfin `.nfo` (`nfo.read_nfo`, also reads a `csfd` `uniqueid` / ``, and supplies the TMDb id). - **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. **Scoped per library** (*miniDLNA → "Knihovny pro generování…"*, `minidlna_libraries` in the pool index, default = all) — lets a library (e.g. Kids) be excluded from the dumb-DLNA output entirely; a movie library's genre-tree links are swept as obsolete the normal way when it's excluded. A `series` **or** `homevideo`-kind library has no genre tags to build a tree from (homevideo has no TMDb id either), so both hardcopy whole into `output/` (`mirror_as_is_libraries`) — this works today independent of phase 8 (homevideo needs no GUI import/browse support to be mirrored). Mirrors have **no obsolete-link mechanism of their own**, so excluding one is pruned explicitly (`FileManager.minidlna_prune_targets` + `HardlinkManager.remove_mirror`) — including the shared `output/Seriály` bucket a mixed-library series and a same-named real library can both write into, reconciled per-entry so pruning one never erases the other's still-valid content. - **Jellyfin API** (thin, hybrid): trigger scans, verify, manage collections; key + URL in `.env`. NFO on disk stays authoritative. - **Subtitles stay external, never muxed** (`subtitles.py`): a `.srt`/`.ass`/ `.ssa`/`.sub`/`.vtt` sharing the video's stem (+ optional `.lang`/`.forced`/ `.sdh`/`.default`) plays natively in Jellyfin — no re-encode, multiple languages side by side, easy to replace. Presence is read live from disk (like NFO), not cached in the index. The one thing Curator owns: a subtitle **follows its video through every rename/move** so it never silently detaches (every rename/move path calls `subtitles.rename_subtitles_with`). ### 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]; (9) mixed-library scanning + generation (movies + series in one folder) [done]. Remaining: **(8) import without TMDb/year** (homevideo, no-TMDb docs). Note: libraries hold **flat** files for now (`pool//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. **miniDLNA generation for `homevideo` needs none of this** — it hardcopies the whole library folder as-is (`mirror_as_is_libraries` **[done]**); this task is only about GUI import/browsing. - Tests: import with no year/id lands correctly; homevideo library loads. ### Phase 9 — mixed-library scanning + generation - Scanning **[done]**: a `mixed` library's top-level files load as movies (`load_pool_movies`), its subfolders as series (`load_series` / `series_libraries` = kind `series` **or** `mixed`, reusing `scan_series`) — *except* a canonical-shaped subfolder with no `Season NN`/`Specials` inside it, which is a movie's version folder instead and loads as a movie too (see "Multiple movie versions" above). Both are kind-aware over the unified `libraries` list. The library filter / table already surfaces mixed libraries. - miniDLNA generation **[done]**: a mixed library's movies are genre-tagged as usual; series in its subfolders have no genre tags, so they're hardcopied straight into `output/Seriály//…` instead (`FileManager.minidlna_scope` → the mixed-library series list; the mirror loop in `generate_filmoteka`). NFO refresh needs no separate handling — it only ever touches movies (`self.filelist`), series never go through NFO/TMDb tagging, mixed or not. - Tests: a mixed folder with both a movie and a series loads/generates correctly **[done]** (`TestMinidlnaScope` in `test_file_manager.py`). ### Later / optional - **ČSFD → TMDb title bridge (done).** A direct TMDb search on a Czech title was matching the wrong film too often, so a lean `csfd.py` was revived (Anubis PoW solver, film-page + search parsing, `original_title` extraction — the entry in `.film-names` whose flag matches the country of origin). `resolve_via_csfd` drives it from the import dialog and from migration. **Not** a return of ČSFD tags — genres still come from TMDb via Jellyfin/NFO. Open follow-up: batch "look up ČSFD links for the whole pool" as a migration pre-pass (today the `csfd_url` is per-movie, set at import / in the edit dialog). - Per-movie folders (`pool//Title (rok) [tmdbid]/…`) instead of flat files — **partially done**: a movie only gets its own folder once it has a second playable version (see "Multiple movie versions" below); a single-file movie stays flat as before. - Collections / BoxSets (Bond, Marvel) via Jellyfin API or `` in NFO. - Optional common umbrella folder above libraries (`pool/Média/`) — 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 - Phase 9 — mixed-library scanning + generation: a `mixed` library's top-level movies genre-tag as usual, series in its subfolders hardcopy into `output/Seriály//…` (no genre tags apply to a series) - 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