# Changelog Document all notable changes to the project, grouped by version and release date. ## Format Each version entry uses these sections (include only those that apply): - **Added** — new features - **Changed** — changes to existing functionality - **Fixed** — bug fixes - **Removed** — removed features - **Dependencies** — added, updated, or removed dependencies ## Rules - Follow semantic versioning: `MAJOR.MINOR.PATCH` - Newest version goes at the top - Always update this file before bumping the version in `pyproject.toml` - Document changes as they are made, not all at once at release time ## Unreleased ### Changed - **The pool index is keyed by the movie, not by a file path** (index **v2**, `src/core/movie.py`). A record used to describe one *file*, so a movie's metadata was a passenger on one particular file — which is why the "primary version" had to carry the tags, why removing it needed a metadata-rescue hack, and why renaming a multi-version movie had to be disabled outright. Now the record **is** the movie (`tmdb-603`, or a diacritics-folded `name--<year>` slug without an id) and owns its files: ```json "tmdb-19995": {"title": "Avatar", "year": "2009", "tmdb_id": 19995, "library": "Filmy", "tags": [...], "csfd_url": …, "media": [{"role": "video", "label": "", "file": "…"}, {"role": "video", "label": "3D HSBS", "file": "…"}]} ``` Every version of a film therefore reads and writes **the same** metadata — adding a version inherits it, removing any version (including the one shown in the table) leaves it intact, and the rescue hack (plus the interim `File.is_primary_version` flag) is gone. A newly seen file also joins an existing record of the same identity instead of blanking its metadata. `Movie.desired_layout` derives where the files belong from the record alone (one video → flat in the library, two or more → its own `Title (rok) [tmdbid]/` folder; a 3D label keeps Jellyfin's documented `.3D.<FORMAT>` spelling in both layouts, other labels use `" - label"`) — **not wired in yet**: `add_movie_version` / `remove_movie_version` still lay files out themselves, and the edit dialog doesn't apply a recomputed layout on rename. A move now updates the record's `library` as well. `check_data_consistency` compares disk against the records' media paths. `File` is now a *view* on one media entry of a record (same public interface); `PoolIndex` gained `get_movie`/`set_movie`/`delete_movie`/ `rekey_movie`/`all_movies`/`find_movie_by_media`/`record_for`, and **migrates a v1 index on first open** — metadata only, **no file is moved** (a v1 movie already sits where v2 wants it), with the old index copied to `.Curator.!index.v1.bak` first. ### Added - **Multiple versions of one movie** (Jellyfin's own "multiple versions" — 2D+3D, 4K+1080p, Director's Cut, …). A movie is identified by its `(title, year, tmdbid)` and stays a **flat file** while it has exactly one playable version (today's layout, unchanged). Adding a second version **promotes it automatically** into its own folder — Jellyfin's required shape, every file prefixed with the exact folder name: ``` Title (rok) [tmdbid]/ Title (rok) [tmdbid] - Výchozí.ext Title (rok) [tmdbid] - 3D HSBS.ext ``` *Upravit film* gets a **„Verze"** list — **"➕ Přidat verzi…"** (pick a file, then a small `VersionLabelDialog`: a **3D formát** preset — its label becomes exactly `3D <FORMÁT>`, the same tokens Jellyfin scans for in a flat `.3D.<FORMÁT>` filename — or free text for a non-3D version like `4K` / `Director's Cut`) and **"➖ Odebrat verzi"** (removing down to one version **demotes back to flat** automatically). The table still shows **one row per movie** — a group's other versions live only in that list, not as separate rows (`FileManager.visible_movies`; the row shown is the plain / `Výchozí` version, `primary_version`). All versions share **one metadata record** (see *Changed → index v2*), so a new version inherits the title/ tags/ČSFD/atributy and removing any version — even the one shown in the table — leaves them intact. Renaming/moving/retitling a grouped movie isn't supported yet (those fields are disabled in the edit dialog; remove the version or edit the files directly). `FileManager.is_movie_group_member` / `list_movie_versions` / `primary_version` / `add_movie_version` / `remove_movie_version`. Scoped to `movie`-kind libraries for now — `mixed`-library grouped movies are a follow-up (their subfolders are still read as series only). - **3D movie tagging** (`naming.py`, Jellyfin's own convention — https://jellyfin.org/docs/general/server/media/movies/). *Upravit film* gets a **„3D formát"** dropdown (žádný / HSBS / FSBS / HTAB / FTAB / MVC), pre-filled from the current filename; picking one renames the pool file to `Title (rok) [tmdbid].3D.<FORMÁT>.ext`. The tag is preserved across every future rename (editing other fields, "Přejmenovat dle metadat") the same way `[tmdbid-N]` already is — `canonical_pool_stem` / `parse_3d_format` / `THREE_D_FORMATS`; `apply_movie_edits(format_3d=…)` (omitted = keep, `""` = clear). This is for a movie that's **only ever going to have one file** — deliberately **not** exposed in the import dialog: importing a second file for a movie already in the pool that way would create a plain, ungrouped duplicate instead of a proper version (see "Multiple movie versions" above, which is the right tool for 2D+3D of the same film). `import_movie` still takes `format_3d=…` as a building block, just with no UI wired to it. - **miniDLNA hardcopies `homevideo` libraries too.** A `homevideo` library (no TMDb id, arbitrary internal structure) has no genre tags to build a tree from — same reasoning as series — so it's now mirrored whole into `output/<název knihovny>` alongside the series copy-as-is mirrors (`FileManager.mirror_as_is_libraries` = `series` + `homevideo`; the series-only `copyasis_folders` used by the library organizer is unchanged). Respects the per-library miniDLNA selection. - **miniDLNA covers series inside `mixed` libraries.** A `mixed` library's top-level files are movies (genre-tagged as usual); any **series in its subfolders** now get hardcopied into `output/Seriály/<vlastní název>/…` — no genre tags apply to a series, so it bypasses the tag tree entirely, same mechanism as a dedicated series library's copy-as-is mirror but scoped to one series' own folder. Respects the per-library miniDLNA selection below. `FileManager.minidlna_scope` now returns a third element, the in-scope mixed-library `series.Series` list. - **miniDLNA: choose which libraries generate.** *miniDLNA → "Knihovny pro generování…"* — a checklist of the pool's libraries (persisted in the pool index, default = all, matching the previous behaviour). "Vygenerovat miniDLNA strom" then only includes movies from the checked libraries in the genre tree / Tipy dne / Nově přidané, and only mirrors checked series/homevideo libraries — the dialog warns up front that unchecking one removes its output. A movie library's genre-tree links are swept as obsolete the normal way; a mirrored (series/homevideo) library or a mixed-library series has **no** obsolete-link mechanism of its own, so deselecting one is handled explicitly: `FileManager.minidlna_prune_targets` works out what fell out of scope (the whole-library output folder, or — since a mixed-library series and a same-named real library can share one output bucket like `Seriály` — just the stale entries in it) and `HardlinkManager.remove_mirror` deletes it. `FileManager.minidlna_libraries` / `set_minidlna_libraries` / `minidlna_scope`. - **External subtitle management for movies** (`src/core/subtitles.py`). Jellyfin plays an external `.srt`/`.ass`/`.ssa`/`.sub`/`.vtt` natively when it shares the video's filename stem (`Title (rok) [tmdbid].cs.srt`, optionally `.forced`/ `.sdh`/`.default`) — no muxing needed. *Upravit film* lists a movie's attached subtitles and can **"➕ Přidat titulky…"** (pick a file, tag its language, mark forced; copied in and named to match) or remove one. Subtitles now **follow the movie through every rename/move** (`rename_movie`, `apply_movie_edits`, `move_to_library`, `rename_to_canonical`, and `rename_episodes_to_canonical` for series) instead of silently detaching — `subtitles.rename_subtitles_with`. `FileManager.list_subtitles` / `attach_subtitle` / `remove_subtitle`. Presence is read live from disk (like NFO), nothing is cached in the pool index. - **Movies and series in one list.** The main table lists both as rows with a **Typ** column (Film / Seriál); size is the file size for a movie and the sum of the episodes for a series. Double-click (or the context menu) opens the right editor by type — a movie → the movie parameters dialog, a series → the series editor with the per-episode SxxEyy tree. The right detail panel handles both. Library filter + search cover both; a tag filter narrows movies (series carry no tags). - **Detail panel on the right** of the table — the selected item's filename (or series folder), Czech title, library and size (`_update_details`). - **Consolidated edit dialogs.** *Filmy → "Upravit film…"* (double-click) opens one dialog for every Curator-owned movie parameter — title, year, TMDb ID (with "Dohledat z TMDb"), library, date, attributes and a ČSFD link — applied at once (rename `Title (rok) [tmdbid].ext`, move between libraries, set metadata) via `FileManager.apply_movie_edits`. A series opens an editor (in *Přehled seriálů* or from the list): edit its title/year/TMDb ID/library **and each episode's season/episode number**, renaming the folder and laying episodes out canonically (`FileManager.apply_series_edits`). - **Library organizer** (*Pool → "Knihovny…"*). One dialog manages the pool's top-level libraries as ``{name, kind}`` — kinds **movie / series / mixed / homevideo** — stored in a unified ``libraries`` setting in the pool index (`FileManager.libraries` / `set_libraries`; legacy `movie_libraries` / `copyasis_folders` are derived from it and synthesised for old pools). Setting the pool creates a folder for every library. Replaces the separate "Knihovny (rozdělení)" and "Copy-as-is složky" dialogs. **mixed** and **homevideo** can be defined now; homevideo scanning lands in a later phase. - **Series import into `mixed` libraries.** `series_libraries` (import target + `load_series` scan) covers kind `series` **and** `mixed`, so episodes can be imported into a mixed library and show up in the series overview/editor. - **ČSFD link stored as a second DB pointer** (`File.csfd_url`) — captured at import (a column in the movie import dialog) and editable in *Upravit film*. - **ČSFD → TMDb title bridge** (`src/core/csfd.py`, lean revival of the removed scraper). A direct TMDb search on a Czech distribution title is unreliable (remakes, localized names); ČSFD indexes Czech release names well **and** lists each film's *original* title, so the flow is: filename → ČSFD search → ČSFD film page → original title + year → TMDb search → tmdbid. - Import dialog: a second button **"🎬 Dohledat přes ČSFD"** next to "Dohledat z TMDb". Per row it uses the filled ČSFD odkaz, else searches ČSFD by the cleaned filename; fills ČSFD odkaz / TMDb ID / Název / Rok (blue = suggested). - Migration: a movie carrying a `csfd_url` is resolved through ČSFD first, so `match_pool_to_tmdb` searches on the original title + year (the proposal is tinted blue with a *"přes ČSFD: …"* tooltip). `FileManager.resolve_via_csfd`. - Search is scoped to the *Filmy* section and rejects two-segment episode hrefs (a bare-title search otherwise returned a series episode); a yearful query that misses is retried without the year. The original title is only taken from a `.film-names` entry whose flag matches the country of origin — a Czech film (which lists only foreign alt-titles there) keeps its `<h1>` title instead of a random Slovenian/English one. - Scope: a title→tmdbid bridge only — **no ČSFD tags/genres return** (those stay TMDb-via-Jellyfin/NFO). ČSFD's Anubis proof-of-work wall is solved (SHA-256, stdlib) and replayed over a shared session so a batch pays it once. - **Import rows pre-filled from metadata next to the source file.** When a picked video sits next to metadata from an earlier Curator/Tagger setup, the import row is seeded on open (no network) — **ČSFD odkaz**, Název, Rok (blue = suggested, verify): - `src/core/legacy_meta.py` — reads Curator's **pre-index** layouts: a per-file `.{filename}.!tag` JSON sidecar (Tagger / early Curator; key `csfd_link`), else a `.Curator.!index` found walking up the tree (matched by relative key, then by unique basename). These win for the ČSFD link. - `nfo.read_nfo` also parses a ČSFD id/URL from `<uniqueid type="csfd">` (bare id or full URL) / legacy `<csfdid>` / `<csfd>` (`nfo.NfoData.csfd_url`), and fills what the sidecar/index don't carry (notably the TMDb id). - `ImportMoviesDialog._prefill_row`. - **Import: "Vynechat řádky bez TMDb ID".** A checkbox in the import dialog that drops every row without a TMDb ID on OK — they are skipped (and no longer need a year). Rows that would be skipped are shown struck-through/italic as the box is ticked; the import summary reports how many were skipped (`ImportMoviesDialog.skipped_no_tmdb`). - **Episode import: "📁 Přidat složku…".** The episode-import dialog can now take a whole series folder and scans it **recursively** (all subfolders — `Season 01`, `Season 02`, …), adding every video file it finds (sidecars skipped); season/episode still parsed per file. A canonical folder name (`Title (rok) [tmdbid-N]`) also pre-fills the series title / year / TMDb ID. The dialog now opens empty (add files or a folder from inside it). `series.collect_episode_files`, `ImportEpisodesDialog._add_folder`. - **Episode import: editable SxxEyy per row.** The `SxxEyy` column is now an editable field, pre-filled from the filename and colour-coded (green valid / red unparsable / plain empty). Fill in an unrecognised episode (or fix a mis-parsed one) right in the import dialog — `S01E05` / `1x05` both work; OK warns before importing if any row still has no valid code. `ImportEpisodesDialog.episode_codes` feeds a new `codes=` override on `FileManager.import_episodes`. - **Remove a series from the pool.** *Seriály → "Odebrat z poolu…"*, the table context menu (series-only selection, one or more), and a button in *Přehled seriálů* delete a whole series folder — every season and episode — after a confirm. `FileManager.remove_series` (refuses a folder outside the pool). - **Bulk-fix series episode filenames.** *Seriály → "Napravit názvy souborů…"* goes over every series in the pool and renames each episode to **`SxxEyy - {název seriálu}.ext`** in the right `Season NN` / `Specials` folder, then removes the season folders emptied by the moves. Episodes with no recognisable `SxxEyy` are left in place and listed (fix them in the series editor). `FileManager.rename_all_series_to_canonical`. - **Pool migration to TMDb** (`src/core/migrate.py`, *Nástroje → "Spárovat pool s TMDb (migrace)…"*). Matches existing movies (no `tmdbid` yet) against TMDb by title + year and proposes an id, filling a missing year from TMDb. Conservative classification — **jisté** (exact diacritics-insensitive title + matching year, unique), **nejednoznačné** (pick from candidates), **bez shody**. A review dialog applies the confident ones or the ones you pick → renames to `Title (rok) [tmdbid].ext`. Already-migrated files are skipped (re-runnable). `FileManager.match_pool_to_tmdb` / `assign_tmdb_id`. ### Changed - **Tags come from TMDb, not NFO.** *Filmy → "Načíst žánry z TMDb"* (`FileManager.refresh_tags_from_tmdb`, `tmdb.movie_details`) fetches genres/year/country/rating by the movie's tmdbid and regenerates its tags; generating the miniDLNA tree refreshes from TMDb first. Genres come localized (cs-CZ); production countries stay TMDb's English names. Removes the wait for Jellyfin to write NFO before the genre tree is populated. - **Movie edit dialog: dropped the free-form tags field**, added **ČSFD odkaz** (genre tags come from TMDb; manual tag editing isn't needed in the Jellyfin flow). - **Episode filename is `SxxEyy - {série}.ext`** inside `Series (rok) [tmdbid]/ Season NN/`. The release string is deliberately dropped — identity is the `SxxEyy` code and Jellyfin fetches the episode title/metadata from TMDb (matched by the code + the folder's `[tmdbid-N]`); an empty/unsafe title collapses to `SxxEyy.ext`. `episode_filename` takes the series title, `is_canonical_episode(path, series_title)`, and import / rename / edit all follow (`series.sanitize_title`). ### Fixed - **Movies can live in mixed libraries.** The import/edit/move dialogs and the library filter offered only `movie`-kind libraries (falling back to a default "Filmy"), so a pool of only mixed/series/homevideo libraries had nowhere to put a movie. They now use `libraries_accepting_movies` (kind **movie or mixed**), and `load_pool_movies` scans a mixed library's **top-level** files as movies (subfolders are series). Movies can be imported into / moved between mixed libraries. - **Cleanup of leftover library folders.** After editing libraries, Curator offers to delete **empty** pool folders that are no longer a library, and lists non-empty ones (never auto-deleted). `FileManager.orphan_pool_folders` / `remove_empty_pool_folder`. ## 1.12.0 — 2026-08-31 ### Added - **Series support** (Jellyfin TV layout, phase 5; `src/core/series.py`, menu **Seriály**). Episodes are matched by `SxxEyy` (also `1x02`) and laid out as `Seriály/Series (rok) [tmdbid]/Season NN/Series SxxEyy.ext` (season 0 → `Specials`). *"Importovat epizody…"* places episode files (series identified via TMDb TV lookup, per-file SxxEyy parsed from the filename); *"Přehled seriálů…"* shows a Series → Season → Episode tree with a naming status and a per-series **"Přejmenovat epizody"** that renames mis-named episodes into the right Season folder. `FileManager` gained `load_series`, `import_episodes`, `rename_episodes_to_canonical`; `tmdb.lookup_tv` / `search_tv` resolve series. - **NFO-driven genres, ČSFD removed** (phase 4). Curator's metadata source is now the `.nfo` Jellyfin writes into the media folders (`src/core/nfo.py`, read-only). *Filmy → "Načíst žánry z NFO"* (`FileManager.refresh_tags_from_nfo`) reads genre/year/country/rating from each movie's NFO and (re)generates its tags (user-added tags kept); generating the miniDLNA tree refreshes from NFO first, so the genre-split tree reflects what Jellyfin knows. The **Filmotéka** menu is now **miniDLNA** ("Vygenerovat miniDLNA strom"). - **Jellyfin integration** (thin/hybrid, phase 3; `src/core/jellyfin.py`, menu **Jellyfin**). NFO/artwork on disk stay authoritative; the client only triggers a scan and reads back what Jellyfin sees. **"Skenovat knihovnu"** (`POST /Library/Refresh`), **"Ověřit proti poolu…"** (matches pooled movies to Jellyfin items by TMDb id, then title+year, and reports what is out of sync — `FileManager.verify_against_jellyfin`), **"Zkontrolovat připojení"**. The status bar shows the Jellyfin state (nenastaveno / nezkontrolováno / ● verze). Config via `.env` — `JELLYFIN_URL`, `JELLYFIN_API_KEY`. - **Movie libraries as physical partitions** (Jellyfin-first, phase 2). The pool can now hold several top-level movie libraries (`movie_libraries` in the pool index — default `Filmy`; add Kids/Dokumenty/Anime via *Pool → "Knihovny (rozdělení)…"*). Import lands in a chosen library, and *Filmy → "Přesunout do knihovny…"* (context menu too) moves a movie between them. The movie table gained a **Knihovna** column and a **library filter** (Vše / per library) in the toolbar. `FileManager` gained `movie_libraries` / `set_movie_libraries`, `import_movie(library=…)`, `move_to_library`, and `library_of`; `load_pool_movies` and the consistency check now scan every movie library and skip the sidecars Jellyfin writes (`.nfo`, artwork, subtitles). - **TMDb identity source** (`src/core/tmdb.py`) — start of the Jellyfin-first pivot. Resolves a **TMDb id** (or a title/year search) into a canonical **title + year**; the rich metadata/artwork are left to Jellyfin. Needs a free `TMDB_API_KEY` in `.env`; degrades gracefully without it. - **`[tmdbid-N]` in the pool naming convention** (`naming.py`): the pooled file is now `Title (YYYY) [tmdbid-N].ext` when the id is known — the exact form Jellyfin matches on. The id is optional (plain `Title (YYYY)` still valid), parsed back by `parse_tmdb_id`, preserved across `rename_to_canonical`, and stripped from the miniDLNA hardlink names (they stay clean `Title.ext`). `FileManager.import_movie` takes a `tmdb_id`. ### Changed - **Import reworked around TMDb, ČSFD removed from the import flow.** The import dialog's "ČSFD odkaz" column is now **TMDb ID**, and **"Dohledat z TMDb"** fills Title/Year/id (by id, or by searching the filename) — a suggested value is tinted blue, a name colliding with the pool red. Import no longer fetches ČSFD tags. (The legacy ČSFD tag actions remain until the tag tree is replaced by NFO-driven miniDLNA generation.) ### Removed - **ČSFD entirely.** Deleted `src/core/csfd.py` (scraping, Anubis PoW solver) and its tests; dropped `File.apply_csfd_tags` / `set_csfd_link` / `get_cached_movie`, the `csfd_link` field and the ČSFD menu/context actions. `File.csfd_cache` → `File.meta` and `csfd_tag_paths` → `auto_tag_paths` (both read the old index keys for back-compat); the folder-name transforms moved to `src/core/transforms.py`. ## 1.11.0 — 2026-07-27 ### Added - **Pool naming convention `Title (YYYY).ext`** (`src/core/naming.py`). Pooled movies now carry a four-digit year in parentheses, so two same-named films coexist and the filename is self-describing. - **Import requires a year**: the import dialog has a per-row **Rok** field (pre-filled from the source filename), and *"Najít ČSFD odkazy"* now also fills the year from the top ČSFD hit — a **suggested** year is tinted **blue** (verify it), a name **colliding** with the pool stays **red**. The import is blocked until every row has a four-digit year, and the file lands in the pool as `Title (rok).ext`. Same title + different year no longer collide. - **Rename to the convention**: *Filmy → "Přejmenovat dle ČSFD (rok)…"* (`FileManager.rename_all_to_canonical`) renames selected movies (or the whole pool) to `Title (rok).ext` using the year from their ČSFD metadata, with a cancelable progress dialog; files with no known year are skipped and name collisions reported. `File.title` keeps the clean title (without the year); `File.name_context` recovers the year from a canonical filename when no ČSFD/tag year exists. ### Changed - **Filmotéka hardlinks are named by the clean title (no year).** In the tag tree a movie now links as `Title.ext` instead of the pooled `Title (YYYY).ext`. When two *different* films would collide on that name in the **same folder**, the colliding ones keep the year (`Title (YYYY).ext`) to tell them apart; a residual clash (same title *and* year) gets a stable numeric suffix. Copy-as-is mirrors (Seriály) are untouched — they stay 1:1. A per-category `filename_template` (Tag schéma dialog) still overrides the default inside its folders. Link creation and obsolete-detection now share one collision-resolved plan (`HardlinkManager._plan_links`), so they can't disagree on a name — the first generation after upgrading re-lays the affected links once, then it is stable. - `File.name_context`'s `year` field is now always a string (was an int when it came straight from the ČSFD cache) — uniform for filename templates. ## 1.10.0 — 2026-07-07 ### Added - **Video integrity check** (`src/core/integrity.py`, *Testy → "Kontrola integrity videa…"*): scans movies for corrupted/unreadable video data via FFmpeg. Two depths — **Důkladná** (full `ffmpeg` decode to `-f null`, catches mid-file data corruption, slow) and **Rychlá** (`ffprobe` container/stream probe, catches truncated/unreadable files, fast). Runs on the selected movies or the whole pool, with a cancelable progress dialog, and reports the bad files (with the first decoder error) separately from those that couldn't be checked. `FileManager.scan_video_integrity` drives the scan; `check_video_integrity` checks one file and degrades gracefully when FFmpeg isn't installed. ## 1.9.0 — 2026-07-07 ### Fixed - **Endless Filmotéka churn (`vytvořeno: N, odebráno: N` every run).** When a movie was re-imported/replaced (new inode), the old hardlink kept the correct name but pointed at the dead inode. Obsolete detection matched links **by name only**, so it spared that orphan and instead forced the freshly created, correct link to a `_1` suffix — which, having an unexpected name, was swept on the next run and recreated again, forever. `HardlinkManager.find_obsolete_links` now matches **both the path and the inode**: an orphan squatting the expected name is removed and the base name is restored, so generation is stable after one cleanup run. ### Changed - **Status bar** now shows the current **Pool** and **Filmotéka** paths as permanent widgets (right-aligned), so they stay visible while the transient area shows the movie count or the current selection — previously the paths were part of the transient message and were wiped as soon as a selection updated the status. ## 1.8.0 — 2026-07-03 ### Changed - **Split config responsibilities by scope.** The global config (`.Curator.!gtag`) now carries **application data only** — window state, MRU folders, and the `pool_dir` / `filmoteka_dir` pointers. Everything describing a particular library — **`tag_schema`** and **`copyasis_folders`** — moved into the pool index (`.Curator.!index`, under a new `settings` section), so it travels with the pool. `FileManager` reads/writes those via the index (`PoolIndex.get_setting` / `set_setting`) and **migrates** any values left in an old global config into the index once, on first open (`FileManager._migrate_global_settings`). ### Removed - **Per-folder config (`.Curator.!ftag`)** and the whole legacy folder-scan path (`FileManager.append` / `get_folder_config` / `save_folder_config` / `set_ignore_patterns` / `get_ignore_patterns`, `config.load_folder_config` etc.) — inherited from Tagger and unused by the Filmotéka workflow. - **Per-file metadata sidecars (`.{filename}.!tag`).** `File` metadata is now stored only in the pool index; an index-less `File` is an in-memory object and writes nothing to disk. - **Old tkinter GUI** (`src/ui/gui.py`), orphaned since the PySide6 rewrite and the only remaining consumer of the folder-config/sidecar code. ## 1.7.0 — 2026-07-02 ### Added - **Nastavení → "Statistiky…"**: a library overview dialog — total movies, total size, average ČSFD rating, with/without ČSFD link, untagged count, and a per-category tag breakdown (counts sorted by frequency). `FileManager.statistics()` aggregates it. - **Testy menu → "Kontrola konzistence dat…"**: compares the pool index against the actual files in pool/Filmy and reports **metadata without a file** (movie deleted directly) and **files without metadata** (dropped into the pool without importing). Read-only diagnosis (`FileManager.check_data_consistency`). - **"- Nově přidané"** special folder in the Filmotéka: the **10** most recently added movies as hardlinks (while **"- Tipy dne"** holds **15** random ones). Import now records an **`added`** timestamp on each movie (`File.added`); ordering falls back to the file's **mtime** for older items — not ctime, which the hardlink generation itself bumps (`File.added_timestamp`, `HardlinkManager.generate_recently_added`). - Standalone **`scripts/tipy_dne.py`** to regenerate the "Tipy dne" folder independently of the app (deployable on the server / cron): reads the pool index, picks N random movies (default 15), clears the tips folder and recreates the hardlinks. Stdlib only; paths via `--pool`/`--output` or the global config. Ships with a systemd oneshot `tipy-dne.service` + daily `tipy-dne.timer`. ### Changed - **Special folders are prefixed with "- "** so DLNA/TV browsers sort them ahead of the genre folders at the output root: the grouping roots (now `- Dle roku`, `- Dle země původu`, `- Dle hodnocení`, …) plus `- Tipy dne` and `- Nově přidané`. Genre folders stay at the root, listed after the special ones. ### Fixed - Obsolete-link cleanup now also removes **orphan links** in the tag tree — hardlinks pointing at an inode no longer in the pool (movie re-imported / replaced) — not just links whose current pool inode moved. Within a managed folder, any file that isn't an expected link is swept, so leftovers from a renamed layout (e.g. an old unprefixed `Dle roku`) are cleaned up on the next generation instead of lingering forever. ## 1.6.0 — 2026-06-16 ### Added - Filmotéka generation now also creates a **"Tipy dne"** folder with **10 random pool movies** (hardlinks; copy-as-is folders like Seriály are excluded). The folder is refreshed each run (`HardlinkManager.generate_random_tips`). ### Fixed - Stale **root-level genre folders** were left in the Filmotéka output when the last movie carrying a genre dropped that tag: obsolete-link cleanup only scanned genre folders still present on current movies, so an emptied one was never visited. Cleanup now also sweeps other root-level folders (excluding the grouping roots and `reserved_subfolders` — copy-as-is mirrors, "Tipy dne") and removes the now-empty folder. Grouping folders (Dle roku/…) were already fine. ## 1.5.0 — 2026-06-16 ### Added - **Free-form per-movie attributes** (`attributes` in the index, set via *Filmy → Nastavit atribut…* / context menu): arbitrary `key → value` pairs (e.g. `collection_sort`) that are exposed to Filmotéka filename templates through `File.name_context`. So a Kolekce template `"{collection_sort} - {title}{ext}"` orders the folder as `01 - Dr. No.mkv`, `02 - …`. Empty value removes the attr. - **Per-category filename template** in the tag schema (`filename_template`, editable in *Nastavení → Tag schéma…*): the hardlink inside that category's folders is named from the template (fields `title`/`year`/`rating`/`ext`/ `stem`/`filename`), e.g. a *Kolekce* with `"{year} - {title}{ext}"` yields `Dle kolekce/James Bond/1962 - Dr. No.mkv` while every other folder (and the pool) keeps the plain filename. `HardlinkManager` takes `category_filename_templates`; `File.name_context()` supplies the fields. - The "Přiřadit štítky" dialog now lists **all tag-schema categories** (including empty user ones like *Kolekce*) and has a **"➕ Nový štítek…"** button to create a tag value (e.g. *Kolekce/James Bond*) on the spot and assign it. Tag values are created on first assignment — the schema editor defines categories + rules, this dialog defines/assigns the individual values. ### Changed - Tag schema `transform` now shapes only the **Filmotéka folder name**, not the tag value. ČSFD tags keep the **exact value** (rating → `Hodnocení/90`) while the grouping transform is applied when building folders (→ `Dle hodnocení/ 90–100 %`). `csfd_field_values` no longer transforms; `HardlinkManager` takes a `category_transforms` map (`FileManager.filmoteka_category_transforms`) and `apply_transform`/`decade_band` run at folder-generation time. ### Fixed - Sidebar filter tree no longer re-expands every category on each check/uncheck: the expanded/collapsed state is preserved across the rebuild (new categories still default to expanded). ## 1.3.0 — 2026-06-16 ### Added - **Configurable tag schema** (`tag_schema` in the global config, edited via *Nastavení → Tag schéma…*): a single source of truth for which tag categories exist, which ČSFD field feeds each (with an optional value transform, e.g. `decade_band` for the rating), and how each maps into the Filmotéka tree (`""` = output root, `"Dle …"` = grouping folder, none = filter-only, no folders). Replaces the hard-coded `FILMOTEKA_CATEGORY_ROOTS` and the fixed category list in `apply_csfd_tags`; `HardlinkManager` roots are now derived from the schema (`FileManager.filmoteka_category_roots`). - **Tag provenance (ČSFD vs user)**: each file tracks which tags came from ČSFD (`csfd_tags` in the index). `apply_csfd_tags` now **regenerates only the ČSFD tags** from the schema and leaves user-added tags untouched — so changing a movie's ČSFD link refreshes its ČSFD tags without wiping your own. ## 1.2.0 — 2026-06-16 ### Added - Fork of the former **Tagger** project as **Curator**, a movie-library manager. - **Pool** concept (single source of truth) with `Filmy` / `Seriály` folders and a configurable **Filmotéka** output folder, stored in the global config. - **Multi-file "Import movies" flow**: pick several videos at once and give each its own Title + ČSFD link (one row per file, more addable in the dialog); a copy/move toggle chooses whether sources are copied (default, non-destructive) or moved into `pool/Filmy` as `Title.ext`. Imported movies are indexed and, if a ČSFD link is set, enriched with tags right away. - **Auto-find ČSFD links** in the import dialog ("🔎 Najít ČSFD odkazy"): for every row without a link it cleans the filename into a query (`clean_filename_to_query` strips resolution/codec/source/group, keeps the year) and fills in the first ČSFD search hit (`find_csfd_url` → `search_movies`, reusing one Anubis session). Existing links are never overwritten; results are a suggestion the user can review before importing. - Import dialog now **highlights in red any title that already exists in the pool** (live as you type) and lets you **remove a single row** (✕ per row), so you can drop one file without discarding the whole batch and re-picking. - **Conflict resolution on import**: when a row is left red (name already in the pool), import asks whether to **replace** the existing movie(s) with the new file, **keep both** (numeric suffix), or **cancel**. `FileManager.import_movie` gained an `on_conflict` policy (`suffix` / `replace` / `skip`); `replace` evicts the existing same-named movie (file + index metadata) first. - `File` now stores `title` and `csfd_link`. - New **PySide6** GUI reframed around the Filmotéka workflow (pool setup, import, tag filter sidebar, movie table, one-click Filmotéka generation), replacing the tkinter GUI as the entry point. - Seriály **copy-as-is mirror**: `pool/Seriály` is cloned 1:1 into the Filmotéka output as hardlinks (`HardlinkManager.mirror_as_is`), generated alongside the movie tree. - **Unified pool metadata index** (`pool_index.py`): the whole pool's metadata lives in a single `<pool>/.Curator.!index` JSON keyed by pool-relative path. `File` reads/writes the index when one is injected and otherwise keeps using per-file `.!tag` sidecars; `FileManager` uses the index for the pool. - **Configurable copy-as-is folders**: `copyasis_folders` in the global config (editable from the GUI) lists pool subfolders mirrored 1:1 during generation; `Seriály` is the default. Generalizes the previously hardcoded Seriály mirror. - Project `README.md` (overview, concepts, workflow, run/build instructions). - **ČSFD scraping** (`csfd.py`, ported from the Tagger devel branch): fetches movie data from a ČSFD link (JSON-LD + HTML parsing). `File.apply_csfd_tags` assigns **Žánr / Rok / Země původu / Hodnocení** tags and caches the fetched data (incl. directors and the first 10 actors) in the metadata. The rating is bucketed into ten-point bands (`rating_band`, e.g. `80–89 %`, `90–100 %`). **Directors and actors are collected but intentionally not turned into tags or Filmotéka folders** — there would be far too many. The GUI auto-fetches on import when a link is given and offers "Načíst tagy z ČSFD" for selected movies. - **Rename a pooled movie** from the app ("Přejmenovat…" in the Movie menu / context menu, F2): `FileManager.rename_movie` renames the physical file in pool/Filmy to `<new name>.<ext>` (extension preserved), moves its metadata to the new index key, and syncs `title`/`filename`. Refuses empty names, names with path separators, and collisions with an existing pooled file. - App startup injects `truststore` so HTTPS uses the OS certificate store — ČSFD fetching works behind corporate SSL inspection (where certifi's bundle lacks the proxy root CA). ### Fixed - `media_utils.add_video_resolution_tag` referenced `subprocess` without importing it. - ČSFD parsing updated for the current site HTML: year is read from JSON-LD `dateCreated`, and the origin line (now bullet-separated, no commas) is tokenized so country / year / duration are extracted correctly. - **ČSFD anti-bot wall (Anubis):** ČSFD now serves a proof-of-work challenge page instead of the movie, so fetches returned a film with no genres/year ("načteno 0 tagů"). `csfd.py` now detects the Anubis challenge, solves the SHA-256 proof-of-work the way the bundled worker JS does, and replays the request through a `requests.Session` (reused across a batch so only the first fetch pays the PoW cost). Žánr / Rok / Země původu tags load again. - "Assign tags" dialog crashed on PySide6/Qt6 — `Qt.ItemIsTristate` was renamed to `Qt.ItemIsAutoTristate`. - Sidebar tag-filter checkboxes never appeared checked: every toggle triggered a table refresh that rebuilt the tree from scratch (all unchecked), wiping the click. The active filter is now kept in a separate model (`_active_filter`) and restored on rebuild. The count after each tag is also now filter-aware — it shows how many of the currently filtered movies carry that tag (i.e. how many would remain if it were checked), instead of always the pool-wide total. The refresh is deferred via `QTimer.singleShot` so the tree is not rebuilt inside its own `itemChanged` signal (which deleted the item Qt was still processing and crashed the app with SIGSEGV on a real click). ### Changed - ČSFD country tag category renamed **Země → Země původu**. Added `scripts/migrate_tag_category.py` to rewrite the category in an existing pool index (backs up `.Curator.!index` first); run against the live pool. - Filmotéka tree **relaid out**: genre folders now sit **directly at the output root** (next to the copy-as-is Seriály mirror), with year tags grouped under a **`Dle roku`** folder and country tags under **`Dle země původu`**. `HardlinkManager` gained a category → root-folder map (`category_roots`, empty root = tag folders at the output root) and now restricts obsolete-link cleanup to the tag-tree's own top-level folders, so copy-as-is mirrors are never touched. The tree also groups the ČSFD rating under `Dle hodnocení`. - ČSFD origin is now parsed as **multiple countries**: a co-production like "USA / Velká Británie" becomes a separate **Země původu** tag per country (so the film is filed under each), instead of one combined tag. `CSFDMovie` gained `countries: list[str]` (replacing the single `country`); the csfd cache schema bumped to v2 (legacy single-country caches are split on read). - Movie table trimmed to **Název / Štítky / Velikost** — the Datum and ČSFD columns were dropped (a ČSFD link is a prerequisite, so its indicator was always the same). - All references to "Tagger" renamed to "Curator" (code, spec, config filenames `.Curator.!gtag` / `.Curator.!ftag`, tests). - `requires-python` narrowed to `>=3.14,<3.15` (PySide6 compatibility). ### Removed - Duplicate `src/core/constants.py` (hard-coded stale `VERSION = "v1.0.3"`). The GUI window title imported from it, so it showed the wrong version. All imports now use `src/constants.py` (version derived from `pyproject.toml`), which also absorbed `APP_VIEWPORT`; the window title uses `APP_TITLE` → "Curator vX.Y.Z". - Legacy Tagger predefined tags: the always-available **Hodnocení** (⭐ rating) and **Barva** (color) categories in `TagManager`, and the automatic **Stav/Nové** tag assigned to every newly imported file. `DEFAULT_TAGS` is now empty; the pool is driven by ČSFD-derived tags (Žánr / Rok / Země původu). ### Dependencies - Added `pyside6` (GUI), `requests` + `beautifulsoup4` (ČSFD scraping), `truststore` (OS cert store for HTTPS). Declared `python-dotenv`, `pillow`, `loguru` (already imported by the inherited code).