Migrate from Poetry and pyenv to uv
This commit is contained in:
+219
-7
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user