# `src/ui/qt_app.py` PySide6 GUI for Curator — reframed around the Filmotéka (movie-library) workflow. Layout: - Toolbar / menu: configure the Pool and the Filmotéka output, import a movie, generate the Filmotéka hardlink tree. - Left sidebar: tag tree used as an AND-filter over the movie list. - Center: the movie table (the contents of pool/Filmy). - Status bar: counts and the current pool/output paths. Source: [`src/ui/qt_app.py`](../../src/ui/qt_app.py) ## Classes ### `ImportMoviesDialog` *class · line 52* ```python class ImportMoviesDialog(QDialog): ``` Collect a Title + Year + TMDb id per file for a batch import into the pool. One row per source file (filename shown; Title, Year and TMDb id editable). "Dohledat z TMDb" fills them from TMDb — by the given id, or by searching the filename. A single toggle decides whether the files are copied (default) or moved into the pool. #### `ImportMoviesDialog.__init__` *method · line 61* ```python __init__(parent: QWidget, sources: List[Path], existing_names: set[str] | None=None, libraries: list[str] | None=None) -> None ``` Build the batch import dialog for the given source files. `existing_names` marks names already in the pool, `libraries` fills the library selector. #### `ImportMoviesDialog._append_row` *method · line 162* ```python _append_row(source: Path) -> None ``` Add one source file as a row: title/year/TMDb/ČSFD fields pre-filled from its name and its sidecar metadata, a 3D format combo, the version button and a remove button. #### `ImportMoviesDialog._prefill_row` *method · line 235* ```python _prefill_row(source: Path, title_edit: QLineEdit, year_edit: QLineEdit, tmdb_edit: QLineEdit, csfd_edit: QLineEdit) -> None ``` Seed a new import row from metadata already next to the source file. Curator's pre-index layouts win for the ČSFD link (a per-file `.{name}.!tag` sidecar, or an old `.Curator.!index` up the tree); a Kodi/Jellyfin `.nfo` fills what they don't carry (notably the TMDb id). #### `ImportMoviesDialog._row_of` *method · line 267* ```python _row_of(title_edit: QLineEdit) ``` The row entry whose title field is `title_edit`, or `None`. #### `ImportMoviesDialog._row_by_source` *method · line 273* ```python _row_by_source(source: Path) ``` The row entry for the source file `source`, or `None`. #### `ImportMoviesDialog._refresh_skip_marks` *method · line 279* ```python _refresh_skip_marks() -> None ``` Strike through the rows that "Vynechat řádky bez TMDb ID" would drop. A version row is struck only when its target would be dropped. #### `ImportMoviesDialog._candidate_stem` *method · line 301* ```python _candidate_stem(title: str, year: str, tmdb_id: str) -> str | None ``` The canonical 'Title (year) [tmdbid]' stem, or None if incomplete. #### `ImportMoviesDialog._mark_collision` *method · line 310* ```python _mark_collision(title_edit: QLineEdit) -> None ``` Red title when the resulting pooled name already exists. #### `ImportMoviesDialog._set_suggested` *method · line 325* ```python _set_suggested(edit: QLineEdit, suggested: bool) -> None ``` Blue tint marks an auto-filled value (TMDb / ČSFD), not yet confirmed. #### `ImportMoviesDialog._remove_row` *method · line 330* ```python _remove_row(title_edit: QLineEdit) -> None ``` Drop a single file from the import list without restarting the dialog. #### `ImportMoviesDialog._has_followers` *method · line 354* ```python _has_followers(source: Path) -> bool ``` True when another row is linked as a version *of* `source`. #### `ImportMoviesDialog._sync_version_row_fields` *method · line 358* ```python _sync_version_row_fields(source: Path) -> None ``` Grey out a row's own identity fields while it's linked as a version — its title/year/tmdbid/ČSFD come from the film it's a version *of*. The 3D formát combo is separate: it's for a row that stays a flat, single-file movie, so it's disabled both when the row *is* a version (its 3D-ness belongs on its own popisek instead — see `_link_as_version`) and when it *has* one (once grouped, 3D lives on whichever version is actually 3D, not on the group itself). #### `ImportMoviesDialog._refresh_version_button` *method · line 390* ```python _refresh_version_button(source: Path) -> None ``` Update the caption and tooltip of a row's version button: "Přiřadit jako verzi…" when unlinked, the target's name when linked. #### `ImportMoviesDialog._link_as_version` *method · line 409* ```python _link_as_version(source: Path) -> None ``` Click handler of the version button. Unlinks a linked row (after confirmation), or asks for a target row and a popisek and links the row as a version of it. #### `ImportMoviesDialog.version_links` *method · line 466* ```python version_links() -> dict[Path, tuple[Path, str]] ``` `{version source: (target source, popisek)}` set via "Přiřadit jako verzi…" — read by the caller to import these as versions (`FileManager.add_movie_version`) instead of standalone films. #### `ImportMoviesDialog._add_files` *method · line 472* ```python _add_files() -> None ``` Add more source files chosen in a file dialog as new rows. #### `ImportMoviesDialog._autofill_tmdb` *method · line 477* ```python _autofill_tmdb() -> None ``` Fill Title/Year/TMDb-id from TMDb per row (blue = suggested, verify). A row that already has a TMDb id is resolved by id; otherwise the filename is searched (narrowed by the year if present) and the top hit's id/title/year are filled in. #### `ImportMoviesDialog._autofill_via_csfd` *method · line 523* ```python _autofill_via_csfd() -> None ``` Fill ČSFD-odkaz / TMDb-id / Title / Year per row via the ČSFD bridge. Per row: use the ČSFD link if given, otherwise search ČSFD by the cleaned filename. From the ČSFD film take the original title + year and search TMDb by those (Czech title as a fallback). Filled values are blue (suggested — verify). Rows that already have a value keep it. #### `ImportMoviesDialog.accept` *method · line 594* ```python accept() -> None ``` Validate the rows before the dialog closes: standalone rows need a four-digit year, version rows don't. Applies the "skip rows without TMDb ID" option first. #### `ImportMoviesDialog.move_files` *property · line 644* ```python move_files -> bool ``` Whether the files should be moved into the pool instead of copied. #### `ImportMoviesDialog.skipped_no_tmdb` *property · line 648* ```python skipped_no_tmdb -> list[Path] ``` The source files dropped by "Vynechat řádky bez TMDb ID" (valid after accept). #### `ImportMoviesDialog.selected_library` *property · line 653* ```python selected_library -> str ``` The library chosen in the library combo box. #### `ImportMoviesDialog.entries` *method · line 656* ```python entries() -> list[tuple[Path, str, str, str, str, str | None]] ``` Return (source, title, year, tmdb_id, csfd_url, format_3d) per **standalone** row — a row linked as a version (see `version_links`) is excluded, it isn't imported as a film of its own. `format_3d` is always `None` for a row that has a follower (see `_has_followers`) even if its combo still shows a value — once grouped, 3D belongs to a specific version's popisek, not the flat identity this row would otherwise import as. ### `ImportEpisodesDialog` *class · line 679* ```python class ImportEpisodesDialog(QDialog): ``` Collect a series identity + episode files, name them Series SxxEyy.ext. #### `ImportEpisodesDialog.__init__` *method · line 682* ```python __init__(parent: QWidget, sources: List[Path], libraries: list[str] | None=None) -> None ``` Build the episode import dialog for a series: series identity fields (title, year, TMDb id, library) and one row per episode file. #### `ImportEpisodesDialog._append_row` *method · line 767* ```python _append_row(source: Path) -> None ``` Add one episode file as a row. The SxxEyy field is pre-filled from the file name (including a part number) and colour-coded by validity. #### `ImportEpisodesDialog._mark_code` *method · line 790* ```python _mark_code(edit: QLineEdit) -> None ``` Colour the SxxEyy field: green = valid, red = unparsable, plain = empty. #### `ImportEpisodesDialog._remove_row` *method · line 805* ```python _remove_row(source: Path) -> None ``` Remove the row for the episode file `source` from the table and the row list. #### `ImportEpisodesDialog._add_files` *method · line 812* ```python _add_files() -> None ``` Add more episode files chosen in a file dialog. #### `ImportEpisodesDialog._add_folder` *method · line 817* ```python _add_folder() -> None ``` Add every video file under a picked folder (recursively, subfolders too). #### `ImportEpisodesDialog._autofill_tmdb` *method · line 844* ```python _autofill_tmdb() -> None ``` Fill title, year and TMDb id from TMDb's TV search (or the given TMDb id). #### `ImportEpisodesDialog.accept` *method · line 870* ```python accept() -> None ``` Validate the series identity and the episode list; warns when some episodes have no valid SxxEyy code (they will be skipped) and asks whether to continue. #### `ImportEpisodesDialog.title` *property · line 892* ```python title -> str ``` The series title as typed. #### `ImportEpisodesDialog.year` *property · line 896* ```python year -> str ``` The series year as typed (may be empty). #### `ImportEpisodesDialog.tmdb_id` *property · line 900* ```python tmdb_id -> str ``` The TMDb TV id as typed (may be empty). #### `ImportEpisodesDialog.library` *property · line 904* ```python library -> str ``` The series library chosen in the combo box. #### `ImportEpisodesDialog.move_files` *property · line 908* ```python move_files -> bool ``` Whether the episode files should be moved into the pool instead of copied. #### `ImportEpisodesDialog.sources` *method · line 911* ```python sources() -> list[Path] ``` The episode source files, in table order. #### `ImportEpisodesDialog.episode_codes` *method · line 914* ```python episode_codes() -> dict[Path, tuple[int, int, Optional[int]]] ``` Per-file `(season, episode, part)` taken from the SxxEyy column. Covers what the user typed / corrected in the dialog; rows whose field is empty or unparsable are omitted (they fall back to filename parsing in `import_episodes` and are skipped if that fails too). ### `SeriesOverviewDialog` *class · line 929* ```python class SeriesOverviewDialog(QDialog): ``` Read-only Series → Season → Episode tree with a per-series rename action. #### `SeriesOverviewDialog.__init__` *method · line 932* ```python __init__(parent: QWidget, filehandler: FileManager) -> None ``` Build the series overview: a tree of series → seasons → episodes with buttons to edit, rename or remove the selected node. #### `SeriesOverviewDialog._reload` *method · line 967* ```python _reload() -> None ``` Rescan the series libraries and rebuild the tree. Each episode shows whether its file name is already canonical ("OK") or needs renaming. #### `SeriesOverviewDialog._selected_series_index` *method · line 994* ```python _selected_series_index() ``` Index of the series that contains the current tree selection, or `None`. #### `SeriesOverviewDialog._selected_node` *method · line 1000* ```python _selected_node() ``` `("series"|"season"|"episode", Series, extra)` for the current selection, by tree depth — `extra` is `None` / `(season, eps)` / the `Episode`. `None` when nothing is selected. #### `SeriesOverviewDialog._rename_selected` *method · line 1015* ```python _rename_selected() -> None ``` Rename the episodes of the selected series to the canonical `SxxEyy - Title` form and report how many were renamed, skipped or conflicting. #### `SeriesOverviewDialog._remove_selected` *method · line 1027* ```python _remove_selected() -> None ``` Delete what is selected in the tree — a whole series, one season, or one episode — after confirmation. #### `SeriesOverviewDialog._edit_selected` *method · line 1056* ```python _edit_selected() -> None ``` Open the series editor for the selected series and apply the changes: episode numbers, folder rename/move and retitled episodes. ### `VersionLabelDialog` *class · line 1079* ```python class VersionLabelDialog(QDialog): ``` Pick a label for a new movie version — a 3D formát preset or free text. Jellyfin tells versions apart purely by this label text, so a 3D preset just *is* the label (`3D HSBS`) — same tokens it already scans for in a flat `.3D.HSBS` filename, just placed after " - " instead. #### `VersionLabelDialog.__init__` *method · line 1087* ```python __init__(parent: QWidget, initial: str='', intro: str='') -> None ``` Build the popisek dialog: a 3D format preset or free text. An empty text is allowed and means the default (plain) version. #### `VersionLabelDialog._sync_label` *method · line 1126* ```python _sync_label() -> None ``` Keep the text field in step with the 3D preset: a chosen format fills the field with `3D ` and disables it; "vlastní popisek" re-enables it. #### `VersionLabelDialog.label` *method · line 1131* ```python label() -> str ``` The popisek as entered (stripped). Empty means the default version. ### `DuplicatesDialog` *class · line 1135* ```python class DuplicatesDialog(QDialog): ``` The *Tools → Duplicate check* dialog: lists groups of movies that look like the same film (same TMDb id, same title and year, or identical file size), with the reasons, and offers to merge a movie into another as a version or to delete it. #### `DuplicatesDialog.__init__` *method · line 1143* ```python __init__(parent: QWidget, filehandler: FileManager) -> None ``` Build the duplicate checker: a tree of duplicate groups with the reasons they were flagged, and merge / delete actions for the selected movie. #### `DuplicatesDialog._reload` *method · line 1179* ```python _reload() -> None ``` Recompute the duplicate groups from the file manager and rebuild the tree. #### `DuplicatesDialog._selected` *method · line 1215* ```python _selected() -> tuple[dict, File] | None ``` The selected `(group, movie)` pair, or `None` when a group header or nothing is selected. #### `DuplicatesDialog._sync_buttons` *method · line 1225* ```python _sync_buttons() -> None ``` Enable the merge and delete buttons only when a movie (not a group header) is selected. #### `DuplicatesDialog._merge_selected` *method · line 1230* ```python _merge_selected() -> None ``` Fold the selected movie into another movie of its group as a version. Asks which movie to keep (when there are several) and for the popisek of the moved file. #### `DuplicatesDialog._remove_selected` *method · line 1260* ```python _remove_selected() -> None ``` Delete the selected movie from the pool (all its versions), after confirmation. ### `MovieEditDialog` *class · line 1282* ```python class MovieEditDialog(QDialog): ``` One dialog for every editable parameter of a movie. #### `MovieEditDialog.__init__` *method · line 1285* ```python __init__(parent: QWidget, file_obj: File, filehandler: FileManager) -> None ``` Build the "Upravit film" dialog for one movie: identity fields, 3D format, attributes, subtitles and the version list with add / rename / remove buttons. #### `MovieEditDialog._reload_subtitles` *method · line 1407* ```python _reload_subtitles() -> None ``` Refill the subtitle table from the subtitle files attached to the movie. #### `MovieEditDialog._add_subtitle` *method · line 1416* ```python _add_subtitle() -> None ``` Attach a subtitle file chosen in a dialog, asking for its language and whether it is forced. #### `MovieEditDialog._remove_subtitle` *method · line 1445* ```python _remove_subtitle() -> None ``` Delete the selected subtitle file after confirmation. #### `MovieEditDialog._refresh_identity_fields` *method · line 1462* ```python _refresh_identity_fields() -> None ``` (Re)fill TMDb ID and enable/disable the identity fields for the current grouping state of `self.file_obj` — called at open and again after adding/removing a version, since that can promote or demote it. #### `MovieEditDialog._reload_versions` *method · line 1494* ```python _reload_versions() -> None ``` Refill the version table. A grouped movie lists its files with their popisek; a flat movie shows one row labelled "Výchozí". #### `MovieEditDialog._add_version` *method · line 1518* ```python _add_version() -> None ``` Add another version: pick a file and a popisek, then call `add_movie_version` (which promotes a flat movie to a folder). #### `MovieEditDialog._rename_version` *method · line 1535* ```python _rename_version() -> None ``` Change the popisek of the selected version (only once the movie has two or more versions). #### `MovieEditDialog._remove_version` *method · line 1559* ```python _remove_version() -> None ``` Delete the selected version after confirmation. The dialog's own file can't be removed here. Down to one version, the movie is demoted back to a flat file. #### `MovieEditDialog._add_attr` *method · line 1587* ```python _add_attr(key: str, value: str) -> None ``` Append a key/value row to the attribute table. #### `MovieEditDialog._remove_attr` *method · line 1593* ```python _remove_attr() -> None ``` Remove the selected attribute row. #### `MovieEditDialog._lookup` *method · line 1598* ```python _lookup() -> None ``` Look up the title and year from TMDb by the typed TMDb id, or by the title when no id is given. #### `MovieEditDialog.title` *property · line 1624* ```python title -> str ``` The title as typed. #### `MovieEditDialog.year` *property · line 1628* ```python year -> str ``` The year as typed (may be empty). #### `MovieEditDialog.tmdb_id` *property · line 1632* ```python tmdb_id -> str ``` The TMDb id as typed (may be empty). #### `MovieEditDialog.library` *property · line 1636* ```python library -> str ``` The library chosen in the combo box. #### `MovieEditDialog.date` *property · line 1640* ```python date -> str ``` The date-added field as typed (`YYYY-MM-DD` or empty). #### `MovieEditDialog.attributes` *property · line 1644* ```python attributes -> dict ``` The attribute table as a dictionary; rows without a key are ignored. #### `MovieEditDialog.csfd_url` *property · line 1655* ```python csfd_url -> str ``` The ČSFD link as typed (may be empty). #### `MovieEditDialog.format_3d` *property · line 1659* ```python format_3d -> str ``` Selected 3D tag, or `""` for "— žádný —" (clears any existing tag). ### `SeriesEditDialog` *class · line 1664* ```python class SeriesEditDialog(QDialog): ``` Edit a series (title/year/id/library) and each episode's SxxEyy. #### `SeriesEditDialog.__init__` *method · line 1667* ```python __init__(parent: QWidget, ser, filehandler: FileManager) -> None ``` Build the series editor: series identity fields plus a season/episode number spin box pair for every episode. #### `SeriesEditDialog.accept` *method · line 1721* ```python accept() -> None ``` Refuse to close without a series title; otherwise accept the dialog. #### `SeriesEditDialog.episode_numbers` *method · line 1727* ```python episode_numbers() -> list[tuple] ``` The edited numbers as `(episode, season, episode number)` triples. #### `SeriesEditDialog.title` *property · line 1731* ```python title -> str ``` The series title as typed. #### `SeriesEditDialog.year` *property · line 1735* ```python year -> str ``` The series year as typed (may be empty). #### `SeriesEditDialog.tmdb_id` *property · line 1739* ```python tmdb_id -> str ``` The TMDb TV id as typed (may be empty). #### `SeriesEditDialog.library` *property · line 1743* ```python library -> str ``` The library chosen in the combo box. ### `LibraryOrganizerDialog` *class · line 1747* ```python class LibraryOrganizerDialog(QDialog): ``` Manage the pool's libraries (the top-level folders): name + kind. #### `LibraryOrganizerDialog.__init__` *method · line 1757* ```python __init__(parent: QWidget, libraries: list[dict]) -> None ``` Build the library list editor: one row per library folder with a kind selector. #### `LibraryOrganizerDialog._add_row` *method · line 1798* ```python _add_row(name: str, kind: str) -> None ``` Add a library row with the given name and kind. #### `LibraryOrganizerDialog._remove_row` *method · line 1809* ```python _remove_row() -> None ``` Remove the selected library row (the folder on disk is not touched). #### `LibraryOrganizerDialog.libraries` *method · line 1814* ```python libraries() -> list[dict] ``` The edited libraries as `{name, kind}` dictionaries; rows without a name are skipped. ### `MinidlnaLibrariesDialog` *class · line 1826* ```python class MinidlnaLibrariesDialog(QDialog): ``` Choose which pool libraries feed miniDLNA generation. #### `MinidlnaLibrariesDialog.__init__` *method · line 1829* ```python __init__(parent: QWidget, libraries: list[dict], selected: list[str] | None) -> None ``` Build the checklist of libraries that miniDLNA generation should include. #### `MinidlnaLibrariesDialog._set_all` *method · line 1872* ```python _set_all(state) -> None ``` Check or uncheck every library in the list. #### `MinidlnaLibrariesDialog.selected_libraries` *method · line 1876* ```python selected_libraries() -> list[str] ``` The names of the checked libraries. ### `MigrateDialog` *class · line 1881* ```python class MigrateDialog(QDialog): ``` Review TMDb match proposals for an existing pool and apply the ids. #### `MigrateDialog.__init__` *method · line 1887* ```python __init__(parent: QWidget, filehandler: FileManager, proposals: list) -> None ``` Build the review table of TMDb matching proposals for existing movies, with the candidate list per row. #### `MigrateDialog._apply` *method · line 1959* ```python _apply(only_auto: bool) -> None ``` Assign the chosen TMDb id to each proposal (`FileManager.assign_tmdb_id`, which renames the file to the canonical `Title (rok) [tmdbid]` form). `only_auto` limits it to proposals classified as confident. Failures are collected and reported; the dialog then closes. ### `AssignTagsDialog` *class · line 1983* ```python class AssignTagsDialog(QDialog): ``` Tri-state bulk tag assignment over the selected movies. Result maps full_path -> 1 (assign), 0 (remove), 2 (leave mixed/unchanged). #### `AssignTagsDialog.__init__` *method · line 1989* ```python __init__(parent: QWidget, tagmanager: TagManager, files: List[File], categories: List[str] | None=None) -> None ``` Build the tag checklist for the selected movies. A tag shown as partially checked is present on only some of them. #### `AssignTagsDialog.tagmanager_categories` *static method · line 2028* ```python tagmanager_categories(tagmanager: TagManager) -> List[str] ``` The category names of the tag manager, sorted. #### `AssignTagsDialog._category_item` *method · line 2031* ```python _category_item(category: str) -> QTreeWidgetItem ``` Return (creating if needed) the header item for a category. #### `AssignTagsDialog._add_tag_item` *method · line 2041* ```python _add_tag_item(category: str, name: str, force_checked: bool=False) ``` Add a checkable tag row under its category (deduplicated). #### `AssignTagsDialog._add_new_tag` *method · line 2063* ```python _add_new_tag() -> None ``` Prompt for a category + value and add it as a checked tag. #### `AssignTagsDialog.accept` *method · line 2079* ```python accept() -> None ``` Store the checkbox state of every tag in `result_map` and accept the dialog. ### `TagSchemaDialog` *class · line 2085* ```python class TagSchemaDialog(QDialog): ``` Editor for the tag schema: categories, their NFO source and folder rules. Each row defines a tag category, optionally which NFO field feeds it (with a value transform), and how it maps into the miniDLNA tree (a grouping folder, the output root, or no folders at all). #### `TagSchemaDialog.__init__` *method · line 2106* ```python __init__(parent: QWidget, schema: list[dict]) -> None ``` Build the editor of the tag schema: one row per tag category with its NFO source, whether it creates folders, and the folder name rules. #### `TagSchemaDialog._append_row` *method · line 2151* ```python _append_row(entry: dict) -> None ``` Add a category row filled from a schema entry. #### `TagSchemaDialog._combo_index` *static method · line 2185* ```python _combo_index(choices: list, value) -> int ``` Index of the entry whose value is `value` in a `(label, value)` choice list, or 0. #### `TagSchemaDialog._remove_selected` *method · line 2191* ```python _remove_selected() -> None ``` Remove the selected category row. #### `TagSchemaDialog._save` *method · line 2196* ```python _save() -> None ``` Read the table back into the schema list `result_schema`: one entry per row with a category, holding its CSFD field, value transform, miniDLNA folder root (only when "make folders" is ticked) and filename template. Then accept the dialog. ### `StatsDialog` *class · line 2216* ```python class StatsDialog(QDialog): ``` Read-only overview of the pool: totals + per-category tag breakdown. #### `StatsDialog.__init__` *method · line 2219* ```python __init__(parent: QWidget, stats: dict, size_text: str) -> None ``` Show the library statistics (movie count, total size, average rating and the other figures from `FileManager.statistics`). ### `QtApp` *class · line 2254* ```python class QtApp(QMainWindow): ``` The main window: menus, the tag sidebar, the movie and series table, the detail panel and the status bar. It delegates all work to `FileManager`. #### `QtApp.__init__` *method · line 2255* ```python __init__(filehandler: FileManager, tagmanager: TagManager) -> None ``` Build the main window around the file manager and the tag manager, then load the pool and restore the window geometry. #### `QtApp.reload_series` *method · line 2285* ```python reload_series() -> None ``` Refresh the cached series list (scans the series/mixed libraries). #### `QtApp._build_menu` *method · line 2293* ```python _build_menu() -> None ``` Create the menu bar: Pool, Filmy, Seriály, miniDLNA, Jellyfin, Nástroje, Nastavení and Testy. #### `QtApp._add_action` *method · line 2355* ```python _add_action(menu: QMenu, text: str, slot, shortcut: str | None=None) -> QAction ``` Create a menu action with an optional keyboard shortcut and connect it to `slot`. #### `QtApp._build_central` *method · line 2363* ```python _build_central() -> None ``` Build the central widget: the tag sidebar, the movie/series table and the detail panel, with a splitter between them. #### `QtApp._update_details` *method · line 2443* ```python _update_details() -> None ``` Fill the right panel from the single selected item (movie or series). #### `QtApp._build_statusbar` *method · line 2469* ```python _build_statusbar() -> None ``` Create the status bar with the pool, output and Jellyfin state labels. #### `QtApp._update_jellyfin_status` *method · line 2482* ```python _update_jellyfin_status(version: str | None) -> None ``` Show Jellyfin connection state; None = not checked / not configured. #### `QtApp.refresh_sidebar` *method · line 2496* ```python refresh_sidebar(filtered: Optional[List[File]]=None) -> None ``` Rebuild the filter tree, preserving the active filter and updating counts. The count after each tag is how many of `filtered` (the movies matching the current filter; all movies when nothing is checked) also carry that tag — i.e. how many would remain if that tag were checked. Check state is restored from `self._active_filter` so it survives the rebuild. #### `QtApp._on_tag_filter_changed` *method · line 2536* ```python _on_tag_filter_changed(item, _col) -> None ``` Update the active AND filter when a tag checkbox in the sidebar is toggled. Category header rows are ignored. #### `QtApp._active_filter_tags` *method · line 2549* ```python _active_filter_tags() -> List[Tag] ``` The active filter as `Tag` objects. #### `QtApp._refresh_library_filter` *method · line 2560* ```python _refresh_library_filter() -> None ``` Repopulate the library filter combo, keeping the current choice. #### `QtApp._selected_library_filter` *method · line 2574* ```python _selected_library_filter() -> str | None ``` The library the table is filtered to, or None for 'Vše'. #### `QtApp.refresh_table` *method · line 2581* ```python refresh_table(*_args) -> None ``` Rebuild the table from the current state: the tag filter, the library filter and the search text are applied to movies; series are filtered by library and search only (they carry no tags). Each movie group appears once, via its primary version. #### `QtApp._versions_word` *static method · line 2620* ```python _versions_word(count: int) -> str ``` The Czech plural form of "version" for a count (`verze` for 2–4, `verzí` otherwise). #### `QtApp._fill_row` *method · line 2623* ```python _fill_row(row: int, item) -> None ``` Fill one table row from a movie (File) or a series.Series. A movie made of several media files (a version folder) is marked so it doesn't pass for a single file: `▣` before the name in bold, the version count in the Typ column, the summed size and a tooltip listing every file. #### `QtApp._item_size` *method · line 2655* ```python _item_size(item) -> str ``` Formatted size of a movie (all its versions), or a series' episodes. #### `QtApp._format_size` *static method · line 2668* ```python _format_size(size_bytes: float) -> str ``` Format a byte count with a binary unit (B, KB, MB, GB, TB), one decimal place. #### `QtApp._selected_items` *method · line 2675* ```python _selected_items() -> list ``` The row objects (`File` or series) for the selected table rows, in row order. #### `QtApp._selected_movies` *method · line 2679* ```python _selected_movies() -> List[File] ``` The selected rows that are movies. #### `QtApp._selected_series` *method · line 2682* ```python _selected_series() -> list ``` The selected rows that are series. #### `QtApp.edit_item` *method · line 2685* ```python edit_item() -> None ``` Open the right editor for the single selected row (movie or series). #### `QtApp._show_table_menu` *method · line 2696* ```python _show_table_menu(pos) -> None ``` Show the context menu of the table for the current selection: series actions for a series-only selection, movie actions otherwise. #### `QtApp._update_selection_status` *method · line 2721* ```python _update_selection_status() -> None ``` Refresh the detail panel and the status bar for the current selection. #### `QtApp._update_path_status` *method · line 2735* ```python _update_path_status() -> None ``` Show the pool and output paths and the number of movies in the status bar. #### `QtApp.set_pool` *method · line 2746* ```python set_pool() -> None ``` Choose the pool folder, then reload the movies, series and library filter. #### `QtApp.set_filmoteka` *method · line 2757* ```python set_filmoteka() -> None ``` Choose the Filmotéka output folder. #### `QtApp.reload_pool` *method · line 2764* ```python reload_pool() -> None ``` Reload the movies and series from the pool and refresh the table. #### `QtApp.import_movie` *method · line 2772* ```python import_movie() -> None ``` Batch-import movie files: pick files, review them in `ImportMoviesDialog`, resolve collisions with films already in the pool (add as version, replace, or skip), then import the rows and attach version-linked rows to their targets. #### `QtApp._resolve_import_conflicts` *method · line 2888* ```python _resolve_import_conflicts(names: list[str]) -> str | None ``` Ask what to do with films already in the pool. Returns the policy for `FileManager.import_movie` — `"version"` (add the new file as another version of that film), `"replace"` (the whole old film goes, then the new one is imported) or `"skip"` — or `None` when the user cancels the import. There's deliberately no "keep both as separate films": these share a TMDb id, so they would share one movie record anyway — the same film twice is a duplicate, and a duplicate is what *versions* are for. #### `QtApp.open_movies` *method · line 2923* ```python open_movies() -> None ``` Open the selected movie files with the system viewer. #### `QtApp._open_path` *method · line 2927* ```python _open_path(path: Path) -> None ``` Open a file with the platform's default application (`startfile`, `open` or `xdg-open`). #### `QtApp.assign_tags` *method · line 2938* ```python assign_tags() -> None ``` Assign or remove tags on the selected movies through `AssignTagsDialog`. #### `QtApp.set_date` *method · line 2955* ```python set_date() -> None ``` Set or clear the date-added field on the selected movies. #### `QtApp.set_attribute` *method · line 2967* ```python set_attribute() -> None ``` Set or remove one free-form attribute on the selected movies. #### `QtApp.rename_movie` *method · line 2991* ```python rename_movie() -> None ``` Rename the single selected movie's file (keeping its extension). #### `QtApp.edit_movie` *method · line 3012* ```python edit_movie() -> None ``` Open the consolidated edit dialog for the selected movie. #### `QtApp.edit_series` *method · line 3035* ```python edit_series(ser) -> None ``` Open the series editor (params + per-episode SxxEyy tree) for `ser`. #### `QtApp.rename_to_canonical_naming` *method · line 3054* ```python rename_to_canonical_naming() -> None ``` Rename movies to the 'Title (rok) [tmdbid].ext' convention from metadata. #### `QtApp.import_episodes` *method · line 3116* ```python import_episodes() -> None ``` Import episode files into a series (Series/Season NN/Series SxxEyy). The dialog opens empty — add episodes file-by-file or pick a whole series folder (its subfolders are scanned too). #### `QtApp.show_series` *method · line 3144* ```python show_series() -> None ``` Open the series overview (`SeriesOverviewDialog`) and refresh the table afterwards. #### `QtApp.rename_all_series` *method · line 3152* ```python rename_all_series() -> None ``` Bulk-fix every series' episode filenames to `SxxEyy - {série}.ext`. #### `QtApp.normalize_default_names` *method · line 3217* ```python normalize_default_names() -> None ``` Tools menu action: rename ` - Výchozí.ext` files of grouped movies to the bare `.ext` name after confirmation. #### `QtApp.check_duplicates` *method · line 3235* ```python check_duplicates() -> None ``` Tools menu action: open `DuplicatesDialog` and refresh the table if it changed anything. #### `QtApp.migrate_pool` *method · line 3245* ```python migrate_pool() -> None ``` Match the existing pool against TMDb and assign ids (one-off migration). #### `QtApp.refresh_from_tmdb` *method · line 3279* ```python refresh_from_tmdb() -> None ``` (Re)load genre/year/rating tags from TMDb (by the tmdbid in the name). #### `QtApp.remove_movies` *method · line 3309* ```python remove_movies() -> None ``` Delete the selected movies from the pool (every version of a grouped movie is removed too), after confirmation. #### `QtApp.remove_series` *method · line 3330* ```python remove_series(series_list: list | None=None) -> None ``` Delete whole series folders from the pool (all seasons/episodes). #### `QtApp.select_minidlna_libraries` *method · line 3352* ```python select_minidlna_libraries() -> None ``` Choose which pool libraries feed miniDLNA generation (persisted). #### `QtApp.generate_filmoteka` *method · line 3370* ```python generate_filmoteka() -> None ``` Generate the miniDLNA / Filmotéka output for the libraries in scope. Refreshes genres from TMDb, syncs the genre hardlink tree (`HardlinkManager.sync_structure`, which also removes obsolete links), hardlinks mirrored libraries 1:1 and rebuilds the mixed-library series bucket. Empty scope shows an information message instead. #### `QtApp.edit_libraries` *method · line 3464* ```python edit_libraries() -> None ``` Open the library organizer (manage the pool's top-level libraries). #### `QtApp._cleanup_orphan_folders` *method · line 3483* ```python _cleanup_orphan_folders() -> None ``` Offer to delete empty pool folders that are no longer libraries. #### `QtApp.move_to_library` *method · line 3509* ```python move_to_library() -> None ``` Move the selected movies into another library. #### `QtApp._jellyfin_ready` *method · line 3539* ```python _jellyfin_ready() -> bool ``` True when the Jellyfin URL and API key are configured; otherwise shows a warning and returns False. #### `QtApp._jellyfin_call` *method · line 3549* ```python _jellyfin_call(fn) ``` Run a Jellyfin call under a wait cursor; return (result, error_str). #### `QtApp.jellyfin_check_connection` *method · line 3560* ```python jellyfin_check_connection() -> None ``` Check that the configured Jellyfin server responds and show its version. #### `QtApp.jellyfin_scan` *method · line 3572* ```python jellyfin_scan() -> None ``` Ask Jellyfin to scan its libraries. #### `QtApp.jellyfin_verify` *method · line 3586* ```python jellyfin_verify() -> None ``` Fetch the movie list from Jellyfin and compare it with the pool (`FileManager.verify_against_jellyfin`). Reports the matched count and lists movies present only in the pool or only in Jellyfin. #### `QtApp.edit_tag_schema` *method · line 3627* ```python edit_tag_schema() -> None ``` Edit the tag schema through `TagSchemaDialog` and store the result in the pool index. #### `QtApp.show_statistics` *method · line 3639* ```python show_statistics() -> None ``` Show the library statistics in `StatsDialog`; the pool must not be empty. #### `QtApp.check_consistency` *method · line 3646* ```python check_consistency() -> None ``` Test: does the pool index match the actual files in pool/Filmy? #### `QtApp.check_video_integrity` *method · line 3689* ```python check_video_integrity() -> None ``` Test: scan movies for corrupted / unreadable video data (FFmpeg). #### `QtApp.closeEvent` *method · line 3772* ```python closeEvent(event) -> None ``` Save the window size to the global config before the window closes. ## Functions ### `run` *function · line 3779* ```python run(filehandler: FileManager, tagmanager: TagManager) -> None ``` Create the Qt application (if none exists yet), show the main window and run the event loop until it closes.