928 lines
59 KiB
Markdown
928 lines
59 KiB
Markdown
# 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
|
||
|
||
## 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:
|
||
```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.
|
||
- 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).
|