Files
Curator/PROJECT.md
T

479 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- **Rust rewrite (branch `rust`).** Curator is a Rust application: the core
(`src/core/`) is a library with the same behaviour and on-disk formats as the
Python version (pool index v2, tag schema, naming, global config keys), and
the GUI is egui/eframe (`src/ui/`). An existing pool opens unchanged. The
Python sources live on in the history of `devel` as the reference.
- `FileManager` is the service layer; `File` is a view on one media entry of an
identity-keyed movie record in `<pool>/.Curator.!index`; series are scanned
live from disk.
- The standalone `tipy-dne` binary replaces `scripts/tipy_dne.py` (which had
stopped finding movies after the index moved to v2). The one-off Python
migration scripts for old ČSFD tag formats were not ported.
- Global config moved from `.Curator.!gtag` next to the script to the platform
config directory (`CURATOR_CONFIG` overrides it); an old `.Curator.!gtag` in
the working directory is picked up on first run.
### GUI decision
The GUI is **egui** (eframe): pure Rust, one static binary, straightforward
Windows builds. It mirrors the PySide6 layout — menu, tag-filter sidebar, movie
and series table, detail panel, status bar — and every dialog of the Python
version. Long operations run in background tasks with progress and cancel.
## 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-<id>`, else a `name-<slug>` 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/<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. A 3D
movie carries an extra `.3D.<FORMAT>` 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.<FORMAT>` 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` /
`<csfdid>`, 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/<jejich vlastní název>` (`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/<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.
**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/<název>/…` 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/<Lib>/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 `<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
- 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/<název>/…` (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