5.1 KiB
src/core/movie.py
The movie object — metadata first, files second.
A movie is identified by (title, year, tmdb_id) and owns its files, not
the other way round. That inversion is the point: the pool index used to key a
record by a file path, so metadata was a passenger on one particular file — and
everything about multi-version movies got awkward (which file "holds" the tags,
why renaming a grouped movie had to be blocked, why an import could silently
create a duplicate instead of a version).
Here the record is the movie; each playable file is a MediaFile under
it, told apart by a free-text label (3D HSBS, 4K, Director's Cut, …). Where those files live is then a pure function of the record —
Movie.desired_layout — so promoting to a folder, demoting back, or
renaming the whole thing are all the same operation: recompute and move what
doesn't match.
Layout (Jellyfin's own conventions — https://jellyfin.org/docs/general/server/media/movies/)::
one video <Library>/Title (rok) [tmdbid-N].ext
two or more <Library>/Title (rok) [tmdbid-N]/
Title (rok) [tmdbid-N] - Výchozí.ext
Title (rok) [tmdbid-N].3D.HSBS.ext
Every file in a movie folder must start with the folder's exact name; both
" - label" and ".3D.FMT" are valid separators, so a 3D file keeps the
documented .3D.<FORMAT> spelling whether it sits flat or grouped.
Subtitles are not media entries — they ride along with their video by filename
stem (see src.core.subtitles).
Source: src/core/movie.py
Classes
MediaFile
class · line 102
class MediaFile:
One file belonging to a movie (today always a playable video).
MediaFile.format_3d
property · line 110
format_3d -> Optional[str]
The 3D format this version's label denotes, if any.
MediaFile.to_record
method · line 114
to_record(pool_dir: Path) -> dict
Serialise for the pool index (path stored pool-relative, POSIX).
MediaFile.from_record
class method · line 123
from_record(data: dict, pool_dir: Path) -> 'MediaFile'
Build a MediaFile from one entry of a movie record's media list, resolving its relative path against the pool directory.
Movie
class · line 132
class Movie:
A film in the pool: its identity, its metadata, and its media files.
Movie.videos
property · line 156
videos -> list[MediaFile]
Playable versions, in a stable order (labelled ones by label).
Movie.is_grouped
property · line 163
is_grouped -> bool
True when the movie needs its own folder (more than one version).
Movie.stem
property · line 168
stem -> str
Canonical Title (rok) [tmdbid-N] — the folder / filename base.
Falls back to the bare title when no four-digit year is known (a movie can be imported without one).
Movie.primary
property · line 182
primary -> Optional[MediaFile]
The version that represents the movie (first by videos).
Movie.desired_layout
method · line 187
desired_layout(pool_dir: Path) -> list[tuple[MediaFile, Path]]
(media, where it belongs) for every file, from the record alone.
The whole flat-vs-folder rule lives here: one video sits directly in
the library, two or more move into <Library>/<stem>/. Callers move
whatever doesn't already match — which makes promotion, demotion and
renaming the same operation.
Movie.to_record
method · line 208
to_record(pool_dir: Path) -> dict
Serialise the movie into the dictionary stored in the pool index, with media paths relative to the pool directory.
Movie.from_record
class method · line 227
from_record(key: str, data: dict, pool_dir: Path, tagmanager=None) -> 'Movie'
Build a Movie from an index record stored under key. Tags are resolved through tagmanager when one is given.
Functions
normalize_3d_label
function · line 53
normalize_3d_label(label: object) -> Optional[str]
The 3D format a version label denotes (upper-case), or None.
Lets a plain label double as the 3D marker — "3D HSBS" typed in the
version dialog and "hsbs" picked from a dropdown mean the same thing.
media_filename
function · line 63
media_filename(stem: str, label: str, ext: str, *, grouped: bool) -> str
Filename for one media file of a movie whose canonical stem is stem.
- no label →
stem.ext(a lone, plain video) - 3D label →
stem.3D.<FORMAT>.ext(Jellyfin's documented 3D form) - anything else →
stem - label.ext
grouped only decides whether an empty label is allowed: inside a movie
folder every file gets one, so it falls back to DEFAULT_VERSION_LABEL.
identity_key
function · line 84
identity_key(tmdb_id: object=None, title: str='', year: object=None) -> str
The pool-index key for a movie — tmdb-<id>, else a title/year slug.
Keying by identity (rather than by file path) is what lets a second file of the same film find its movie instead of becoming a separate entry.