# Changelog
Document all notable changes to the project, grouped by version and release date.
## Format
Each version entry uses these sections (include only those that apply):
- **Added** — new features
- **Changed** — changes to existing functionality
- **Fixed** — bug fixes
- **Removed** — removed features
- **Dependencies** — added, updated, or removed dependencies
## Rules
- Follow semantic versioning: `MAJOR.MINOR.PATCH`
- Newest version goes at the top
- Always update this file before bumping the version in `Cargo.toml`
- Document changes as they are made, not all at once at release time
## Unreleased
### Changed
- **The release binary is 2.5× smaller** (`curator` 42.7 → 17 MB, `tipy-dne`
6 → 1.8 MB): the release profile strips debug symbols, uses LTO with a single
codegen unit and optimises for size (`opt-level = "s"`), and the GUI renders
through OpenGL (`glow`) instead of `wgpu`.
### Dependencies
- `eframe` with the `glow` renderer only (default features off; `wgpu` and the
web screen-reader feature dropped).
## 2.0.0 — 2026-10-17
### Changed
- **Curator is now a Rust application.** The whole app — core and GUI — was
rewritten in Rust; the GUI is egui/eframe instead of PySide6. Behaviour and
on-disk formats are unchanged: the pool index (v2), tag schema, file naming,
versions/parts, series layout and miniDLNA output are the same, so an
existing pool opens as-is. Every menu action and dialog of the Python version
is there.
- Long operations (import, TMDb refresh, integrity scan, bulk renames, miniDLNA
generation) run in the background with a progress bar and a cancel button;
TMDb / ČSFD / Jellyfin lookups no longer freeze the window.
- **The global config moved** from `.Curator.!gtag` next to the script to the
platform config directory (`~/.config/curator/.Curator.!gtag` on Linux);
`CURATOR_CONFIG` overrides the location. An old `.Curator.!gtag` in the
working directory is read on first run, so the pool and output paths carry
over.
- **`tipy-dne` is a Rust binary** (built next to `curator`) replacing
`scripts/tipy_dne.py`; `scripts/tipy-dne.service` now runs it. Tips links are
named by the clean movie title, the same as the app's own tips folder.
- The status-bar paths are truncated (full path on hover) instead of
overlapping the status text.
### Fixed
- **"Tipy dne" from the systemd timer found no movies** since the pool index
moved to v2 — the script read index keys as file paths. `tipy-dne` reads the
media files of each movie record.
### Removed
- The Python sources, PyInstaller spec, `prebuild.py` and the committed
`dist/Curator` binary (a release build is `target/release/curator`).
- The one-off Python migration scripts for old ČSFD-era tag formats
(`migrate_tag_category`, `split_country_tags`, `strip_rating_bands`,
`strip_tag_categories`); they remain in the history of `devel`.
- The detailed per-function pages in `docs/modules/`; `cargo doc` generates the
API reference from the source.
### Dependencies
- Rust crates replace the Python stack: `eframe`/`egui`/`egui_extras` (GUI),
`rfd` (file dialogs), `reqwest` (HTTP, OS certificate store), `scraper` +
`ego-tree` (ČSFD), `sha2` (Anubis proof-of-work), `roxmltree` (NFO),
`serde`/`serde_json`, `regex`, `unicode-normalization`, `chrono`, `rand`,
`dirs`, `dotenvy`, `tracing`, `thiserror`/`anyhow`, `file-id`, `indexmap`;
dev: `tempfile`, `egui_kittest`.
## 1.14.0 — 2026-10-17
### Changed
- **The codebase is mypy-clean** and plain `uv run mypy` works (`[tool.mypy]`
in `pyproject.toml` lists the targets). Qt enums are now fully qualified
(`QDialog.DialogCode.Accepted`, `QMessageBox.StandardButton.Yes`,
`Qt.GlobalColor.red`, …) instead of PySide6's short aliases, and the few
loosely typed spots (tag schema, transforms, `on_files_changed`, dialog cell
widgets) are annotated. No behaviour change.
### Removed
- `src/core/media_utils.py` — Tagger-era leftovers (`load_icon` returned a
Tkinter image, `add_video_resolution_tag` logged via `print()`); nothing in
the app called either.
### Dependencies
- Removed `pillow` — `media_utils` was its only user.
## 1.13.0 — 2026-10-12
### Changed
- **The standalone build is now a single file.** `Curator.spec` produces one
self-contained `dist/Curator` executable (onefile) instead of a
`dist/Curator/` folder of libraries. It unpacks to a temp folder on every
start, so launching takes a few seconds longer.
- **Custom attributes are gone from the GUI.** The attribute table in
*Upravit film* and *Filmy → "Nastavit atribut…"* (menu and context menu) are
removed. Values already stored keep working in miniDLNA filename templates
(`File.name_context`); they just can't be edited from the app anymore.
- **The pool index is keyed by the movie, not by a file path** (index **v2**,
`src/core/movie.py`). A record used to describe one *file*, so a movie's
metadata was a passenger on one particular file — which is why the "primary
version" had to carry the tags, why removing it needed a metadata-rescue
hack, and why renaming a multi-version movie had to be disabled outright.
Now the record **is** the movie (`tmdb-603`, or a diacritics-folded
`name-
-` 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.` 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.