Files

208 lines
5.1 KiB
Markdown

# `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`](../../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 `<Library>/<stem>/`. 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.<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*
```python
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.