Files
Curator/docs/modules/core.file_manager.md
T

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: every mirror_as_is_libraries name no longer in mirror_libs, except one that shares the bucket's name (reconciled via the second element instead).
  • valid_bucket_children — basenames that must remain directly under output/<mixed_series_bucket> this run: the in-scope mixed-library series' own folder names, plus — when a real library named mixed_series_bucket is 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 _N suffix.
  • "replace": evict the existing same-named movie(s) (file + metadata) and import the new one under the plain name.
  • "skip": do not import; return None.

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.mkv and X.3D.HSBS.mkv lying 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>.