Migrate from Poetry and pyenv to uv

This commit is contained in:
2026-10-12 15:48:26 +02:00
parent c399a6aed3
commit c69f5f9166
93 changed files with 13068 additions and 2401 deletions
+219 -7
View File
@@ -19,9 +19,13 @@ Each version entry uses these sections (include only those that apply):
- 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
## 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
@@ -54,8 +58,106 @@ Each version entry uses these sections (include only those that apply):
**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
@@ -71,9 +173,15 @@ Each version entry uses these sections (include only those that apply):
then a small `VersionLabelDialog`: a **3D formát** preset — its label becomes
exactly `3D <FORMÁT>`, the same tokens Jellyfin scans for in a flat
`.3D.<FORMÁT>` filename — or free text for a non-3D version like `4K` /
`Director's Cut`) and **"➖ Odebrat verzi"** (removing down to one version
**demotes back to flat** automatically). The table still shows **one row
per movie** — a group's other versions live only in that list, not as
`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/
@@ -82,9 +190,8 @@ Each version entry uses these sections (include only those that apply):
supported yet (those fields are disabled in the edit dialog; remove the
version or edit the files directly). `FileManager.is_movie_group_member` /
`list_movie_versions` / `primary_version` / `add_movie_version` /
`remove_movie_version`. Scoped to `movie`-kind libraries for now —
`mixed`-library grouped movies are a follow-up (their subfolders are still
read as series only).
`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),
@@ -240,6 +347,69 @@ Each version entry uses these sections (include only those that apply):
`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
@@ -269,6 +439,48 @@ Each version entry uses these sections (include only those that apply):
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