Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping

This commit is contained in:
2026-09-13 17:16:48 +02:00
parent c0ad1c1553
commit c399a6aed3
26 changed files with 4635 additions and 221 deletions
+102 -27
View File
@@ -66,6 +66,16 @@ movie table, and one-click Filmotéka generation.
- **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
@@ -182,11 +192,29 @@ library manager**. Agreed shape:
`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.
- 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*); the
table always shows one row per movie (`visible_movies`). Shared metadata
lives on one **primary** version (`File.is_primary_version`), migrated if
that one is ever removed. **Not yet supported:** renaming/moving/retitling
a grouped movie (remove the version or edit files directly), and grouped
movies inside `mixed` libraries (scoped to `movie`-kind for now — a mixed
library's subfolders are still read as series only).
- 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.
- **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
@@ -195,15 +223,45 @@ library manager**. Agreed shape:
- **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).
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**
(CZ-title source/override); filename hand-editable before/after import.
(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.
`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)
@@ -231,9 +289,9 @@ physical partitions + import-into / move-between [done]; (3) Jellyfin API — sc
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).
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.
@@ -249,28 +307,42 @@ Note: libraries hold **flat** files for now (`pool/<Library>/Title (rok)
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
- 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.
- 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`).
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 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.
- **Č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).
@@ -280,6 +352,9 @@ Note: libraries hold **flat** files for now (`pool/<Library>/Title (rok)
- 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