Migrate from Poetry and pyenv to uv
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
# `src/core/series.py`
|
||||
|
||||
Series naming + scanning for the Jellyfin TV layout.
|
||||
|
||||
Jellyfin wants series laid out as::
|
||||
|
||||
Seriály/Series (year) [tmdbid-N]/
|
||||
tvshow.nfo (Jellyfin writes it)
|
||||
Season 01/
|
||||
Series (year) S01E01.ext
|
||||
Specials/ (season 0)
|
||||
|
||||
Episodes are matched by `SxxEyy`. These helpers parse the episode number out of
|
||||
a raw filename, build the canonical episode/season/series names, and scan a
|
||||
series library into a `Series → Season → Episode` tree with a naming status.
|
||||
|
||||
Source: [`src/core/series.py`](../../src/core/series.py)
|
||||
|
||||
## Classes
|
||||
|
||||
### `Episode`
|
||||
|
||||
*class · line 49*
|
||||
|
||||
```python
|
||||
class Episode:
|
||||
```
|
||||
|
||||
One episode file found in a series folder: its path, its season and episode numbers (`None` when not recognised) and its part number when the episode is split across several files.
|
||||
|
||||
#### `Episode.code`
|
||||
|
||||
*property · line 58*
|
||||
|
||||
```python
|
||||
code -> str
|
||||
```
|
||||
|
||||
Human-readable episode code, e.g. `S01E02`, with `.ptN` appended for a part (`S01E02.pt3`). `?` when the season or episode is unknown.
|
||||
|
||||
### `Series`
|
||||
|
||||
*class · line 66*
|
||||
|
||||
```python
|
||||
class Series:
|
||||
```
|
||||
|
||||
One series folder with its title, year, TMDb id, library and the episodes found below it.
|
||||
|
||||
#### `Series.seasons`
|
||||
|
||||
*method · line 74*
|
||||
|
||||
```python
|
||||
seasons() -> dict[int, list[Episode]]
|
||||
```
|
||||
|
||||
Episodes grouped by season number (`-1` = unrecognised).
|
||||
|
||||
## Functions
|
||||
|
||||
### `parse_episode`
|
||||
|
||||
*function · line 84*
|
||||
|
||||
```python
|
||||
parse_episode(name: str) -> Optional[tuple[int, int]]
|
||||
```
|
||||
|
||||
Extract `(season, episode)` from a filename, or `None`.
|
||||
|
||||
### `parse_episode_part`
|
||||
|
||||
*function · line 94*
|
||||
|
||||
```python
|
||||
parse_episode_part(name: str) -> Optional[int]
|
||||
```
|
||||
|
||||
Part number implied by a Jellyfin stacking suffix in a filename, or `None`.
|
||||
|
||||
Looks for *cd*, *dvd*, *part*, *pt*, *disc* or *disk* + a number sitting
|
||||
right before the extension (`Show S01E02.pt2.mkv`,
|
||||
`Show S01E02-part-2.mkv`, …) — the same tokens Jellyfin itself scans for.
|
||||
|
||||
### `parse_episode_code`
|
||||
|
||||
*function · line 105*
|
||||
|
||||
```python
|
||||
parse_episode_code(text: str) -> Optional[tuple[int, int, Optional[int]]]
|
||||
```
|
||||
|
||||
Parse a compact user-typed code: `SxxEyy`, optionally + a part.
|
||||
|
||||
Accepts everything `parse_episode` does, plus a trailing part marker
|
||||
right after it — `S01E02P3`, `S01E02 pt3`, `S01E02.part.3`,
|
||||
`S01E02 cd2` — for a file that is only *one part* of that episode.
|
||||
Returns `(season, episode, part)` with `part` `None` when absent, or
|
||||
`None` when no `SxxEyy` is found at all.
|
||||
|
||||
### `collect_episode_files`
|
||||
|
||||
*function · line 126*
|
||||
|
||||
```python
|
||||
collect_episode_files(folder: Path) -> list[Path]
|
||||
```
|
||||
|
||||
Every video file under `folder` (recursively), sidecars excluded, sorted.
|
||||
|
||||
Lets the episode-import dialog take a whole `Series/Season NN/` tree (or any
|
||||
nested layout) in one pick instead of file-by-file. Season/episode numbers
|
||||
are still read per file from its name by `parse_episode`.
|
||||
|
||||
### `season_folder_name`
|
||||
|
||||
*function · line 142*
|
||||
|
||||
```python
|
||||
season_folder_name(season: int) -> str
|
||||
```
|
||||
|
||||
Folder name for a season — `Specials` for 0, else `Season NN`.
|
||||
|
||||
### `is_season_folder`
|
||||
|
||||
*function · line 147*
|
||||
|
||||
```python
|
||||
is_season_folder(name: str) -> bool
|
||||
```
|
||||
|
||||
True for a season folder name — `Specials` or `Season <n>` (any padding).
|
||||
|
||||
### `looks_like_series_folder`
|
||||
|
||||
*function · line 152*
|
||||
|
||||
```python
|
||||
looks_like_series_folder(folder: Path) -> bool
|
||||
```
|
||||
|
||||
True when `folder` holds at least one `Season NN`/`Specials` subfolder.
|
||||
|
||||
That's the shape Curator always gives a series (episodes always land in a
|
||||
season folder, never loose at the series root) — a movie's version folder
|
||||
never has one, its files sit flat inside it. This is how a `mixed`
|
||||
library tells the two apart even though both use the identical outer
|
||||
`Title (rok) [tmdbid]/` name: `FileManager.is_movie_group_member`
|
||||
and `scan_series` both check it before deciding which one a
|
||||
canonical-shaped subfolder is.
|
||||
|
||||
### `sanitize_title`
|
||||
|
||||
*function · line 173*
|
||||
|
||||
```python
|
||||
sanitize_title(title: str) -> str
|
||||
```
|
||||
|
||||
A series/episode title made safe to sit in a filename (spaces collapsed).
|
||||
|
||||
### `episode_filename`
|
||||
|
||||
*function · line 179*
|
||||
|
||||
```python
|
||||
episode_filename(season: int, episode: int, series_title: str, ext: str, *, part: Optional[int]=None) -> str
|
||||
```
|
||||
|
||||
Canonical episode filename `SxxEyy - {series title}.ext`.
|
||||
|
||||
The release string is deliberately **not** kept — identity is the `SxxEyy`
|
||||
code, and Jellyfin fetches the episode title/metadata from TMDb (matched by
|
||||
the code + the series folder's `[tmdbid-N]`). An empty title collapses to
|
||||
`SxxEyy.ext`. `part` marks one physical part of that episode — an old
|
||||
recording split across several files — with Jellyfin's own `.ptN`
|
||||
stacking suffix right before the extension.
|
||||
|
||||
### `series_folder_stem`
|
||||
|
||||
*function · line 200*
|
||||
|
||||
```python
|
||||
series_folder_stem(title: str, year: object=None, tmdb_id: object=None) -> str
|
||||
```
|
||||
|
||||
Series folder name: `Title (year) [tmdbid-N]` (year/id optional).
|
||||
|
||||
Reuses the movie convention when a year is present; otherwise falls back to
|
||||
`Title [tmdbid-N]` or just `Title` so a year-less series still works.
|
||||
|
||||
### `is_canonical_episode`
|
||||
|
||||
*function · line 216*
|
||||
|
||||
```python
|
||||
is_canonical_episode(path: Path, series_title: str) -> bool
|
||||
```
|
||||
|
||||
True when `path` is already `SxxEyy - {series title}.ext` in its season.
|
||||
|
||||
### `scan_series`
|
||||
|
||||
*function · line 229*
|
||||
|
||||
```python
|
||||
scan_series(lib_dir: Path, library: str, *, skip_movie_folders: bool=False) -> list[Series]
|
||||
```
|
||||
|
||||
Scan one series library into a list of `Series`.
|
||||
|
||||
Each immediate subfolder of `lib_dir` is a series; its name yields the
|
||||
title/year/tmdbid. Video files anywhere below it are episodes (season/episode
|
||||
parsed from the filename). Non-video sidecars are skipped.
|
||||
|
||||
`skip_movie_folders=True` (a `mixed` library, where a movie's version
|
||||
folder and a series folder both use the identical
|
||||
`Title (rok) [tmdbid]/` shape) additionally skips a canonical-shaped
|
||||
folder with no `Season NN`/`Specials` inside it — that's a movie's
|
||||
version folder instead, left for `FileManager.load_pool_movies` to
|
||||
read. Left `False` (the default, a plain `series`-kind library, which
|
||||
never holds movies) every subfolder is read as a series regardless of
|
||||
shape — a raw, not-yet-renamed import may have loose episode files
|
||||
sitting at the series root, with no season folder yet.
|
||||
Reference in New Issue
Block a user