# `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 /Title (rok) [tmdbid-N].ext two or more /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.` 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`](../../src/core/movie.py) ## Classes ### `MediaFile` *class · line 102* ```python class MediaFile: ``` One file belonging to a movie (today always a playable video). #### `MediaFile.format_3d` *property · line 110* ```python format_3d -> Optional[str] ``` The 3D format this version's label denotes, if any. #### `MediaFile.to_record` *method · line 114* ```python to_record(pool_dir: Path) -> dict ``` Serialise for the pool index (path stored pool-relative, POSIX). #### `MediaFile.from_record` *class method · line 123* ```python 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* ```python class Movie: ``` A film in the pool: its identity, its metadata, and its media files. #### `Movie.videos` *property · line 156* ```python videos -> list[MediaFile] ``` Playable versions, in a stable order (labelled ones by label). #### `Movie.is_grouped` *property · line 163* ```python is_grouped -> bool ``` True when the movie needs its own folder (more than one version). #### `Movie.stem` *property · line 168* ```python 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* ```python primary -> Optional[MediaFile] ``` The version that represents the movie (first by `videos`). #### `Movie.desired_layout` *method · line 187* ```python 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 `//`. Callers move whatever doesn't already match — which makes promotion, demotion and renaming the same operation. #### `Movie.to_record` *method · line 208* ```python 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* ```python 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* ```python 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* ```python 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..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* ```python identity_key(tmdb_id: object=None, title: str='', year: object=None) -> str ``` The pool-index key for a movie — `tmdb-`, 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.