Files

59 KiB
Raw Permalink Blame History

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 Cargo.toml
  • Document changes as they are made, not all at once at release time

Unreleased

Changed

  • The release binary is 2.5× smaller (curator 42.7 → 17 MB, tipy-dne 6 → 1.8 MB): the release profile strips debug symbols, uses LTO with a single codegen unit and optimises for size (opt-level = "s"), and the GUI renders through OpenGL (glow) instead of wgpu.

Dependencies

  • eframe with the glow renderer only (default features off; wgpu and the web screen-reader feature dropped).

2.0.0 — 2026-10-17

Changed

  • Curator is now a Rust application. The whole app — core and GUI — was rewritten in Rust; the GUI is egui/eframe instead of PySide6. Behaviour and on-disk formats are unchanged: the pool index (v2), tag schema, file naming, versions/parts, series layout and miniDLNA output are the same, so an existing pool opens as-is. Every menu action and dialog of the Python version is there.
  • Long operations (import, TMDb refresh, integrity scan, bulk renames, miniDLNA generation) run in the background with a progress bar and a cancel button; TMDb / ČSFD / Jellyfin lookups no longer freeze the window.
  • The global config moved from .Curator.!gtag next to the script to the platform config directory (~/.config/curator/.Curator.!gtag on Linux); CURATOR_CONFIG overrides the location. An old .Curator.!gtag in the working directory is read on first run, so the pool and output paths carry over.
  • tipy-dne is a Rust binary (built next to curator) replacing scripts/tipy_dne.py; scripts/tipy-dne.service now runs it. Tips links are named by the clean movie title, the same as the app's own tips folder.
  • The status-bar paths are truncated (full path on hover) instead of overlapping the status text.

Fixed

  • "Tipy dne" from the systemd timer found no movies since the pool index moved to v2 — the script read index keys as file paths. tipy-dne reads the media files of each movie record.

Removed

  • The Python sources, PyInstaller spec, prebuild.py and the committed dist/Curator binary (a release build is target/release/curator).
  • The one-off Python migration scripts for old ČSFD-era tag formats (migrate_tag_category, split_country_tags, strip_rating_bands, strip_tag_categories); they remain in the history of devel.
  • The detailed per-function pages in docs/modules/; cargo doc generates the API reference from the source.

Dependencies

  • Rust crates replace the Python stack: eframe/egui/egui_extras (GUI), rfd (file dialogs), reqwest (HTTP, OS certificate store), scraper + ego-tree (ČSFD), sha2 (Anubis proof-of-work), roxmltree (NFO), serde/serde_json, regex, unicode-normalization, chrono, rand, dirs, dotenvy, tracing, thiserror/anyhow, file-id, indexmap; dev: tempfile, egui_kittest.

1.14.0 — 2026-10-17

Changed

  • The codebase is mypy-clean and plain uv run mypy works ([tool.mypy] in pyproject.toml lists the targets). Qt enums are now fully qualified (QDialog.DialogCode.Accepted, QMessageBox.StandardButton.Yes, Qt.GlobalColor.red, …) instead of PySide6's short aliases, and the few loosely typed spots (tag schema, transforms, on_files_changed, dialog cell widgets) are annotated. No behaviour change.

Removed

  • src/core/media_utils.py — Tagger-era leftovers (load_icon returned a Tkinter image, add_video_resolution_tag logged via print()); nothing in the app called either.

Dependencies

  • Removed pillow — media_utils was its only user.

1.13.0 — 2026-10-12

Changed

  • The standalone build is now a single file. Curator.spec produces one self-contained dist/Curator executable (onefile) instead of a dist/Curator/ folder of libraries. It unpacks to a temp folder on every start, so launching takes a few seconds longer.
  • Custom attributes are gone from the GUI. The attribute table in Upravit film and Filmy → "Nastavit atribut…" (menu and context menu) are removed. Values already stored keep working in miniDLNA filename templates (File.name_context); they just can't be edited from the app anymore.
  • 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-<title>-<year> slug without an id) and owns its files:
    "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.
  • "Odebrat z poolu" removes the whole movie (FileManager.remove_movie): on a multi-version row it deletes every version, their subtitles and the version folder — it used to delete only the file shown in the table and leave the rest behind as a half-empty group. Subtitles of a flat movie are removed too now.

Added

  • Upravit film: one "Média filmu" section with a folder mode. A flat movie (one file in the library root, plus subtitles) shows as "Samostatný film"; adding a second medium moves it into its own folder automatically. The folder has two modes, chosen with a "Verze / Díly" switch: versions — alternatives, each with its own popisek (free text such as "kinoverze", "zkrácená verze", or a 3D preset via "✏️ Popisek / 3D…"); parts — pieces played back to back, listed in order and reordered with ↑ / ↓. "➕ Přidat médium…" adds by the chosen mode (for a flat movie the switch decides what the second file becomes). Switching an existing folder converts it: to parts numbers the files in their current order; to versions makes the first file the default and asks a popisek for each other one. New FileManager.movie_group_mode, ordered_media (parts sorted numerically, part2 before part10), move_movie_part, set_movie_group_mode; all of them relabel the folder through temporary names, so swapping two parts never overwrites a file.
  • Movie parts (Jellyfin multi-part / stacking) — a film that TMDb lists as one movie but that you have as several files. Upravit film gets "➕ Přidat díl…": the film moves into its own folder (Jellyfin requires it for parts, even with no other versions) as Folder-part1.ext, Folder-part2.ext, … and Jellyfin plays them back to back as one movie — unlike versions, which it offers as alternatives. Under the hood a part is a version whose popisek is partN, so everything that handles versions handles parts too (removing down to one demotes to flat, subtitles follow each part, renaming works). Any Jellyfin spelling typed as a popisek (part2, CD2, pt 2, disc-2) is written back as -part2 and the existing file becomes part1 — so the import collision dialog's "Přidat jako verzi" with popisek CD2 works the same way. The version table shows "díl N", the main table "Film · N díly". Parts and versions can't be combined in one folder yet (add_movie_part refuses). New FileManager.add_movie_part, movie.normalize_part_label / part_label.
  • Upravit seriál: TMDb lookup, optionally from a movie id. The series editor gets a "🔎 Dohledat" button (title + year from TMDb, by the typed id or by the title) and a "Metadata projednou z ID filmu (ne seriálu)" checkbox for series that TMDb lists only as a movie (a miniseries cut together, a documentary in parts): ticked, the lookup uses the movie endpoints instead of the TV ones. The movie id is then not written into the series folder's [tmdbid-N] — Jellyfin's TV provider would resolve it as a TV id and match an unrelated show — so Jellyfin matches the series by title and year instead (SeriesEditDialog._lookup, .tmdb_id).
  • Import movies: assign a version right in the batch (e.g. a 2D + a 3D file of the same film picked together). A row's new "🔗 Přiřadit jako verzi…" button picks which other row in the dialog it's a version of and a popisek (pre-filled with the 3D formát detected in its filename via naming.three_d_format_from_filename — e.g. Avatar.2009.3D.HSBS.mkv → 3D HSBS — free text otherwise, same VersionLabelDialog as Upravit film → Verze); its own název/rok/TMDb ID/ČSFD fields grey out since they come from the film it's a version of. On import the target row lands first via import_movie, then every linked row is added onto it via FileManager.add_movie_version — one movie with several versions instead of two separate (duplicate) movies. Removing a row it points to un-links it back to standalone; a row already pointed at by others can't itself become a version (no chains).
  • Import movies: "3D formát" per row, for the opposite case — a film whose only file is entirely 3D, with no plain version to make it a version of. A combo box (pre-filled from the filename, same detection as the popisek above) tags a standalone row's import straight to Title (rok) [tmdbid].3D.<FORMÁT>.ext, exactly like the 3D formát dropdown in Upravit film — no more importing plain, then editing it in just to add the tag. Disabled (and reset) the moment a row is linked as a version or gains one as a follower, since a grouped movie's 3D-ness belongs on the specific version's popisek instead, never on the group as a whole.
  • Duplicate check (Nástroje → Kontrola duplicit…, FileManager.find_duplicates). Lists movies that are in the pool more than once — a version folder counts as one movie, never as a duplicate of itself. Flagged when two movies share the same TMDb id (e.g. X.mkv and X.3D.HSBS.mkv lying flat side by side, or one film in two libraries), the same title + year (one copy paired with TMDb, the other not), or a byte-identical file size (a plain copy); overlapping hits form one group with every reason listed. Per file the dialog offers "Sloučit jako verzi do…" (FileManager.merge_as_version: moves the file into the other movie's version folder — promoting it from flat when needed, a 3D file pre-labelled 3D <FORMÁT> — carries its subtitles along, keeps the target's metadata and only adds missing tags / ČSFD link, and drops the duplicate's own record) or "Smazat vybraný z poolu…".
  • Multi-version movies are marked in the main table: ▣ before the bold name, Film · N verze in the Typ column, the size summed over all versions and a tooltip listing every file; the detail panel shows the folder and the version count.
  • Episodes split into physical parts — an old recording cut into several files, as opposed to E01-E02 (several episodes in one file) — get Jellyfin's own .ptN stacking suffix: SxxEyy - {série}.ptN.ext. Recognised from cd/dvd/part/pt/disc/disk + a number already in the source filename (Show.S01E02.pt2.mkv, Show S01E01-part-1.mkv, …), or typed by hand in the import dialog's SxxEyy column as S01E02P3 (new series.parse_episode_part/parse_episode_code); carried through scanning, import, "Napravit názvy souborů…" and the series tree.
  • Přehled seriálů → "Odebrat z poolu…" now targets whatever is selected in the tree, not always the whole series: pick a single episode to delete just that file (+ its subtitles), a season to delete the whole Season NN/ Specials folder, or the series itself as before. New FileManager.remove_episodes/remove_season; a season folder left completely empty by the removal is cleaned up, one still holding something else is left alone.
  • 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), "✏️ Přejmenovat verzi…" (FileManager.rename_movie_version — same dialog, pre-filled with the current label; renames the file + its subtitles in place, shared metadata untouched) and "➖ Odebrat verzi" (removing down to one version demotes back to flat automatically). A flat movie always shows one row too — labelled „Výchozí", a placeholder since it has no on-disk label yet; renaming is disabled until a second version exists (there's nothing to rename it from until then). 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. Works in mixed libraries too (see Fixed below for how a version folder there is told apart from a series).
  • 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.

Fixed

  • Adding/removing a movie version could silently drop its 3D formát. Promoting a flat, 3D-only movie to a group (adding a second version) always renamed the pre-existing file to " - Výchozí", discarding whatever .3D.<FORMAT> tag it had — same the other way round: demoting back to flat when the sole survivor's own popisek was a 3D formát always produced a plain filename. Both now read the file's own current formát (naming.parse_3d_format / movie.normalize_3d_label) and carry it over automatically, so a movie can freely gain and lose versions — 3D-only, 2D-only, several formats at once — without ever losing a formát that wasn't touched in that particular step. Affects add_movie_version (its existing_label param, and merge_as_version's keep_label which now derives from it too) and remove_movie_version.
  • A version's popisek can be left empty — for a plain "2D" file with nothing else to call it — instead of being rejected; it becomes movie.DEFAULT_VERSION_LABEL ("Výchozí") the same way the pre-existing file already did on promotion. Applies to add_movie_version, rename_movie_version, merge_as_version, and everywhere they're driven from the GUI (Upravit film → Verze, Kontrola duplicit, Importovat filmy…'s "Přiřadit jako verzi…") — none of them require typing "Výchozí" by hand anymore. VersionLabelDialog's popisek field says so directly.
  • Renaming or demoting a version could wipe the film's tags. Every version holds its own in-memory copy of the shared metadata; one loaded before a tag was added wrote its stale copy back when it was renamed, relabelled or left as the sole survivor. Those paths now use the new File.save_location, which re-reads the movie record first so only the file's own path and popisek change.
  • A movie's version couldn't be removed when the edit dialog was open on it. Upravit film opens on the movie's primary version, and removing that very file was blocked ("Nelze odebrat verzi, kterou právě upravuješ…") — so in a folder whose first file alphabetically was the one to go, it couldn't go at all. It can now: the dialog carries on with a sibling (all versions share one movie record) and Upravit film applies its edits to that file.
  • Parts and versions could be mixed in one folder, e.g. -part1, -part2 and a version labelled díl 1 — and the version table showed both the part and the version as "díl 1". add_movie_version and rename_movie_version now refuse a folder holding both kinds, and the version table has separate Typ (díl N / verze / Výchozí) and Popisek (the exact label) columns.
  • The default version of a grouped movie is named bare. In a folder of two or more versions the plain one is now Folder/Folder.ext, not Folder/Folder - Výchozí.ext: "Výchozí" is only the table's name for it. Other versions stay Folder - <popisek>.ext. The version table and index label read it as the default. Pool files already named - Výchozí are renamed with Nástroje → "Sjednotit názvy výchozích verzí…" (FileManager.normalize_default_version_names; subtitles and metadata follow, a folder whose bare name is already taken is left alone).
  • Importing a film already in the pool added a root-level duplicate instead of a version. pooled_with_stem compared only the filename stem, so the files of a grouped movie (Folder - 3D HSBS.mkv) never matched the film's identity and the import wrote Title (rok) [tmdbid].mkv beside the folder. Matching is now by movie identity (flat file, any version of a group, or a 3D-tagged copy). When the batch import finds such a film it asks "Přidat jako verzi" / "Nahradit celý film novým" / "Přeskočit" (no "keep both as separate films" — same TMDb id means one movie record, so that's just a duplicate) and, for "Přidat jako verzi", asks for each file's popisek (pre-filled from its 3D formát). import_movie(on_conflict="version", version_label=…) does the adding; "replace" removes a grouped movie as a whole rather than one of its files, and "suffix" no longer lands beside a folder of the same identity.
  • Repair for an already-created root duplicate: Nástroje → Kontrola duplicit flags it (same TMDb id) and "Sloučit jako verzi do…" folds it into the group.

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.
  • Adding a movie version in a mixed library silently broke the movie. Its version folder and a series folder are both Title (rok) [tmdbid]/ — indistinguishable by name — and a mixed library's subfolders used to be read as series unconditionally (load_pool_movies only scanned that library's top-level files as movies). So the movie vanished from the Film list and reappeared, mis-parsed, as a "series" — no longer reachable via Upravit film. Told apart by shape now instead: a series always nests its episodes in a Season NN/Specials folder, a movie's version folder never does (series.looks_like_series_folder). A mixed library's canonical-shaped subfolder without one is read as a movie by load_pool_movies/is_movie_group_member, and skipped by scan_series (skip_movie_folders=True there, so a movie-and-series pair with the same identity can sit side by side without either one leaking into the other's listing) — no manual per-item type switch needed.

Dependencies

  • Poetry and pyenv are replaced by uv. pyproject.toml was recreated with uv init / uv add (no [tool.poetry], no [build-system]), poetry.lock is replaced by uv.lock, and .venv is built on a uv-managed Python 3.14. Commands are now uv sync / uv add / uv run (DESIGN_DOCUMENT_PYTHON.md §11). Dependency lower bounds moved up to the versions uv resolved; Poetry's upper bounds (<7.0.0, …) are gone.
  • Added pyinstaller to the dev group — uv run pyinstaller Curator.spec now works out of the box (it was used for builds but never declared).
  • ruff 0.15 → 0.17 (also mypy 2.4, pytest 9.1, PySide6 6.12, Pillow 12.3, python-dotenv 1.2.4). ruff 0.17 enables a much wider default rule set; the whole codebase was brought in line with it — see Changed below.

Changed

  • Codebase cleaned up for ruff 0.17's default rules (uv run ruff check passes). Mostly mechanical: PEP 604/585 annotations (X | None, list[...] instead of Optional/List), sorted imports, stale # noqa removed, unused unpacked variables prefixed with _, silent try/except/pass blocks rewritten as contextlib.suppress, dialog class-level tables in src/ui/qt_app.py annotated ClassVar. scripts/tipy_dne.py is now executable (it has a shebang).
  • Three blanket except Exception handlers are narrowed — anything outside the listed types now propagates instead of being swallowed: config.load_global_config (OSError/ValueError/TypeError), the ffprobe resolution lookup in media_utils (OSError/SubprocessError/ValueError), and the window-geometry restore in qt_app (ValueError/AttributeError).
  • User-facing "missing packages" hints (ČSFD) now say uv sync instead of poetry install.

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).