6.1 KiB
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
Classes
Episode
class · line 49
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
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
class Series:
One series folder with its title, year, TMDb id, library and the episodes found below it.
Series.seasons
method · line 74
seasons() -> dict[int, list[Episode]]
Episodes grouped by season number (-1 = unrecognised).
Functions
parse_episode
function · line 84
parse_episode(name: str) -> Optional[tuple[int, int]]
Extract (season, episode) from a filename, or None.
parse_episode_part
function · line 94
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
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
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
season_folder_name(season: int) -> str
Folder name for a season — Specials for 0, else Season NN.
is_season_folder
function · line 147
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
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
sanitize_title(title: str) -> str
A series/episode title made safe to sit in a filename (spaces collapsed).
episode_filename
function · line 179
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
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
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
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.