Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping
This commit is contained in:
+102
-27
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user