Files

1889 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `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 <FORMAT>` 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 `<folder> - Výchozí.ext` files of grouped movies to the bare `<folder>.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.