36 KiB
src/core/file_manager.py
FileManager: the application's service layer. It owns the list of pooled movies (filelist), the pool index, the library configuration and the global config, and implements every operation the GUI triggers — import, rename/move/edit, multi-version grouping, duplicate detection, series import/rename/removal, miniDLNA/Filmotéka scope, ČSFD/TMDb lookups and the consistency checks. The GUI never touches the filesystem directly.
Source: src/core/file_manager.py
Classes
FileManager
class · line 71
class FileManager:
The service layer behind the GUI: pool and library configuration, the movie list, and every file operation. See the method reference below and docs/architecture.md.
FileManager.__init__
method · line 72
__init__(tagmanager: TagManager)
Load the global config and prepare an empty file list. The pool index is opened lazily once a pool directory is known.
FileManager.pool_dir
property · line 84
pool_dir -> Path | None
The pool folder (pool_dir from the global config), or None if not set.
FileManager.movies_dir
property · line 89
movies_dir -> Path | None
The Filmy folder inside the pool (the default movie library), or None.
FileManager.series_dir
property · line 94
series_dir -> Path | None
The Seriály folder inside the pool (the default series library), or None.
FileManager._open_index
method · line 102
_open_index(pool: Path) -> PoolIndex
(Re)load the pool index and migrate any legacy global settings.
FileManager._ensure_index
method · line 108
_ensure_index() -> PoolIndex | None
Return the pool index, opening it lazily when a pool is configured.
FileManager._migrate_global_settings
method · line 114
_migrate_global_settings() -> None
Move library settings from the old global config into the index once.
Earlier versions stored tag_schema / copyasis_folders in the
global .!gtag. They now belong to the pool; seed the index from the
old values (if present and not already migrated) and drop them from the
global config so it carries app data only.
FileManager._get_setting
method · line 134
_get_setting(key: str)
Read a library setting from the index (falls back to the default).
FileManager._set_setting
method · line 142
_set_setting(key: str, value) -> None
Persist a library setting into the index (needs a configured pool).
FileManager.libraries
property · line 150
libraries -> list[dict]
All pool libraries as [{"name", "kind"}, …] (source of truth).
For pools predating the unified model this is synthesised from the legacy
movie_libraries (→ kind movie) + copyasis_folders (→ series).
FileManager.set_libraries
method · line 173
set_libraries(specs: list[dict]) -> None
Replace the library list (list of {"name", "kind"}) and persist it.
FileManager.libraries_of_kind
method · line 185
libraries_of_kind(*kinds: str) -> list[str]
Names of libraries matching any of kinds.
FileManager.minidlna_libraries
property · line 190
minidlna_libraries -> list[str] | None
Libraries included in miniDLNA generation, or None meaning all of them. Stored in the pool index; names that no longer exist as libraries are dropped when read.
FileManager.set_minidlna_libraries
method · line 205
set_minidlna_libraries(names: list[str] | None) -> None
Restrict miniDLNA generation to names; None resets to "all".
FileManager._set_setting_raw
method · line 210
_set_setting_raw(key: str, value) -> None
Persist an arbitrary pool-index setting (no legacy-default fallback).
FileManager.minidlna_scope
method · line 217
minidlna_scope() -> tuple[list[File], list[str], list]
Return what miniDLNA generation should include as (movies, mirrored library names, mixed-library series). Narrowed to minidlna_libraries when that is set, otherwise the whole pool. Libraries left out here have their generated links swept as obsolete on the next run.
FileManager.minidlna_prune_targets
method · line 254
minidlna_prune_targets(mirror_libs: list[str], mixed_series: list, mixed_series_bucket: str) -> tuple[list[str], set[str]]
What miniDLNA output to remove for mirrors that fell out of scope.
mirror_libs / mixed_series come from minidlna_scope (the
current run's selection); mixed_series_bucket is the shared output
folder mixed-library series hardcopy into (e.g. "Seriály") — the same
name a real series/homevideo library may also use, so the two
sources are reconciled together rather than one blindly wiping the other.
Returns (stale_libraries, valid_bucket_children):
stale_libraries— mirrored-library output folders (output/<name>) to delete entirely: everymirror_as_is_librariesname no longer inmirror_libs, except one that shares the bucket's name (reconciled via the second element instead).valid_bucket_children— basenames that must remain directly underoutput/<mixed_series_bucket>this run: the in-scope mixed-library series' own folder names, plus — when a real library namedmixed_series_bucketis itself in scope — that library's current subfolders. Anything else found there is stale.
FileManager.mirror_as_is_libraries
property · line 290
mirror_as_is_libraries -> list[str]
Libraries hardcopied 1:1 into miniDLNA — series and homevideo.
Neither has genre tags to build a tree from (homevideo has no TMDb id
either), so both mirror whole into output/<library name> instead of
going through the tag tree. A superset of copyasis_folders
(which the library organizer UI still calls "copy-as-is" and keeps
scoped to kind series only).
FileManager.copyasis_folders
property · line 302
copyasis_folders -> list[str]
Names of pool subfolders mirrored 1:1 (copy-as-is) — the series libraries.
FileManager.set_copyasis_folders
method · line 306
set_copyasis_folders(names: list[str]) -> None
Set the series (copy-as-is) libraries, keeping the other kinds.
FileManager.movie_libraries
property · line 313
movie_libraries -> list[str]
Pool folders holding single-file movies (kind movie).
FileManager.libraries_accepting_movies
property · line 318
libraries_accepting_movies -> list[str]
Libraries a movie can live in / be assigned to — kind movie or mixed.
FileManager.set_movie_libraries
method · line 322
set_movie_libraries(names: list[str]) -> None
Set the movie libraries, keeping the other kinds; never empty.
FileManager.orphan_pool_folders
method · line 328
orphan_pool_folders() -> list[tuple[str, bool]]
Top-level pool folders that aren't a configured library.
Returns (name, is_empty) pairs. Only these are candidates for cleanup;
non-empty ones are reported but never auto-deleted (they may hold media).
FileManager.remove_empty_pool_folder
method · line 344
remove_empty_pool_folder(name: str) -> bool
Delete an empty orphan folder in the pool root. Returns True if removed.
FileManager.library_of
method · line 356
library_of(file_obj: File) -> str | None
Which library (top-level pool folder) a pooled movie lives in.
FileManager.tag_schema
property · line 367
tag_schema -> list[dict]
Tag categories + ČSFD/Filmotéka rules (see config.DEFAULT_TAG_SCHEMA).
FileManager.set_tag_schema
method · line 371
set_tag_schema(schema: list[dict]) -> None
Set the tag schema and persist it.
FileManager.filmoteka_category_roots
method · line 375
filmoteka_category_roots() -> dict[str, str]
Category → output root-folder map derived from the tag schema.
Categories with filmoteka_root set to None are filterable but get no
folders in the generated tree.
FileManager.filmoteka_category_transforms
method · line 387
filmoteka_category_transforms() -> dict[str, str]
Category → folder transform-name map (for grouping, e.g. rating bands).
Tags keep their exact value; this transform only shapes the folder name in the generated tree. Categories without a transform are omitted.
FileManager.filmoteka_category_filename_templates
method · line 399
filmoteka_category_filename_templates() -> dict[str, str]
Category → hardlink-name template map (applied inside that category).
FileManager.filmoteka_dir
property · line 408
filmoteka_dir -> Path | None
The Filmotéka output folder (the generated hardlink tree), or None.
FileManager.set_pool_dir
method · line 412
set_pool_dir(pool_dir: Path) -> None
Set the managed pool root and create every library's top-level folder.
FileManager.set_filmoteka_dir
method · line 421
set_filmoteka_dir(filmoteka_dir: Path) -> None
Set the Filmotéka output folder (generated hardlink tree).
FileManager.load_pool_movies
method · line 426
load_pool_movies() -> None
Reload the movie list from every movie library, via the pool index.
Scans each configured movie library (movie_libraries) recursively —
a movie may sit directly in the library or inside its own per-movie
folder. Curator's own metadata and the sidecars Jellyfin writes (NFO,
artwork, subtitles) are skipped.
FileManager.check_data_consistency
method · line 470
check_data_consistency() -> dict
Compare the pool index against the actual files in the movie libraries.
Detects drift such as a movie file deleted directly (metadata left behind) or a file dropped into the pool without importing (no metadata).
Returns a dict with: ok — False if the pool isn't set (with 'error') missing — indexed media files that no longer exist on disk untracked — pool files (pool-relative) with no index entry index_count / disk_count — totals for the summary
FileManager.verify_against_jellyfin
method · line 523
verify_against_jellyfin(jf_movies: list) -> dict
Compare the pool against what Jellyfin currently sees.
Matches each pooled movie to a Jellyfin item by TMDb id (from the filename), falling back to (title, year). Reports what is out of sync.
jf_movies is a list of jellyfin.JellyfinMovie. Returns a dict:
matched — how many pooled movies Jellyfin has
pool_only — pooled filenames Jellyfin does not see (rescan/rematch)
jellyfin_only — Jellyfin titles with no matching pooled movie
pool_total / jellyfin_total — totals for the summary
FileManager.scan_video_integrity
method · line 568
scan_video_integrity(files: list[File], deep: bool=True, on_progress: Callable[[int, int, File], None] | None=None, should_cancel: Callable[[], bool] | None=None) -> dict
Check a list of movies for corrupted/unreadable video data.
Runs check_video_integrity on each file (deep = full ffmpeg
decode, otherwise a quick ffprobe probe). on_progress(done, total, file) is called after each file and should_cancel() is polled
before each — so a GUI can show progress and stop between files.
Returns a dict with: ok — False if ffmpeg/ffprobe isn't available (with 'error') problems — IntegrityResult list for files that failed (bad or unable-to-check), in scan order checked — how many files were actually processed cancelled — True if the scan stopped early
FileManager.statistics
method · line 611
statistics() -> dict
Aggregate stats over the loaded pool movies for the stats view.
Returns a dict with totals and, per tag category, the tag counts sorted by frequency (descending).
FileManager._identity_stem
method · line 651
_identity_stem(file_obj: File) -> str
The movie-identity stem file_obj belongs to.
A version inside a movie folder belongs to the folder's name (its own
filename carries the version label, which is not part of identity); a
flat file belongs to its own stem minus any .3D.<FORMAT> tag.
FileManager.pooled_with_stem
method · line 663
pooled_with_stem(title: str) -> list[File]
Pooled files of the movie identity title (case-insensitive) — a
flat file, every version of a grouped movie, or a 3D-tagged copy of
either. A film already in the pool must be found however it's stored,
or importing it again silently creates a second, root-level copy.
FileManager._evict
method · line 671
_evict(file_obj: File) -> None
Delete a pooled movie: its metadata, the file, and the list entry.
FileManager.import_movie
method · line 679
import_movie(source: Path, title: str, move: bool=False, on_conflict: str='suffix', year: int | str | None=None, tmdb_id: int | str | None=None, library: str | None=None, csfd_url: str | None=None, format_3d: str | None=None, version_label: str | None=None) -> File | None
Bring a video file into a movie library and index its metadata.
Lands in pool/<library>/ (library defaults to the first movie
library, "Filmy"). The pooled filename follows the naming convention
Title (YYYY) [tmdbid-N].ext when a year is given (and a
tmdb_id when known — the form Jellyfin matches on); without a year it
falls back to Title.ext. The GUI import always supplies a year, so a
library built through the app is uniformly Title (YYYY) [tmdbid-N].ext.
format_3d (one of naming.THREE_D_FORMATS) appends the
.3D.<FORMAT> tag right away, when the file is already a 3D rip.
on_conflict="version" adds the file as another version of a film
already in the pool instead (its popisek is version_label, else the
format_3d tag, else plain/default) — the file is never placed beside
that film as a second movie.
By default the original is copied (non-destructive). With move=True
the source file is moved into the pool instead, leaving nothing behind.
on_conflict decides what happens when a pooled movie of the same name
already exists:
"suffix"(default): keep both, the new file gets a_Nsuffix."replace": evict the existing same-named movie(s) (file + metadata) and import the new one under the plain name."skip": do not import; returnNone.
Raises:
ValueError: a year was given but is not a four-digit number.
FileManager.rename_movie
method · line 778
rename_movie(file_obj: File, new_title: str) -> File
Rename a pooled movie's file to <new_title>.<ext> and reindex it.
Renames the physical file in pool/Filmy (keeping its extension), moves
the metadata to the new key, and syncs title/filename. The
extension is preserved; new_title is the bare name without it.
Raises:
ValueError: empty name, a name containing a path separator, or
file_obj is one version of a multi-version movie (renaming
just one file would orphan its siblings).
FileExistsError: another pooled file already uses that name.
FileManager._canonical_name_for
method · line 819
_canonical_name_for(file_obj: File, *, include_3d: bool=True) -> str | None
The Title (YYYY) [tmdbid-N] stem this movie should have, or None.
Uses the clean title and the year from the movie's metadata
(name_context: cache → Rok tag → the current filename). A
(YYYY) that leaked into the title is stripped so it isn't doubled, and
an existing [tmdbid-N] / .3D.<FORMAT> on the current filename is
preserved (include_3d=False drops the 3D tag — used for a movie-group
folder name, where 3D becomes a per-version label instead). Returns
None when no four-digit year is known.
FileManager.rename_to_canonical
method · line 848
rename_to_canonical(file_obj: File) -> File | None
Rename a pooled movie to Title (YYYY).ext from its metadata.
The year comes from the movie's metadata (NFO cache, falling back to a
Rok tag or the existing filename). file_obj.title keeps the title
(without the year). Does not fire on_files_changed — the caller
refreshes once after a batch. A movie with multiple versions is skipped
(None) — renaming one version alone would orphan its siblings; use
the version tools in Upravit film instead.
Returns:
The file when renamed (or already canonical); None when no
four-digit year is known (or the movie has multiple versions), so
no canonical name can be built.
Raises: FileExistsError: another pooled file already uses that name.
FileManager.rename_all_to_canonical
method · line 888
rename_all_to_canonical(files: list[File], on_progress: Callable[[int, int, File], None] | None=None, should_cancel: Callable[[], bool] | None=None) -> dict
Rename each movie to Title (YYYY).ext from its metadata.
Skips files already canonical and those without a known year. Reports conflicts (target name taken) without aborting the rest.
Returns a dict with: renamed — (old_name, new_name) pairs actually renamed skipped_no_year — filenames with no four-digit year available conflicts — (old_name, message) where the target name was taken unchanged — count already in canonical form cancelled — True if stopped early
FileManager.move_to_library
method · line 937
move_to_library(file_obj: File, new_library: str) -> File
Move a pooled movie into another library (top-level pool folder).
Moves the file into pool/<new_library>/ and re-indexes it under the
new key. Raises:
ValueError: the target isn't a known movie library, or file_obj
is one version of a multi-version movie (moving just one file
would orphan its siblings).
FileExistsError: a file of that name already sits in the target.
FileManager.apply_movie_edits
method · line 976
apply_movie_edits(file_obj: File, *, title: str, year: int | str | None=None, tmdb_id: int | str | None=None, library: str | None=None, date: str | None=None, attributes: dict | None=None, tags: list[str] | None=None, csfd_url: str | None=None, format_3d: str | None=None) -> File
Apply all editable parameters of a movie at once (rename/move + metadata).
Renames the file to Title (rok) [tmdbid].3D.<FORMAT>.ext (year/3D
permitting), moves it into library, and sets title / date /
attributes / tags (a full ["Category/Name", …] list;
NFO-tag provenance is kept for the ones that remain). No-ops that don't
change the path skip the move.
format_3d follows naming.canonical_pool_stem: omitted
(None) keeps whatever 3D tag the file already has, an empty string
clears it, one of naming.THREE_D_FORMATS sets it.
Raises: FileExistsError: the target name is already taken.
FileManager.is_movie_group_member
method · line 1078
is_movie_group_member(file_obj: File) -> bool
True when file_obj is one version inside its own per-movie folder.
Recognised in movie and mixed libraries. A mixed library's
subfolder can be either a movie's version folder or a series — both
use the identical Title (rok) [tmdbid]/ shape — so there it also
checks the folder holds no Season NN/Specials subfolder (the
shape Curator always gives a series; a movie's version folder never
has one, its files sit flat inside it — see
series.looks_like_series_folder).
FileManager.list_movie_versions
method · line 1101
list_movie_versions(file_obj: File) -> list[File]
Every version sharing file_obj's movie-group folder, name-sorted.
A single-element list (just file_obj) when it isn't grouped.
FileManager.primary_version
method · line 1118
primary_version(versions: list[File]) -> File
Which version represents the movie in the table.
Metadata is shared by the whole movie record now, so this is purely a
display choice: the plain/unlabelled file first, then the one labelled
movie.DEFAULT_VERSION_LABEL, then alphabetically. Unrelated to
which version Jellyfin plays by default (its own, alphabetical rule).
FileManager.visible_movies
method · line 1135
visible_movies() -> list[File]
filelist collapsed to one row per movie for the table.
Ungrouped files pass through unchanged; a movie-group folder
contributes only its primary_version — the rest are reachable
via Upravit film → Verze, not as separate rows.
FileManager.add_movie_version
method · line 1155
add_movie_version(file_obj: File, source: Path, label: str, *, move: bool=False, existing_label: str | None=None) -> File
Add source as another playable version of file_obj's movie.
Promotes a flat movie to its own per-movie folder on the first extra
version — the pre-existing file is moved in as
<folder> - {existing_label}.ext (only on this first promotion) and
source lands as <folder> - {label}.ext. A movie already grouped
just gets one more file in the same folder. Shared metadata stays
wherever it already was; the new version starts with none of its own
(attach subtitles / a 3D formát to it separately if needed).
An empty label (a plain "2D" version with nothing else to call it)
falls back to movie.DEFAULT_VERSION_LABEL ("Výchozí") —
Jellyfin's own examples label every grouped file, so an unlabelled one
still needs something, just not something the user has to type.
Likewise existing_label left unset (None) is derived from the
pre-existing file's own name: its current .3D.<FORMAT> tag if it
has one — so promoting a movie that was flat-and-3D-only never
silently drops that on the floor — else the same "Výchozí" default.
Raises:
RuntimeError: no pool, or file_obj has no valid title+year yet.
FileExistsError: a version of that name already exists.
FileManager.rename_movie_version
method · line 1228
rename_movie_version(file_obj: File, new_label: str) -> File
Rename one version's label within its movie's version folder.
Only meaningful once a movie is grouped — a flat (single-version) movie has no on-disk label to rename; give it a second version first (its sole version keeps showing as "Výchozí" in Upravit film either way, that's just a placeholder for the table).
An empty new_label falls back to movie.DEFAULT_VERSION_LABEL
("Výchozí"), same as add_movie_version.
Raises:
ValueError: file_obj isn't part of a version group.
FileExistsError: another version already uses that label.
FileManager.normalize_default_version_names
method · line 1263
normalize_default_version_names() -> list[tuple[str, str]]
Give a grouped movie's default version its bare name.
Pool files written before the default version went unlabelled still
read <folder> - Výchozí.ext; this renames each to <folder>.ext
(subtitles and metadata follow). A folder whose bare name is already
taken is left alone. Returns the (old, new) filenames renamed.
FileManager.remove_movie_version
method · line 1292
remove_movie_version(file_obj: File) -> None
Remove one version (+ its subtitles) from a movie's version group.
When exactly one version is left afterward, the movie is demoted back
to a flat file — moved out to the library root under the bare
canonical stem, or stem.3D.<FORMAT> when that sole survivor's own
popisek was a 3D formát (so a movie that ends up 3D-only again keeps
saying so, instead of silently becoming a plain file) — and the
now-empty folder is removed. Metadata needs no rescuing: it lives on
the movie record, not on any one version, so removing even the
version shown in the table leaves it untouched.
Raises:
ValueError: file_obj isn't part of a version group (nothing to
remove it from — a lone movie has no "versions" to drop).
FileManager.remove_movie
method · line 1341
remove_movie(file_obj: File) -> None
Delete a whole movie from the pool — every version, subtitle and record.
For a flat movie that is just its file (+ subtitles); for a grouped one it is the entire per-movie folder, so removing the row shown in the table never leaves the other versions behind as a half-empty group.
FileManager._movie_units
method · line 1362
_movie_units() -> list[list[File]]
filelist grouped per movie: one list per flat file / version folder.
FileManager.find_duplicates
method · line 1370
find_duplicates() -> list[dict]
Movies that show up in the pool more than once.
Each movie counts once however many versions it has (a version folder is one movie, not a duplicate of itself). Two movies are flagged when they share
- the same TMDb id — e.g.
X.mkvandX.3D.HSBS.mkvlying flat side by side instead of grouped, or the same film in two libraries; - the same title + year — typically one copy paired with TMDb and one not (or paired with a different id);
- a byte-identical file size — almost certainly the same file copied.
Overlapping hits are merged into one group. Returns a list of
{"movies": [File, …], "reasons": [str, …]} — movies holds the
primary_version of each movie — sorted by title.
FileManager.merge_as_version
method · line 1452
merge_as_version(keep: File, other: File, label: str, *, keep_label: str | None=None) -> File
Fold the duplicate movie other into keep as one more version.
other's file is moved into keep's version folder (promoting
keep from flat when needed, with keep_label — left None,
the usual case, it's derived from keep's own current 3D formát if
it has one, same as add_movie_version's existing_label);
its subtitles follow. keep's metadata wins — other only
contributes tags and a ČSFD link keep doesn't have yet — and
other's own record entry is dropped so it doesn't linger as a
phantom row. An empty label for other is likewise fine — it
falls back to "Výchozí" (add_movie_version).
Raises:
ValueError: other is itself a multi-version movie, or both are
the same movie folder / file.
RuntimeError / FileExistsError: see add_movie_version.
FileManager.list_subtitles
method · line 1506
list_subtitles(file_obj: File) -> list
External subtitle sidecars currently sitting next to a movie.
FileManager.attach_subtitle
method · line 1510
attach_subtitle(file_obj: File, source: Path, *, language: str | None=None, flag: str | None=None, move: bool=False) -> Path
Copy (or move) a picked subtitle file next to file_obj, correctly named.
Raises:
ValueError: source isn't a recognised subtitle format.
FileManager.remove_subtitle
method · line 1522
remove_subtitle(subtitle_path: Path) -> None
Delete one attached subtitle file.
FileManager.refresh_tags_from_tmdb
method · line 1526
refresh_tags_from_tmdb(files: list[File] | None=None, on_progress: Callable[[int, int, File], None] | None=None, should_cancel: Callable[[], bool] | None=None) -> dict
(Re)derive each movie's tags from TMDb (by the tmdbid in its name).
Fetches genres / year / country / rating from TMDb per the tag schema and
regenerates the auto tags (File.apply_nfo, which is source-agnostic);
user-added tags are kept. Movies with no tmdbid in the filename are skipped.
Returns {ok, updated, without_id, errors}.
FileManager.resolve_via_csfd
method · line 1567
resolve_via_csfd(*, csfd_url: str | None=None, query: str | None=None, session: object | None=None) -> dict
ČSFD → TMDb bridge for one movie.
With csfd_url set, reads that ČSFD film; otherwise searches ČSFD for
query (a cleaned filename) and takes the top hit. The ČSFD original
title + year then drive a TMDb search — far more reliable than a raw
Czech-title search. Pass a shared requests.Session to reuse the
Anubis auth cookie across a batch.
Returns {"csfd_url", "film": CsfdFilm | None, "tmdb": TmdbResult | None}.
Best-effort: a network / anti-bot / no-match failure is a None in the
result, not an exception.
FileManager.match_pool_to_tmdb
method · line 1604
match_pool_to_tmdb(files: list[File] | None=None, on_progress: Callable[[int, int, File], None] | None=None, should_cancel: Callable[[], bool] | None=None) -> list
Propose a TMDb id for every movie that doesn't have one yet.
Searches TMDb by each movie's title + year and classifies the result
(see migrate.classify). A movie that carries a ČSFD link
(File.csfd_url) is first resolved through ČSFD so the search runs on
the original title + year — the pure Czech-title search picks the wrong
film for remakes/localized names. Movies whose filename already carries a
[tmdbid-N] are skipped. Returns a list of migrate.MatchProposal.
FileManager.assign_tmdb_id
method · line 1663
assign_tmdb_id(file_obj: File, tmdb_id: int | str, year: int | str | None=None) -> File
Attach a TMDb id to a movie and rename it to the canonical convention.
year (typically from the TMDb match) fills a missing local year so the
file becomes Title (rok) [tmdbid].ext. Title/library/tags/date are kept.
FileManager.refresh_tags_from_nfo
method · line 1680
refresh_tags_from_nfo(files: list[File] | None=None) -> dict
(Re)derive each movie's auto tags from its NFO (written by Jellyfin).
Reads the .nfo next to each movie and regenerates the genre / year /
country / rating tags per the tag schema (File.apply_nfo); user-added
tags are preserved. Movies without an NFO yet are left untouched.
Returns {updated, without_nfo}.
FileManager.series_libraries
property · line 1709
series_libraries -> list[str]
Pool folders that can hold series — kind series or mixed.
FileManager.load_series
method · line 1713
load_series() -> list
Scan every series library into a list of series.Series.
A mixed library also skips a canonical-shaped subfolder that
doesn't look like a series (see series.scan_series's
skip_movie_folders) — that's a movie's version folder instead,
read by load_pool_movies. A plain series-kind library
never holds movies, so it keeps reading every subfolder as a series
regardless of shape (a raw, not-yet-renamed import may have loose
files sitting at the series root, with no season folder yet).
FileManager.import_episodes
method · line 1737
import_episodes(sources: list[Path], title: str, year: int | str | None=None, tmdb_id: int | str | None=None, library: str | None=None, move: bool=False, on_conflict: str='suffix', codes: Mapping[Path, tuple[int, int] | tuple[int, int, int | None]] | None=None) -> dict
Import episode files into <library>/Series (rok) [tmdbid]/Season NN/.
Each source's season/episode/part comes from codes (an explicit
{path: (season, episode)} or {path: (season, episode, part)}
override set at import time) when present, otherwise it is parsed from
the filename. It is then copied (or moved) in as Series SxxEyy.ext
(.ptN appended when it's just one part of that episode). Files with
neither an override nor a recognisable SxxEyy are skipped and
reported.
Returns {imported, skipped_no_code, errors}.
FileManager.rename_episodes_to_canonical
method · line 1807
rename_episodes_to_canonical(series) -> dict
Rename a series' episodes to SxxEyy - {series title}.ext in its season.
series is a series.Series. Episodes with no recognisable code are
skipped; a target name already taken is reported as a conflict.
Returns {renamed, skipped, conflicts}.
FileManager.rename_all_series_to_canonical
method · line 1842
rename_all_series_to_canonical(on_progress: Callable[[int, int, str], None] | None=None, should_cancel: Callable[[], bool] | None=None) -> dict
Canonicalise every series' episode filenames across the pool.
Iterates all series (series + mixed libraries); per series it
renames each episode to SxxEyy - {series title}.ext in the right
Season NN / Specials folder (rename_episodes_to_canonical)
and then removes any season folder left empty by the moves. Episodes with
no recognisable SxxEyy are left in place and listed.
Returns {series, renamed:[(series, old, new)], skipped_no_code:[(series, name)], conflicts:[(series, name, msg)], empty_dirs_removed:[str], cancelled}.
FileManager.apply_series_edits
method · line 1891
apply_series_edits(series, *, title: str, year: int | str | None=None, tmdb_id: int | str | None=None, library: str | None=None) -> dict
Rename/move a series folder and re-title its episodes to match.
Renames the series folder to Title (rok) [tmdbid] (moving it into
library if changed), then re-derives the series and renames every
episode to NewTitle SxxEyy.ext in the right Season folder.
Returns {folder, episodes} (episodes = rename_episodes result).
Raises: FileExistsError: the target series folder already exists.
FileManager.remove_series
method · line 1927
remove_series(series) -> None
Delete a whole series folder from the pool (every season and episode).
Series carry no index metadata (they are scanned from disk), so removing one is just deleting its folder. Refuses a folder that is not inside the pool as a safety check.
Raises: ValueError: the series folder is outside the pool.
FileManager.remove_episodes
method · line 1948
remove_episodes(episodes: list) -> None
Delete specific episode files (+ their subtitles) from the pool.
episodes is a list of series.Episode — series carry no index
metadata (they are scanned live from disk), so this is just deleting
files. A Season NN/Specials folder left completely empty by the
removal is cleaned up too; one that still holds something else (a
stray sidecar, another episode) is left alone.
FileManager.remove_season
method · line 1973
remove_season(series, season: int) -> None
Delete every episode of one season of series (+ the season folder).
season is a season number as reported by series.seasons()
(-1 = episodes with no recognisable season — there is no single
folder for those, just the files themselves get removed).
Raises:
ValueError: series has no episodes in that season.
FileManager.assign_tag_to_file_objects
method · line 1988
assign_tag_to_file_objects(files_objs: list[File], tag)
Attach a tag (a Tag or a category/name string) to every file in the list and register the tag with the tag manager.
FileManager.remove_tag_from_file_objects
method · line 2005
remove_tag_from_file_objects(files_objs: list[File], tag)
Detach a tag (a Tag or a category/name string) from every file in the list.
FileManager.filter_files_by_tags
method · line 2024
filter_files_by_tags(tags: Iterable)
Return only the files that carry all the given tags (AND filter). With no tags, the whole file list is returned. Tags may be Tag objects or category/name strings.
Functions
_version_filename
function · line 57
_version_filename(stem: str, label: str, suffix: str) -> str
Filename of one version of a movie folder.
The default version ("Výchozí", or no label at all) carries no label —
it's just <stem><suffix>, the same name the movie had flat. Any other
version is <stem> - <label><suffix>.