Files
Curator/CHANGELOG.md
2026-10-17 09:03:35 +02:00

871 lines
56 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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.
### Dependencies
- 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).
## 1.13.0 — 2026-10-12
### Changed
- **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:
```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.
- **"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.
- **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).