55 KiB
55 KiB
Changelog
Document all notable changes to the project, grouped by version and release date.
Format
Each version entry uses these sections (include only those that apply):
- Added — new features
- Changed — changes to existing functionality
- Fixed — bug fixes
- Removed — removed features
- Dependencies — added, updated, or removed dependencies
Rules
- Follow semantic versioning:
MAJOR.MINOR.PATCH - Newest version goes at the top
- Always update this file before bumping the version in
pyproject.toml - Document changes as they are made, not all at once at release time
1.13.0 — 2026-10-12
Changed
- Custom attributes are gone from the GUI. The attribute table in
Upravit film and Filmy → "Nastavit atribut…" (menu and context menu) are
removed. Values already stored keep working in miniDLNA filename templates
(
File.name_context); they just can't be edited from the app anymore. - The pool index is keyed by the movie, not by a file path (index v2,
src/core/movie.py). A record used to describe one file, so a movie's metadata was a passenger on one particular file — which is why the "primary version" had to carry the tags, why removing it needed a metadata-rescue hack, and why renaming a multi-version movie had to be disabled outright. Now the record is the movie (tmdb-603, or a diacritics-foldedname-<title>-<year>slug without an id) and owns its files: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"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": "…"}]}File.is_primary_versionflag) is gone. A newly seen file also joins an existing record of the same identity instead of blanking its metadata.Movie.desired_layoutderives where the files belong from the record alone (one video → flat in the library, two or more → its ownTitle (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_versionstill lay files out themselves, and the edit dialog doesn't apply a recomputed layout on rename. A move now updates the record'slibraryas well.check_data_consistencycompares disk against the records' media paths.Fileis now a view on one media entry of a record (same public interface);PoolIndexgainedget_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.bakfirst. - "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,part2beforepart10),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 ispartN, 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-part2and the existing file becomespart1— so the import collision dialog's "Přidat jako verzi" with popisekCD2works 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_partrefuses). NewFileManager.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, sameVersionLabelDialogas 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 viaimport_movie, then every linked row is added onto it viaFileManager.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.mkvandX.3D.HSBS.mkvlying 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-labelled3D <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 verzein 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.ptNstacking 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 asS01E02P3(newseries.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/Specialsfolder, or the series itself as before. NewFileManager.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:Upravit film gets a „Verze" list — "➕ Přidat verzi…" (pick a file, then a smallTitle (rok) [tmdbid]/ Title (rok) [tmdbid] - Výchozí.ext Title (rok) [tmdbid] - 3D HSBS.extVersionLabelDialog: a 3D formát preset — its label becomes exactly3D <FORMÁT>, the same tokens Jellyfin scans for in a flat.3D.<FORMÁT>filename — or free text for a non-3D version like4K/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 inmixedlibraries 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 toTitle (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_moviestill takesformat_3d=…as a building block, just with no UI wired to it. - miniDLNA hardcopies
homevideolibraries too. Ahomevideolibrary (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 intooutput/<název knihovny>alongside the series copy-as-is mirrors (FileManager.mirror_as_is_libraries=series+homevideo; the series-onlycopyasis_foldersused by the library organizer is unchanged). Respects the per-library miniDLNA selection. - miniDLNA covers series inside
mixedlibraries. Amixedlibrary's top-level files are movies (genre-tagged as usual); any series in its subfolders now get hardcopied intooutput/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_scopenow returns a third element, the in-scope mixed-libraryseries.Serieslist. - 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_targetsworks 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 likeSeriály— just the stale entries in it) andHardlinkManager.remove_mirrordeletes 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/.vttnatively 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, andrename_episodes_to_canonicalfor 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) viaFileManager.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 unifiedlibrariessetting in the pool index (FileManager.libraries/set_libraries; legacymovie_libraries/copyasis_foldersare 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
mixedlibraries.series_libraries(import target +load_seriesscan) covers kindseriesandmixed, 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_urlis resolved through ČSFD first, somatch_pool_to_tmdbsearches 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-namesentry 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}.!tagJSON sidecar (Tagger / early Curator; keycsfd_link), else a.Curator.!indexfound walking up the tree (matched by relative key, then by unique basename). These win for the ČSFD link.nfo.read_nfoalso 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
SxxEyycolumn 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/1x05both work; OK warns before importing if any row still has no valid code.ImportEpisodesDialog.episode_codesfeeds a newcodes=override onFileManager.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}.extin the rightSeason NN/Specialsfolder, then removes the season folders emptied by the moves. Episodes with no recognisableSxxEyyare 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 (notmdbidyet) 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 toTitle (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. Affectsadd_movie_version(itsexisting_labelparam, andmerge_as_version'skeep_labelwhich now derives from it too) andremove_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 toadd_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,-part2and a version labelleddíl 1— and the version table showed both the part and the version as "díl 1".add_movie_versionandrename_movie_versionnow 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, notFolder/Folder - Výchozí.ext: "Výchozí" is only the table's name for it. Other versions stayFolder - <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_stemcompared only the filename stem, so the files of a grouped movie (Folder - 3D HSBS.mkv) never matched the film's identity and the import wroteTitle (rok) [tmdbid].mkvbeside 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}.extinsideSeries (rok) [tmdbid]/ Season NN/. The release string is deliberately dropped — identity is theSxxEyycode and Jellyfin fetches the episode title/metadata from TMDb (matched by the code + the folder's[tmdbid-N]); an empty/unsafe title collapses toSxxEyy.ext.episode_filenametakes 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 uselibraries_accepting_movies(kind movie or mixed), andload_pool_moviesscans 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
mixedlibrary silently broke the movie. Its version folder and a series folder are bothTitle (rok) [tmdbid]/— indistinguishable by name — and amixedlibrary's subfolders used to be read as series unconditionally (load_pool_moviesonly 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 aSeason NN/Specialsfolder, a movie's version folder never does (series.looks_like_series_folder). Amixedlibrary's canonical-shaped subfolder without one is read as a movie byload_pool_movies/is_movie_group_member, and skipped byscan_series(skip_movie_folders=Truethere, so amovie-and-seriespair 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.tomlwas recreated withuv init/uv add(no[tool.poetry], no[build-system]),poetry.lockis replaced byuv.lock, and.venvis built on a uv-managed Python 3.14. Commands are nowuv 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 checkpasses). Mostly mechanical: PEP 604/585 annotations (X | None,list[...]instead ofOptional/List), sorted imports, stale# noqaremoved, unused unpacked variables prefixed with_, silenttry/except/passblocks rewritten ascontextlib.suppress, dialog class-level tables insrc/ui/qt_app.pyannotatedClassVar.scripts/tipy_dne.pyis now executable (it has a shebang). - Three blanket
except Exceptionhandlers are narrowed — anything outside the listed types now propagates instead of being swallowed:config.load_global_config(OSError/ValueError/TypeError), the ffprobe resolution lookup inmedia_utils(OSError/SubprocessError/ValueError), and the window-geometry restore inqt_app(ValueError/AttributeError). - User-facing "missing packages" hints (ČSFD) now say
uv syncinstead ofpoetry 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 bySxxEyy(also1x02) and laid out asSeriá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.FileManagergainedload_series,import_episodes,rename_episodes_to_canonical;tmdb.lookup_tv/search_tvresolve series. - NFO-driven genres, ČSFD removed (phase 4). Curator's metadata source is now
the
.nfoJellyfin 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_librariesin the pool index — defaultFilmy; 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.FileManagergainedmovie_libraries/set_movie_libraries,import_movie(library=…),move_to_library, andlibrary_of;load_pool_moviesand 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 freeTMDB_API_KEYin.env; degrades gracefully without it. [tmdbid-N]in the pool naming convention (naming.py): the pooled file is nowTitle (YYYY) [tmdbid-N].extwhen the id is known — the exact form Jellyfin matches on. The id is optional (plainTitle (YYYY)still valid), parsed back byparse_tmdb_id, preserved acrossrename_to_canonical, and stripped from the miniDLNA hardlink names (they stay cleanTitle.ext).FileManager.import_movietakes atmdb_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; droppedFile.apply_csfd_tags/set_csfd_link/get_cached_movie, thecsfd_linkfield and the ČSFD menu/context actions.File.csfd_cache→File.metaandcsfd_tag_paths→auto_tag_paths(both read the old index keys for back-compat); the folder-name transforms moved tosrc/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) toTitle (rok).extusing the year from their ČSFD metadata, with a cancelable progress dialog; files with no known year are skipped and name collisions reported.File.titlekeeps the clean title (without the year);File.name_contextrecovers the year from a canonical filename when no ČSFD/tag year exists.
- 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
Changed
- Filmotéka hardlinks are named by the clean title (no year). In the tag
tree a movie now links as
Title.extinstead of the pooledTitle (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-categoryfilename_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'syearfield 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á (fullffmpegdecode to-f null, catches mid-file data corruption, slow) and Rychlá (ffprobecontainer/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_integritydrives the scan;check_video_integritychecks 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: Nevery 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_1suffix — which, having an unexpected name, was swept on the next run and recreated again, forever.HardlinkManager.find_obsolete_linksnow 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 thepool_dir/filmoteka_dirpointers. Everything describing a particular library —tag_schemaandcopyasis_folders— moved into the pool index (.Curator.!index, under a newsettingssection), so it travels with the pool.FileManagerreads/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_configetc.) — inherited from Tagger and unused by the Filmotéka workflow. - Per-file metadata sidecars (
.{filename}.!tag).Filemetadata is now stored only in the pool index; an index-lessFileis 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
addedtimestamp 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.pyto 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/--outputor the global config. Ships with a systemd oneshottipy-dne.service+ dailytipy-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 dneand- 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 (
attributesin the index, set via Filmy → Nastavit atribut… / context menu): arbitrarykey → valuepairs (e.g.collection_sort) that are exposed to Filmotéka filename templates throughFile.name_context. So a Kolekce template"{collection_sort} - {title}{ext}"orders the folder as01 - 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 (fieldstitle/year/rating/ext/stem/filename), e.g. a Kolekce with"{year} - {title}{ext}"yieldsDle kolekce/James Bond/1962 - Dr. No.mkvwhile every other folder (and the pool) keeps the plain filename.HardlinkManagertakescategory_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
transformnow 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_valuesno longer transforms;HardlinkManagertakes acategory_transformsmap (FileManager.filmoteka_category_transforms) andapply_transform/decade_bandrun 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_schemain 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_bandfor the rating), and how each maps into the Filmotéka tree (""= output root,"Dle …"= grouping folder, none = filter-only, no folders). Replaces the hard-codedFILMOTEKA_CATEGORY_ROOTSand the fixed category list inapply_csfd_tags;HardlinkManagerroots are now derived from the schema (FileManager.filmoteka_category_roots). - Tag provenance (ČSFD vs user): each file tracks which tags came from ČSFD
(
csfd_tagsin the index).apply_csfd_tagsnow 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ályfolders 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/FilmyasTitle.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_querystrips 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_moviegained anon_conflictpolicy (suffix/replace/skip);replaceevicts the existing same-named movie (file + index metadata) first. Filenow storestitleandcsfd_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ályis 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.!indexJSON keyed by pool-relative path.Filereads/writes the index when one is injected and otherwise keeps using per-file.!tagsidecars;FileManageruses the index for the pool. - Configurable copy-as-is folders:
copyasis_foldersin the global config (editable from the GUI) lists pool subfolders mirrored 1:1 during generation;Seriályis 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_tagsassigns Žá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_movierenames the physical file in pool/Filmy to<new name>.<ext>(extension preserved), moves its metadata to the new index key, and syncstitle/filename. Refuses empty names, names with path separators, and collisions with an existing pooled file. - App startup injects
truststoreso 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_tagreferencedsubprocesswithout 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.pynow detects the Anubis challenge, solves the SHA-256 proof-of-work the way the bundled worker JS does, and replays the request through arequests.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.ItemIsTristatewas renamed toQt.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 viaQTimer.singleShotso the tree is not rebuilt inside its ownitemChangedsignal (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.pyto rewrite the category in an existing pool index (backs up.Curator.!indexfirst); 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 rokufolder and country tags underDle země původu.HardlinkManagergained 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 underDle 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.
CSFDMoviegainedcountries: list[str](replacing the singlecountry); 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-pythonnarrowed to>=3.14,<3.15(PySide6 compatibility).
Removed
- Duplicate
src/core/constants.py(hard-coded staleVERSION = "v1.0.3"). The GUI window title imported from it, so it showed the wrong version. All imports now usesrc/constants.py(version derived frompyproject.toml), which also absorbedAPP_VIEWPORT; the window title usesAPP_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_TAGSis 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). Declaredpython-dotenv,pillow,loguru(already imported by the inherited code).