Release Notes¶
These release notes come from the same branch as this version of the Kometa Wiki.
Loading release notes…
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- Warn and ignore missing collection names in `collection.not` post-filters instead of rejecting the definition, allowing overlays to continue after an excluded collection is deleted.
- Preserve IMDb show mappings when TMDb cannot provide a TVDb ID, allowing metadata files to match those shows by IMDb ID. #3820
- Avoid warning about a missing optional `sort_by` when Letterboxd user-page builders use the default sort. #3814
- Reject items without a release or first-air date when applying numeric `tmdb_year` filters instead of aborting missing-item processing. #3815
- Apply IMDb keyword filters to missing movies and shows before reporting them or sending them to Radarr/Sonarr. #3816
### Added
- Added `mdblist_watchlist` builder to create collections based on watchlist
## [v2.5.4] - 2026-10-31
### Docs
- Replace the MkDocs wiki with Zensical, keeping Read the Docs hosting, branch versions, and existing documentation URLs.
- Add Release Notes to Kometa website, available under `Useful Links` section of navigation
### Removed
- Drop support for python 3.10 due to it going end of life. Python 3.11 or above is now required.
### Added
- Add `list_export` for exporting builder results to MDBList, FlickList, WeTrakr, TMDb, or Floppy lists with `sync` or `append` mode options. Legacy `sync_to_*_list` attributes removed from documentation
- Add `dovi_level` and `dovi_profile` filters for use with `plex_all` builders.
- Add `hdr10plus` and `atmos` boolean attributes for `plex_search` and `smart_filter` builders
- Add library schedule modes for full, added, changed, alphabetical, and explicitly selected items, plus a `--mode` / `KOMETA_MODE` override for a run.
- Add Seerr connector for handling media requests to Radarr and Sonarr. Supports adding to standard and 4K arr instances simultaneously.
- Add `apple_chart`, `itunes_chart` and `spotify_chart` builders for per-country song and album charts
- Add music support to `tautulli_popular` and `tautulli_top`; keep `tautulli_watched` as a legacy alias.
- Add `music_playlists` to Music library collection files for building Music Playlists.
- Add Last.fm connector and various music builders.
- Add YouTube Music builders for music libraries and operation for updating artist images
- Log the current value when an item or episode rating already matches the requested rating.
- Add `seasons: {latest: true}` filtering against the highest-numbered Plex season, excluding Specials.
- Add an artist-level Lidarr connector with global or per-library configuration, quality-profile and monitoring updates, tag add/remove/sync support, cached successful additions, and `lidarr_add_all_existing` / `lidarr_remove_by_tag` library operations. Plex artists are added only when their metadata contains one valid MusicBrainz artist ID.
- Add music-library `lidarr_all` and `lidarr_taglist` builders, which create Plex artist collections by matching Lidarr and Plex MusicBrainz artist IDs.
### Fixed
- Fix TMDb episode cache writes failing after an episode is renumbered or moved; replace stale positions atomically without retaining external IDs from a different episode. #3801
- Ignore the unsupported `kometa_poster` and `pmm_poster` attributes with a clear error directing users to supported poster options, instead of attempting legacy poster rendering. #3796
- Fetch complete MDBList digital release dates for `mdb_digital` date operations when batch or older cached results omit them, and cache confirmed missing dates to avoid repeated lookups. #3798
- Fix `lidarr_taglist` and `lidarr_remove_by_tag` ignoring tags that do not exist in Lidarr: a misspelled `lidarr_taglist` tag selected every Lidarr artist, and `lidarr_remove_by_tag: kometa, temp` removed every `kometa` artist when `temp` did not exist. Both now match nothing and log the missing tags.
- Allow FlickList and WeTrakr list builders (including list details and user lists), and `floppy_tracked`, in season and episode collections instead of rejecting them during collection validation.
- Resolve FlickList user list URLs (`/u/<user>/lists/<slug>`) to numeric API IDs for list builders and list descriptions.
- Invalidate stale MDBList cache entries that could apply incorrectly scaled Letterboxd ratings to Plex. #3771
- Exclude documentation-only `zensical.toml` from Docker source integrity checks to avoid reporting unmodified images as modified.
- Show the source integrity summary after collection and playlist summaries, before the end-of-run errors and warnings introduction.
- Cache TVDb movies and shows independently when they share a numeric ID, migrating existing entries without discarding metadata or resetting expiration. #3804
- Fix cached MDBList entries never returning the MyAnimeList rating: `query_mdb()` rebuilt that rating with the source label `myanimelist_rating`, which `MDbObj` does not recognize, so `mdb_myanimelist` in `mass_*_rating_update` operations, `<<mdb_myanimelist_rating>>` overlay text and `value_filter` only worked on a fresh MDBList fetch. #3773
- Fix the `anidb` overlay rating source (`<<anidb_rating>>`, e.g. `rating1: anidb` in the `ratings` Defaults file, and `anidb_rating` in `value_filter`) never returning a value: the AniDB permanent rating was matched against the misspelled variable name `anidb_rating_rating`, so every item logged `Overlay Warning: No 'anidb_rating' found` and no AniDB rating was rendered.
- Fix `year`, `unmatched`, and `trash` smart filters being removed from Show and Movie libraries
- Resolve MDBList season and episode entries through their parent show in mixed playlists, preventing unrelated shows from being added.
- Preserve smart-label collection filters during initialization so existing shows count toward `minimum_items`, preventing streaming collections from alternating between deletion and recreation. #3739
- Resolve `label` filters in Music libraries to the selected artist, album, or track label field.
- Accept a Kometa Utilities "Use Kometa App" MyAnimeList block that omits `client_secret`, and refresh that official app without sending a client secret.
- Accept `stinger_rating` filters and all numeric modifiers for collections and overlays, including negative integer thresholds. #3732
- Normalize typographic dash variants when matching music artist, album, and track names, so names such as `Sophie Ellis-Bextor` match Plex's `Sophie Ellis‐Bextor` when the result is unique.
- Validate People-Images poster URLs before selection so missing images allow an available TMDb poster to be selected; cache validation misses for the current run and distinguish temporary validation failures from confirmed missing images. #3715
- Add the existing `plex_bulk_edit_batch_size` operation to the configuration schema.
- Invalidate obsolete reverse ID lookups when provider mappings change, including equivalent numeric and text IDs, so cached reads agree with the corrected mapping during the same run. #3803
- Cache IMDb direct ratings locally. Library load batch-fetches that library's IMDb ids from Kometa Utilities `POST /ratings` and reuses them until `cache_expiration`; later direct rating lookups read the cache, and misses are batched instead of requested one title at a time.
- Cache IMDb genres locally. Library load batch-fetches that library's IMDb ids from Kometa Utilities `POST /genre` and reuses them until `cache_expiration`; later genre lookups read the cache, and misses are batched instead of requested one title at a time.
- Fix startup crashes when timing instrumentation is enabled by using the initialized logger for timing announcements. #3718
- Make up to three attempts for Plex image uploads returning HTTP 502, 503, or 504, and report retries with the item and image details instead of a traceback, including artwork resets and overlay uploads/removal.
- Map the German genre names Dokumentarfilm, Kriegsfilm, Liebesfilm, and TV-Film to the existing default genre posters. #3713
- Avoid quadratic item deduplication across builder entries and while compiling overlay item lists, reducing episode-overlay preparation work for large libraries while preserving item order, limits, and existing filters. #3722
## [v2.5.3] - 2026-10-12
### Added
- Add a source integrity diagnostic comparing distributed files against pristine upstream source for the exact running commit, with reusable cached baselines and modified/missing/added file reporting. Startup waits at most 30 seconds while retrieval has up to 180 seconds to finish silently; clearer status messages and the end-of-run summary describe the startup check, and Docker image metadata includes the full Kometa version and build number.
- Add grouped `asset_directory` mappings keyed by library type (`movies`, `shows`, `music`) and `collections` at global, library, collection-file, and collection levels to separate collection artwork and enabled downloads from item assets, including `assets_for_all_collections`.
- Add the `commonsense_rating` numeric Plex filter for `plex_search` and `smart_filter`, using Plex's Common Sense Media age-rating metadata.
### Fixed
- Validate Docker source commit and version metadata, embed validated commit identity, and populate image labels from the exact build checkout. Warn on missing or invalid metadata while the release workflow transitions, and document the arguments for custom image builds.
- Exclude blank warning/error messages from end-of-run summaries, log duplicate-collection counts, advice, and spacer lines at INFO level while retaining a warning per affected library, and group Letterboxd validation failures for missing resources while retaining full errors in detailed summaries (#3704).
- Group alternate TMDb movie, show, and collection lookup failure messages by type while retaining IDs and API details in the original log and detailed summaries.
- Correct the Letterboxd animation chart collection name to Top 250 Animation and remove the old Top 100 Animation collection (#3702).
- Group missing TMDb collection, movie, and show lookups by type in end-of-run summaries while retaining IDs and troubleshooting details in the original log messages.
- Include searched asset paths in missing artwork log messages while keeping end-of-run warning summaries grouped without paths; retain the existing missing-asset reporting settings.
## [v2.5.2] - 2026-10-07
### Changed
- Standardize collection-level Radarr and Sonarr definition setting names: `radarr_root_folder_path`, `radarr_minimum_availability`, `radarr_quality_profile`, `radarr_tags`, `sonarr_root_folder_path`, `sonarr_quality_profile`, `sonarr_language_profile`, `sonarr_series_type`, `sonarr_season_folder`, and `sonarr_tags` are now the preferred keys; the previous names remain supported as aliases.
- Upgrade SIMKL authentication to AUTH V2 with access-token validation, refresh-on-failure, and an optional `force_refresh` setting.
- Default `settings.threading.workers` to 4 (was effectively single-threaded) and `settings.threading.prefetch_collection_children` to `true`, deferring a collection's `sync_collection` "what to remove" lookup and PMS's comma-separated `/library/metadata/{ids}` batch-read to the shared thread pool instead of blocking the main collection loop; the cache is now RLock-guarded (`_LockedConnection`) so concurrent worker threads can share one SQLite connection safely.
- Consolidate end-of-run log-summary routing and repeated high-volume item warnings, including metadata entries skipped because their titles were not found.
### Added
- Add a `settings.threading` config block (`workers`, `tmdb_pages`, `parallel_sources`, `prefetch_collection_children`) backed by a shared `ThreadPoolExecutor`, used to defer non-Plex `gather_ids` work and Plex item-reload batching off the main collection loop.
- Add a `--delete-collections-labels` CLI flag (no short form - would collide with `-d`/`--divider`) that deletes all collections, same as `-dc`, and for any deleted collection whose title matches an existing Plex label of the same name (the Smart Label default), batch-removes that one label from just the items that have it - instead of `-dl`'s full library-wide, one-item-at-a-time label wipe.
- Add WeTrakr as a tracker connector (`wetrakr` config block, OAuth2 device-flow authorization via the Kometa Utilities website) with `wetrakr_list`, `wetrakr_list_details`, `wetrakr_user_lists`, `wetrakr_tracking`, `wetrakr_favorites`, and `wetrakr_ratings` builders, plus `sync_to_wetrakr_list`/`sync_missing_to_wetrakr_list` to sync a collection back to a WeTrakr list and `wetrakr_user` as a mass rating source; a list's description used as a collection summary (`wetrakr_description`, `wetrakr_list`/`wetrakr_list_details`) now carries a credit line linking back to WeTrakr, per their attribution terms. `wetrakr_user_lists` also accepts a bare username or profile URL now, resolved to a numeric user id via WeTrakr's search API.
### Fixed
- Avoid unnecessary Plex account and user lookups for owner-only playlists when user syncing and exclusions are disabled; playlist reporting still requires plex.tv access. #3687
- Log the Apprise delivery-failure warning again with Apprise 2, whose `notify()` returns an `AppriseResult` object instead of `False`, so failed and partially failed notifications were silently ignored.
- Accept YAML lists for schedules and scheduled visibility attributes across the config, collection, overlay, and playlist schemas, matching documented runtime OR semantics.
- Accept scalar values for the legacy `mass_content_rating_update` and `mass_original_title_update` operations in the config schema, matching runtime normalization and the other legacy mass metadata operations.
- Evaluate `non_existing` schedules against collection or playlist existence, preserving OR behavior in schedule lists while allowing `all[...]` to require absence alongside other conditions. #3675
- Allow `floppy_list` and `floppy_list_details` builders in episode-level collections, including exact episodes from public Floppy RSS feeds.
- Group `STARZ`, `Starz Encore`, and `STARZ Encore` network values into the canonical `Starz` Network Default collection.
- Honor per-collection `limit_<<key>>` template variables across Defaults files, including Letterboxd charts, Based On collections, and Streaming collections.
- Fix the `resolution` Defaults overlay file applying the `-Dovetail` variant to every DV HDR10+ item at 4K and 1080P, even without an edition, by matching the Dovetail weights to their plain counterparts. #3654
- Normalize Letterboxd ratings returned by MDBList batch lookups to their native 0–5 scale, preventing doubled ratings in overlays and metadata updates. #3533
- Keep localized collections from scheduled-out collection files classified as configured during `delete_collections` operations.
- Address the Docker image's managed virtual-environment interpreter directly at startup instead of depending on `PATH`-based interpreter selection. #3656
- Replace a bare truthy check on a collection builder's Plex object with an explicit `is not None` check, avoiding a silent full Plex `items()` fetch through `Collection`/`Playlist.__len__` on any falsy-looking-but-real collection.
- Guard against `None` `childCount` on blank/separator collections when computing the collection's starting item count.
- Only fetch a parent item's `titleSort` when building a display title if sorted output was actually requested, instead of unconditionally.
- Report Jikan/MyAnimeList search and lookup errors (including Jikan returning an error body for an unreachable MyAnimeList backend) as service errors with recovery guidance instead of a traceback, and fix an `mal_search` error that misidentified itself as an AniList error.
- Read FlickList ratings once per run instead of once per library item, so `mass_user_rating_update: flicklist_user` and `flicklist_ratings` no longer exhaust FlickList's 1,000 requests/hour limit on larger libraries.
- Validate that OMDb, IMDb service, and MDBList rating/search responses are the expected object shape before indexing into them, reporting a clean error instead of an unhandled traceback when one of those APIs answers with an unexpected body (e.g. an error payload shaped as a list); remove dead rating/image lookup code left over from the Trakt removal that referenced the no-longer-existing `config.Trakt`.
- Stop the retired-Trakt config cleanup from discarding unrelated attributes that merely mention "trakt" in a string value (e.g. an `mdblist_list` URL whose slug references Trakt-sourced data, or `summary` prose) or an entire collection whose name contains "trakt", by matching only the actual Trakt builder/attribute key names instead of a substring check against every key and string value.
- Report a `mass_*_rating_update`/`mass_*_episode_rating_update` value of the retired `trakt`, `trakt_user`, or `mdb_trakt` options with the Trakt-removal message instead of a confusing "must be a number between 0 and 10" error from the generic numeric-rating parser.
- Stop a `smart_label` collection whose label isn't currently applied to any items in Plex from overwriting the existing collection's smart filter: the unresolved label filter URL is `None`, and `update_smart_collection()` translated that into the bogus `.../allNone` URI, logging `Metadata: Smart Collection updated to None` and wiping the collection's real filter. An unresolved filter URL is now skipped, leaving the collection intact so the existing `Label: ... was not added to any items in the Library` error can report the problem.
## [v2.5.1] - 2026-09-24
### Added
- Trace Plex queries before they start and report completion or failure with elapsed time, making pending requests visible when diagnosing apparent hangs.
### Fixed
- Support MDBList official movie and show list URLs, including the Most Watched, Popular, Trending, and Anticipated lists.
- Use Prime Video watch provider ID `9` for the streaming default when no region is specified, honoring the default US region.
- Report artwork upload timeouts as Plex server errors without a traceback, while preserving retries and continuing with remaining artwork.
- Handle missing TMDb season details during show metadata loading with a descriptive error instead of a traceback and a library-aborting critical error.
- Report MDBList daily quota and rate limits with recovery guidance instead of nested errors and playlist tracebacks.
- Report invalid webhook URLs and notification request failures without tracebacks, continuing with other webhooks instead of interrupting collection processing.
- Report empty or unreadable IMDb chart pages as service errors with chart context and recovery guidance instead of a traceback.
- Report TMDb IDs that no longer exist as a concise error without a traceback, with a clearer message.
- Log a concise error instead of a traceback when a collection or playlist cannot be loaded from Plex after being built.
- Stop the post-Trakt-removal config cleanup from discarding unrelated `null`/empty values throughout config, default, and overlay files, which caused conditionals like the `languages` overlay default's `back_radius` to fail validation with `each condition must have a result value`.
### Changed
- Log "No Item Edits" when episode rating operations queue no changes, matching the existing item operations message.
### Fixed
- Report unrecognized downloaded overlay poster images with the file path and recovery guidance instead of a traceback, and continue processing other items.
## [v2.5.0] - 2026-09-21
### Changed
- Promote `develop` as the publicly documented beta branch; it now automatically mirrors `nightly` on every push instead of requiring a manual sync. `nightly` remains the active development branch but is no longer referenced in user-facing documentation.
- Trakt functionality has been removed from Kometa following recent changes to their support of third-party apps and updates to their API and Developer guidances. Please see the FAQ page or Announcements channel in the Discord Server for further information
- Send explicitly configured `watched` filters to Tracearr's Public API v2 history endpoint, reducing history records transferred before Kometa applies its remaining filters.
- Fetch MDBList data in cache-aware batches of up to 100 items, substantially reducing API quota usage for library operations, direct rating overlays, and overlay value filters.
- Consolidate item-specific IDs, titles, GUIDs, and URLs in end-of-run warning and error tables, derive missing-rating groups from active overlay sources, and move missing overlay template values into the Overlay Summary.
### Added
- Add the `mdblist_streaming` builder for MDBList's JustWatch streaming charts, with country, period, provider, and genre options.
- Add the `tracearr_watched_media` builder for retrieving Tracearr's distinct watched or partially watched movie, show, and episode sets using provider-ID matching and the compact Public API v2 watched-media endpoint.
- Add the `text` builder for defining ordered IDs inline as a YAML scalar, literal multiline string, or list using the same identifier syntax as `text_file`.
- Add `url_theme` and `file_theme` metadata attributes for uploading theme music to individual movies and shows.
- Support the `folder_location` Plex search option in music-library track builders.
- Add a FlickList connector (`flicklist` config attribute) with read-only `flicklist_list`, `flicklist_list_details`, `flicklist_user_lists`, `flicklist_watchlist`, `flicklist_favorites`, `flicklist_watched`, `flicklist_ratings`, `flicklist_up_next`, and `flicklist_tracked` builders, plus a `flicklist_description` summary source.
- Add `sync_to_flicklist_list` and `sync_missing_to_flicklist_list` to sync a collection's contents to a FlickList list, mirroring `sync_to_trakt_list`/`sync_missing_to_trakt_list`.
- Add `flicklist_user` as a mass rating source, using the FlickList user's personal ratings.
- Support MDBList episode lists in episode-level collections.
### Fixed
- Show trace messages in terminal and Docker console output when `--trace` is enabled without `--debug`.
- Retry incomplete or malformed TMDb language configuration during startup instead of reporting a valid language such as `en` as invalid, and log rejected responses at trace level.
- Fix playlists never being reordered after creation, where every move failed with a `404` on `/playlists/<id>/items/None/move` because updating the metadata of the items cleared the Plex playlist item IDs needed to reorder them. #2265
- Refresh a playlist after successful moves so `sync_to_users` copies use its current post-move order instead of the cached pre-sort order.
- Report "Trakt Connection Successful (Public Mode)" instead of a plain "Successful" when a configured Trakt authorization fails to refresh, so the run log doesn't contradict the authentication error logged just above it.
- Treat empty Plex collections and playlists as existing objects so stale smart filters can be repaired and repeated runs do not create duplicate collections; also clarify that empty or stale smart collections can cause duplicate-title warnings. #3572
- Stop crashing with `ValueError: substring not found` when repairing a stale smart collection whose stored filter is empty or missing a query string.
- Skip automatically discovered stale TMDb IDs during franchise discovery and TMDb-based Plex filtering without generating failure notifications. #3561
- Reject malformed TVDb external IDs returned by TMDb so IMDb Defaults skip unmappable shows instead of later reporting a TVDb URL error. #3548
- Honor disabled debug mode in terminal and Docker console output while retaining diagnostic debug records in log files. #3568
- Continue metadata and overlay processing when a Plex request times out while removing an existing `Overlay` label during image updates; log the failed image update instead of aborting the library run.
- Skip nonnumeric, non-finite, negative, and above-range provider ratings instead of sending them to Plex or rendering them in rating overlays, and leave invalid provider and overlay values uncached so they remain visible on later runs.
- Fix CLI arguments (e.g. `--config`) being silently reset to their defaults on Python 3.14, where the `ProcessPoolExecutor` running the actual work now defaults to the `forkserver` multiprocessing start method on Linux instead of `fork`.
- Fix the `resolution` Defaults overlay file selecting unpaired `-Dovetail` variants when a resolution or edition counterpart is disabled or unmatched, while continuing to use paired Dovetail overlays when both match; also resolve overlay suppression without list-mutation skips or allowing lower-weight group losers to suppress other overlays.
- Prevent Plex poster operations and later same-run processing from requesting invalid metadata by reducing show reload payloads and refreshing items plus rating-key mappings after `split_duplicates` replaces metadata records. #3519
- Silently ignore invalid TMDb aggregate cast/crew role entries and isolate remaining TMDb object parsing failures to the affected item so mass rating operations and subsequent overlays continue; the invalid entries come from TMDb and require no user action. #3518
- Honor HTTP 429 `Retry-After` response headers across shared HTTP integrations, including Trakt rating updates, instead of immediately continuing with subsequent requests. #3525
- Prevent `delete_collections` from treating configured collections with localized or custom-resolved titles as unconfigured when operations run before collections.
- Fixed `audio_codec` track builder
- Include collections removed by the `delete_collections` library operation, the `--delete-collections` CLI option, dynamic collection sync, and `delete_collections_named` in the run-end notification total.
- Fix `audio_language`/`subtitle_language` `plex_search` filters matching zero items whenever a library has multiple Plex-reported locale variants for the requested language (e.g. `de` + `de-DE`); the variants were being `AND` together into an impossible filter instead of `OR`. Regression from #3440.
- Accept Plex-reported language names and fall back from three-letter language codes in `audio_language`/`subtitle_language` searches, show available languages for invalid values, and display expanded language variants once in filter summaries. #3570
- Fix `episode_*` ratings from producing a critical error when Trakt did not have an episode in its database
- Fix DC-based lists in 'universe' Defaults file
- Fix ghost progress logging (e.g. `Parsing ID x/y`) writing carriage-return-only output regardless of whether stdout is a terminal, which collapses an entire run into a single unbounded log line for containerized/piped deployments; ghost output is now skipped when stdout is not a TTY. Fixes #3542.
- Fix `plex_collectionless` never recognizing an item as belonging to a smart collection (used by most Defaults collection files, e.g. `genre`, `studio`, `audio_language`), since Plex does not tag items with smart collection membership; membership is now resolved from each retained collection's actual items instead of the item's own `collections` tag list. Fixes #3537.
- Fix `prefetch_mdblist` raising when an orphaned Season/Episode's parent Show has been deleted from Plex (`item.show()` returning `None`); such items are now skipped instead of crashing the MDBList prefetch pass.
## [v2.4.9] - 2026-09-21
- No changes
## [v2.4.8] - 2026-08-15
### Fixed
- Pushed 2.4.8 to resolve an issue in GitHub workflows that prevented docker builds from being deployed for nightly updates.
### Added
- Add `plex_id` and `plex_rating_key` builders for selecting Plex items directly by metadata ID, GUID, or rating key.
- Add `audio_codec` as a Plex search and filter attribute for music tracks.
### Changed
- Allow pipe-separated hourly, monthly, and yearly schedule values. Monthly schedules also support inclusive ranges, such as `monthly(1-7)` and `monthly(25-last)`.
## [v2.4.7] - 2026-08-12
### Added
- Added a fallback Trakt Client ID for "public" mode if the current Trakt credentials are invalid
- Add a Floppy connector with `floppy_list`, `floppy_list_details` and `floppy_tracked` builders, optional API-token authentication for private lists, and `sync_tags` support for applying Floppy list tags as Plex item labels.
- Add Floppy as a movie, show, and episode mass-rating source for Plex audience, critic, or user rating fields.
- Add `floppy` as a direct Defaults ratings-overlay source for movies, shows, and episodes.
- Add Tracearr connector support with history-based collection and playlist builders for popular, watched, trending, rewatched, completed, binged, most transcoded, watch time, in-progress, and full history views, including user-specific and playback-quality filters, direct movie provider-ID matching, v2 capability detection with v1 fallback, and in-run history reuse.
- Add Serializd as a connector
- Add `serializd` as a genre source for shows and episodes for the mass metadata update operation, includes nanogenres.
- Add `serializd` as a source for show and episode audience, critic, and user mass metadata updates, and `serializd_user` for episodes only.
- Add `sync_watchlist_to_serializd` show-library operation to mark the Plex server owner's watched episodes as watched in Serializd.
- Add builders: `serializd_list`, `serializd_watchlist`, `serializd_trending`, `serializd_popular`, and `serializd_featured`
- Add `serializd` as a direct rating source for show and episode-level Ratings Defaults overlays.
- Add `sync_to_mdb_list` for synchronizing collections with MDBList static lists.
### Fixed
- Sort IMDb award years and multi-edition year values before resolving `starting`/`ending` ranges, so out-of-order repository entries do not cause `latest` ranges to include the wrong years.
- Refresh an item's in-run Plex state after `mass_poster_update` removes its `Overlay` label, ensuring the following overlay pass detects the reset poster and reapplies its overlays instead of incorrectly skipping it.
- Redact registered secrets from critical-error webhook messages before sending them to Discord, Slack, Notifiarr, or other webhook destinations. #3472
- Make nightly Docker builds wait for, and use, the matching base image after dependency changes.
- Accept all successful Trakt API responses so `sync_to_trakt_list` handles the `201 Created` returned when adding list items instead of marking the collection build as failed. #3453
- Add `pyinstrument` to `requirements.txt` (it was only in `dev-requirements.txt`), so `KOMETA_PROFILE=pyinstrument` actually works in a normal/Docker install instead of silently no-opping.
- Fix `folder_location`/`folder_location.not` smart_filter matching for show libraries, which previously failed with `plex_search attribute: folder_location not supported` (or, for `builder_level: episode`, `Unknown filter field "location"`) because the filter was being resolved against Plex's `show` libtype, which doesn't expose a folder filter; it's now resolved against the `episode` libtype instead, matching how Plex exposes it for shows. #3483
- Lock the poster, background, logo, and square art fields after resetting them from a source, and check the logo's actual Plex field name (`clearLogo`, not `logo`), so subsequent runs with `ignore_locked: true` skip re-resetting instead of looping forever. #3487
### Changed
- Document Trakt's free connected-app limitation and identify which Trakt builders and account features require OAuth/VIP access versus public API access.
- Migrate Trakt authentication documentation and shipped configuration examples from the legacy PIN/OAuth callback flow to Device Code Flow, including headless-server guidance and the optional `webhooks.trakt_pin` notification.
- Batch Plex writes that were previously one API call per item: label/genre sync, `item_critic`/`audience`/`user_rating` updates, and title-parentheses removal now merge into shared `saveMultiEdits()`/`batchMultiEdits()` calls (respecting `plex_bulk_edit_batch_size`) instead of one `edit_tags()`/`editField()`/`editTitle()` call per item; label/genre and rating writes for the same item are further merged into a single PUT instead of one per attribute type, grouped by library and media type so mixed-library/mixed-type playlists route through the correct Plex connection. The overlay `Overlay` label add is batched the same way, flushing every `plex_bulk_edit_batch_size` items instead of only once at the very end of the run.
- Reduce repeated in-run Plex/API lookups via caching: `modules/cache.py`'s ID-map queries (`query_tmdb_to_tvdb_map` and siblings, `query_guid_map`/`update_guid_map`) are memoized in-process; `check_filter` memoizes plain item-attribute reads (excluding writable fields), media-derived number filters (channels/height/width/aspect/versions/audio_language/subtitle_language/duration), and per-item seasons/episodes/albums/tracks sub-item listings, the last of which is now reused via `cached_item_subitems` at every remaining call site (including `overlays.py`'s runtime/total_runtime lookups) instead of re-querying Plex each time.
- Reduce redundant network/image work: `Requests.get_image()` is memoized by URL for the run; validate-only image URL checks use HEAD instead of GET; validate-only checks are skipped entirely for Kometa's own GitHub-hosted assets; and fresh local overlay images are reused instead of being re-fetched from GitHub on every call.
- `modules/request.py`: read-only file/URL YAML loads (that are never saved back) now use ruamel's safe loader, skipping comment/formatting-preservation bookkeeping that was pure overhead for these loads.
- Dedupe forced Plex reloads for tag filters checked back-to-back in `check_filters`; snapshot the overlay backup folder once per run instead of calling `os.path.exists()` per item; buffer log file writes instead of flushing after every line (`WARNING` and above still flush immediately, and handler removal/close force a final flush so the file is always complete); early-exit `check_for_var`'s variable-resolution loop once a pass makes no substitutions, and skip its redundant second pass when no arithmetic `<<var+N>>` syntax is present.
- Improve `audio_language`/`subtitle_language` `plex_search` matching (including the default `languages.yml` flag overlays) to recognize every locale- or region-tagged variant of a language that actually exists in the library (e.g. `es-419`, `en-US`, `spa`) instead of only an exact 2-letter match. A single `es`/`it`/`zh` filter now matches every variant Plex reports for that language, resolved with one lightweight, cached lookup of the library's language filter choices rather than a separate Plex query per variant.
## [v2.4.6] - 2026-07-30
### Added
- Add `folder_location` and `folder_location.not` Plex search attributes for movie, show, and track-level music smart collections and Plex searches, using the root folders configured on the Plex library.
- Add `--timings`/`--timing` (`KOMETA_TIMINGS`/`KOMETA_TIMING`) runtime support for diagnosing slow runs. When enabled, network calls, cache lookups, and major per-collection/per-library phases are timed; a summary prints to the log at the end of the run and a full per-source/per-library breakdown is exported as JSON/CSV to the logs directory. Disabled by default, with negligible overhead when off. Also adds an optional `KOMETA_PROFILE=pyinstrument` environment variable for deeper single-run profiling, writing an HTML report to the logs directory.
- Add TVDB ID support to YamTrack builder, enabling import of TVDB show IDs from YamTrack lists and tracked pages alongside existing TMDb support.
### Fixed
- Send IMDb's web client identifier with GraphQL requests so `imdb_search` and IMDb-backed defaults are not rejected with HTTP 403; report HTTP and non-JSON GraphQL responses as contextual IMDb errors instead of uncaught JSON decoding tracebacks. #3444
- Report IMDb chart GraphQL and HTML fallback failures as contextual IMDb errors instead of raising `IndexError` when the fallback page does not contain `__NEXT_DATA__` chart data. #3446
- Batch collection and playlist `item_label`, `item_label.remove`, `item_label.sync`, and `non_item_remove_label` updates through Plex multi-edit requests, grouped by library and media type and split by `plex_bulk_edit_batch_size`, instead of sending one label request per item.
- Fail Letterboxd collection validation when a configured list or watchlist cannot be loaded, preventing sync mode from emptying an existing collection after a blocked or unavailable source request.
- Treat TMDb connection failures, timeouts, HTTP 408/429/5xx, and backend-unavailable responses as transient service failures: retry discovery, pagination, item requests, and lazy object hydration consistently; emit concise retry warnings instead of tracebacks; and surface exhausted retries as a service-unavailable error.
- Add the affected URL and HTTP status to exhausted empty TVDb response warnings; limit empty documents to three attempts with short exponential delays while retaining six attempts and the existing delay for other transient failures; and open a run-scoped circuit after two distinct URLs exhaust retries so a systemic TVDb outage does not generate thousands of requests and hours of retry delays.
- Stop ntfy from sending an access-test notification whenever Kometa initializes the ntfy client.
- Restore per-item label removal when deleting smart-label collections, preventing an `AttributeError` from the removed `batch_edit_tags` API.
- Dispatch the configured `delete` webhook after every successful Kometa-managed collection or playlist deletion instead of only for `delete_not_scheduled` and `delete_playlist`; suppress the event for playlist sync's internal delete-and-recopy step. #3420
- Add a per-library mass image-operation summary grouped by operation, source, image type, media level, and result while retaining per-item success messages, and collapse repeated `No Reset Image Found` warnings by image type in the end-of-run summary.
- `modules/plex.py`: standardize Plex API retry handling so known 400/404/401 responses and Kometa `Failed` exceptions remain terminal, while exhausted transient retries re-raise their underlying exception instead of becoming opaque `RetryError`; collection move failures also include the item title and original Plex response, and `query_collection`/`get_actor_id` convert terminal Plex responses into contextual Kometa errors.
- Added Drop-in replacement for the jikan api as it is being deprecated
- Propagate the exit code from the worker process so `--validate` (and other runs) return a non-zero status on failure. Previously `start()` ran inside a `ProcessPoolExecutor` whose result was never retrieved, so a `sys.exit(1)` from a failed validation was swallowed and the parent always exited `0`, breaking CI usage.
- Fix the startup requirements version check so each package is compared against its own pinned requirement. Previously `v1`/`v2` leaked between loop iterations and the package name lookup was case-sensitive, causing bogus messages such as `GitPython version: 3.1.55 does not match expected: 1.4.14`, where the expected value came from an unrelated package.
- Resolve movie folder paths using the path style reported by the media server rather than the platform Kometa runs on. A Linux or Docker install pointed at a Plex server on Windows received paths such as `P:\Movies\Title\file.mkv`, which `os.path.dirname` reduced to an empty string, logging `Plex Error: No location found` for every movie and silently disabling `add_existing`, `upgrade_existing`, `monitor_existing`, `item_radarr_tag`, and `item_sonarr_tag`.
- Fix `streaming` Defaults to more consistently use regional variants of Amazon, and stop trying to use `Originals` lists that do not exist
## [v2.4.5] - 2026-07-22
### Added
- Add semantic `letterboxd_crew`, `letterboxd_studio`, `letterboxd_country`, `letterboxd_language`, `letterboxd_genre`, `letterboxd_theme`, `letterboxd_similar`, and `letterboxd_collection` builders for Letterboxd discovery pages.
- Add `value_filter` overlay-file attribute to filter items at selection time based on a runtime-fetched numeric value; supports comparators `gte`, `gt`, `lt`, `lte` on any `rating_sources` variable using the normalised 0–10 scale.
- Add `overlay_value_cache` table (replaces `overlay_special_text2`) with a `UNIQUE(rating_key, type)` constraint and an `expiration_date` column; values refresh automatically after `cache_expiration` days.
- Add `_overlay_state` and `_overlay_images` per-library tables replacing the dual-use `overlay TEXT` column in `_overlays`; one row per overlay per item, written only on successful resolution.
- Add one-time migration that moves rows from `overlay_special_text2` into `overlay_value_cache` (deduped), resets overlay application state for a full reprocess, and logs a visible "Overlay Cache Upgraded" separator.
- Rework `defaults/overlays/ratings.yml` to fetch ratings directly during the overlay run; Mass Rating Update operations are no longer required for fetched sources. Adds auto image-pick, Fresh/Rotten filtering via `value_filter`, a `Direct` image level for tomatoes/tomatoesaudience, and new image assets under `defaults/overlays/images/rating/`.
- Accept HTTP(S) URLs anywhere a `text_file` builder used to require a local file path. Kometa first tries to parse the response as JSON (matching today's behaviour) and falls back to a plain-text line-by-line parse on parse failure. Gzip-compressed responses are auto-decompressed. Mixed local/URL lists in a single `text_file` builder are supported.
- Add `logo` and `square`/`square_art` asset detection and setting via `url_` and `file_` methods for Collection builders, as well as Defaults via `url_*_<<key>>` template variables.
- Add Plex-compatible local asset names for posters, backgrounds, logos, and square art, including `.tbn` files and numbered variants such as `poster-2.png`; exact filenames such as `poster.jpg` take priority over numbered variants.
- Add `trakt` as a source for posters, backgrounds, and logos. Square art remains unsupported.
- Add `tvdb` as a source for posters, backgrounds, logos and square art.
- Add `mass_metadata_update`, a grouped Library Operation for genre, content rating, title, studio, date, rating, artwork, mapper, and backup updates. Existing mass metadata operations move under their matching sub-attributes..
- Add YamTrack connector support with `yamtrack_list` and `yamtrack_list_details` builders.
- Add `yamtrack_tracked` builder to collect YamTrack profile movie, TV, and anime items by tracked status.
- Add Plex show edition support wherever movie editions are supported, including edition filters, metadata matching/editing, overlays, dynamic collections, and `item_edition`.
### Fixed
- Allow all semantic Letterboxd discovery builders to use `collection_order: custom`, preserving Letterboxd's returned order in Plex collections.
- Resolve TMDb episode ratings by Plex's episode-level `tmdb://` GUID before falling back to season and episode numbers, allowing alternate Plex episode orderings such as split-cour anime to map correctly; persist the direct episode ID in the existing TMDb episode cache for zero-request warm lookups.
- `modules/request.py`/`modules/plex.py`: fix `get_image()` and `delete_user_playlist()` crashing a collection or playlist sync outright on a transient network failure (connection reset, timeout, plex.tv DNS resolution). Both now catch the underlying `requests` exception and raise `Failed`, matching how every other network-facing call in these modules already degrades.
- `modules/tvdb.py`: add a distinct `Unavailable` exception for TVDb requests that exhaust their retry budget without ever returning usable content (e.g. repeated HTTP 202/empty-body "still generating" responses), separate from `NotFound`'s definitive 4xx. Previously both were re-raised with the identical "No Series/Movie found" message, so a confirmed-dead TVDb ID and a transient TVDb hiccup were indistinguishable in the log. All `get_tvdb_obj()` call sites (`modules/builder.py`, `modules/operations.py`) now log `NotFound` at debug (unchanged), `Unavailable` at warning (previously logged at error via the generic `Failed` handler), and any other `Failed` at error (unchanged).
- Fix custom `rating<n>_file` (and `git`/`repo`/`url`) overlay images being silently replaced by a built-in `default`/`pmm` asset.
- Prevent Kometa from creating duplicate collections when Plex search misses an existing same-named collection by falling back to the full collection inventory before creating.
- Refine the run summary output: group repeated overlay, Letterboxd/TMDb, missing TVDb season/episode and IMDb episode, Plex resolution-regex, Trakt TVDb-miss, and missing local artwork/theme path messages; update the supported-artwork summary wording; normalize asset-directory and logo warning summaries; print overlay and convert summaries in the same `Count | Message` format as warning/error summaries; add a visual divider before the summary intro text; rename the overlay section to `Overlay Summary`; and keep the run-status table hidden when there is no status data to show.
- Group missing TMDb collection errors into a single end-of-run summary row while preserving each collection ID in the main log.
- Fix overlay cache poisoning where application state was written for unresolved overlays (e.g. no IMDb rating), causing the item to be permanently skipped even after a rating became available.
- Fix duplicate rows accumulating in `overlay_special_text2` on every run due to a missing `UNIQUE(rating_key, type)` constraint.
- Ignore episode logo and square-art files during asset-directory discovery because Plex episodes only support poster and background artwork.
- `modules/textfile.py`: fix `tmdb:` entries in playlist/mixed mode (where `is_movie=None`) being silently dropped. The old `is_movie is not False` check treated `None` as `True`, tagging TV show TMDb IDs as `"tmdb"` type which only checks `movie_map`. Now returns both `(id, "tmdb")` and `(id, "tmdb_show")` so the builder resolves against both movie and show maps. Also updates `mdblist` and `simkl` three-state contract comments and docs.
- `modules/poster.py`: fix 18 Optional-member-access errors flagged by pyright. The `ImageData.__init__` triple-nested-ternary is unwound into an explicit `if`/`elif`/`else` (also skips a redundant `os.stat()` call when `compare=` is provided explicitly). `apply_vars` and `adjust_text_width` early-return on `self.text is None` (matches the callers' existing `if component.text:` guard). `get_generated_layer` raises `Failed` with a descriptive message instead of crashing with `'NoneType' object is not subscriptable` when `Image.load()` or text-dimension computation returns unexpectedly. Baseline drops 1223 → 1205.
- `modules/letterboxd.py`: fix 9 Optional-call/iterable errors flagged by pyright by removing dead defensive code. `letterboxdpy` is a pinned hard dependency in `requirements.txt`, so the speculative `try/except ImportError` (and the runtime `_require_library()` guard it required) was never reachable in any supported install. Imports are now unconditional, matching every other integration module (`plex.py`, `tmdb.py`, etc.). The two `util.get_list(...)` for-loops in `validate_letterboxd_lists` / `validate_letterboxd_user_pages` now pass `return_none=False` and fall back to `[]`, so they never try to iterate `None`. Net diff: -19 lines, no behavioural change for any real install. Baseline drops 1205 → 1196 (and `modules/letterboxd.py` falls out of the per-file table entirely).
- `modules/anidb.py`: switch `from lxml import etree` to `import lxml.etree as etree` so pyright can resolve the symbol. `lxml.etree` is a C extension (no Python wrapper module), and the `from X import Y` form trips pyright's `reportAttributeAccessIssue` while the equivalent `import X.Y as Y` form does not. Identical runtime behaviour. Baseline drops 1196 → 1195.
- `modules/convert.py`: fix 7 Optional-cluster errors flagged by pyright and harden two real latent bugs in the process. Extracted the thrice-repeated `re.search("-(.*)", check_id).group(1)` pattern into a `_hama_suffix()` static helper that raises `MappingConvertError("Malformed Hama ID ...")` if the regex doesn't match, instead of letting an `AttributeError` escape from `.group(1)` on `None`. Restructured the `int(self.tmdb_to_tvdb(tmdb_id, fail=True))` call to extract the result first and check for `None` (the surrounding `except Failed: pass` only catches the documented failure mode; `int(None)` would have been an `AttributeError` slipping past it). The two `util.get_list(...)` for-loops now pass `return_none=False` and fall back to `[]` so they never try to iterate `None` (matches the fix already used in `modules/letterboxd.py`). Added 2 new tests covering the helper's happy path + malformed-input behaviour. Baseline drops 1140 → 1067.
- `modules/imdb.py`: fix 7 pyright errors spanning four patterns and harden two latent bugs. (1) `validate_imdb` iterates `util.get_list(...)` with the now-familiar `return_none=False or []` belt-and-suspenders. (2) `main_data = imdb_dict[X].strip()` becomes `str(imdb_dict[X]).strip()` so a non-string YAML value (e.g. a bare numeric id) doesn't crash with `AttributeError`. (3) In `_graphql_json.check_constraint`, the `if range_data:` block reads `range_name[i]` — but `range_name` defaults to `None`, and pyright noticed the invariant 'range_data only populates when range_name is not None' isn't visible across loop iterations. Made the invariant explicit: `if range_data and range_name is not None:`. (4) `re.search(r"(\d+) of (\d+).*", relevant).group(...)` in `_imdb_keywords` now guards on `if result is not None`, falling back to `(0, 0)` (matches the `else` branch already used for inputs without `"of"`). Also added `@overload` signatures to `IMDb._interface(interface)` so `"ratings"`/`"basics"` return `dict` and `"episode"` returns `list[list[str]]`, fixing the `self.ratings[imdb_id]` type-mismatch at the cached-property layer. Baseline drops 1188 → 1181 (and `modules/imdb.py` falls out of the per-file table entirely).
- Bundle: fix 9 pyright errors across four small files — all the same `util.get_list(...)` iterable + `.strip()`-on-possibly-non-str pattern that's been fixed in `letterboxd.py`, `convert.py`, `imdb.py`, `anilist.py`. Touched files: `modules/mdblist.py` (2 errors — `validate_mdblist_lists`), `modules/icheckmovies.py` (3 errors — `validate_icheckmovies_lists`), `modules/textfile.py` (3 errors — `validate_file` + `get_ids`; the `or []` belt-and-suspenders was the missing piece since `return_none=False` alone isn't visible to pyright through the untyped helper), `modules/config.py` (1 of 2 errors — `notify`; the other 'Code too complex to analyze' error needs a refactor). All three small files fall out of the per-file error table. Baseline drops 1181 → 1172.
- `modules/validator.py`: switch `except ryaml.error.YAMLError` to `except ryaml.YAMLError` at both call sites (lines 144 and 291). Same C-extension import-form trap as `modules/anidb.py` and `modules/request.py`: pyright resolves the top-level `ruamel.yaml.YAMLError` alias but not the `.error.YAMLError` chain. Identical runtime behaviour — `ruamel.yaml` re-exports `YAMLError` from `ruamel.yaml.error`. Baseline drops 1181 → 1179 (and `modules/validator.py` falls out of the per-file table entirely).
- `modules/anilist.py`: fix 7 pyright errors across three patterns and harden one latent bug. (1) `int(util.validate_date(value, return_as="%Y%m%d"))` becomes `int(str(...))` to satisfy pyright's `int(str | datetime)` complaint — `validate_date` returns `str` when `return_as` is provided but the helper is not typed to reflect that. (2) The `mod == "gte"`/`"lte"` blocks did `value -= 1` / `value += 1` where pyright saw `value` as `int | LiteralString | Unknown`. The invariant '`gte`/`lte` modifiers only apply to numeric attributes (`start`/`end`/`episodes`/`duration`/`score`/`popularity`)' is true at runtime but not visible across the dispatch table. Cast with `int(value)` to make the contract explicit. (3) `validate(name, data)` iterated `util.get_list(data)` (possibly None) and called `d.lower()` on items pyright saw as `dict | int | str`. Added `return_none=False or []` + wrapped `d` in `str(...)`. Added 1 test (numeric input doesn't `AttributeError`). Baseline drops 1181 → 1174.
- `modules/request.py`: fix 7 pyright errors across three patterns and harden three latent bugs. (1) `get_stream` did `dl / total_length * 100` inside the per-chunk log line, but `total_length` is `None` when the server doesn't send a `Content-Length` header — division would `TypeError`. Added an `if total_length:` branch that falls back to a bytes-only progress line. (2) `get_header` had an implicit `return None` path that the `get_cloudscrape_html` caller would crash on with `cloud_headers.pop("User-Agent")`. Made the fallback `return {}` so callers always get a dict (semantically identical to `None` for `requests.get(headers=...)`). (3) The `YAML` class let `self.path=None` silently flow into `os.path.exists()` / `open()` when the caller forgot to pass either `path` or `input_data`. Added an explicit `raise Failed("Either path or input_data must be provided")` guard at the start of the no-input-data branch. Also rewrote the `except ruamel.yaml.error.YAMLError` clause to use `ruamel.yaml.YAMLError` (which pyright can resolve) and replaced `e.problem` direct access with `getattr(e, "problem", None)` since `problem` only exists on `MarkedYAMLError` (a bare `YAMLError` would have crashed the error handler with `AttributeError`). Added 4 tests (`get_header` matrix + `YAML()` failure path). Baseline drops 1181 → 1174 (and `modules/request.py` falls out of the per-file table entirely).
- `modules/logs.py`: fix 5 pyright errors across two clusters. (1) `remove_main_handler` and `remove_playlists_handler` called `self._logger.removeHandler(self.main_handler)` directly, but `main_handler` / `playlists_handler` are initialised to `None` in `__init__` and only assigned in the corresponding `add_*` method. Calling `remove_*` before `add_*` would crash with a type error from `logging.removeHandler(None)`. Added explicit `if self.main_handler is not None:` guards. (2) `MyLogger.findCaller` used `while hasattr(f, "f_code"):` to walk the frame chain — semantically correct (every non-`None` frame has `f_code`) but pyright doesn't narrow `Optional[FrameType]` through `hasattr`. Switched to the equivalent `while f is not None:` which is both clearer and pyright-friendly. Baseline drops 1156 → 1151 (and `modules/logs.py` falls out of the per-file table entirely).
- Singles bundle: fix 4 pyright errors across three small files (one error each in `modules/ntfy.py` and `modules/tvdb.py`, two in `modules/ergast.py`). (a) `modules/ntfy.py`: change `title: str = None` to `title: str | None = None` — classic PEP 484 Optional-default annotation. (b) `modules/tvdb.py`: wrap `released` in `str(...)` before `datetime.strptime` — the local `parse_page` helper returns `list | str | None` and the existing `if released else released` ternary handles None, but pyright can't see the str-only branch. (c) `modules/ergast.py`: `Race.session_info` subtracted `timedelta` from `self.date` unconditionally, but `self.date` is set to `None` when the API returns a malformed/missing date string (existing `except (ValueError, TypeError): self.date = None`). Added an explicit `if self.date is None: video_date = None` early branch — real latent bug that would crash with `TypeError` rather than gracefully degrade. Added 1 regression test in `tests/test_ergast.py`. Three files drop off the per-file error table. Baseline drops 1151 → 1147.
- `modules/library.py`: fix 7 pyright errors across three patterns. (1) Added missing `@abstractmethod def get_ids(self, item) -> tuple: ...` to the `Library(ABC)` base — the method is concrete on `Plex` but wasn't declared abstract, so pyright saw `self.get_ids(item)` as an attribute access on `Library*` (the unbound subclass type) and flagged it. (2) Added `-> list` return annotations to the existing `@abstractmethod` declarations for `get_all` and `item_labels` and replaced their `pass` bodies with `...` — the implicit `None` return type was being propagated to call-sites like `for la in self.item_labels(item)` and `for item in items` which then crashed `reportOptionalIterable`. (3) Matched the existing `poster.compare if poster else ""` defensive ternary for the parallel `background`, `logo`, and `square_art` cache-update calls in `image_update` — the `X_uploaded` flag guarantees `X` is non-`None` at runtime but pyright can't follow that invariant across the dispatch chain. Baseline drops 1147 → 1140 (and `modules/library.py` falls out of the per-file table entirely).
- `modules/util.py`: high-leverage fix — chip 26 pyright errors out of the file itself AND cascade-fix 56 more errors across other files for a total **−82 baseline reduction** in one PR. Three patterns: (1) Added `@overload` declarations to `util.get_list` so pyright knows `return_none=False` guarantees `list` (never `None`). This single change fixed errors in 8 caller modules (kometa.py, anilist, builder, icheckmovies, imdb, mdblist, **meta.py dropped 59 → 25**, `textfile.py` dropped off). (2) The module-level `logger: MyLogger = None` was lying — the runtime default is None but the annotation said `MyLogger`. Switched to a `TYPE_CHECKING` split (`logger: MyLogger` for pyright, `logger = None` for runtime) so consumers can still call `logger.info(...)` without an Optional guard. (3) Hardened two real latent bugs in the process: `HTTPError.response` is `Response | None` per `requests` typing, but the `retry_if_http_429_error` predicate and `wait_for_retry_after_header.__call__` both used it unconditionally — added explicit `is not None` checks. And `parse()`'s error-message branch did `for o in options` when `options` could be `None` (the `elif` condition allowed entry via the `translation` clause without `options` being set) — now falls back to `translation` keys. Added 4 regression tests. Baseline drops 1181 → 1100 across 9 files; util.py and `textfile.py` drop off the per-file table entirely.
- `modules/radarr.py` + `modules/sonarr.py` + `stubs/arrapi/`: eliminate 108 pyright errors across `radarr.py` and `sonarr.py` (54 each) with a **local stub package** for `arrapi`. Root cause and fix are identical to the `tmdbapis` stubs in the same PR: `BaseObj._parse()` in the `arrapi` library (also Kometa-maintained, `Kometa-Team/ArrAPI`) has no return-type annotation, producing the same ~N-member union explosion on every attribute of every `Movie`, `Series`, and `Tag` object. Stub files created: `stubs/arrapi/{__init__,exceptions}.pyi`, `stubs/arrapi/apis/{__init__,base,radarr,sonarr}.pyi`, `stubs/arrapi/objs/{__init__,base,reload,simple}.pyi`. `RadarrAPI.add_multiple_movies` and `SonarrAPI.add_multiple_series` are fully annotated with their actual positional parameter lists (they had 8 and 11 positional params respectively); `_raw: Any` is declared as an instance attribute on `SonarrAPI` (it's an internal raw-API object with `.v3`/`.v4`/`.new_codebase` attributes, not a method). Four remaining `Optional.id/name` errors in each file (`qp: QualityProfile | None` loop-assigned variable used without a None guard) are pre-existing real bugs and left for a separate PR. Baseline drops 397 → 297 (`radarr.py` and `sonarr.py` each drop 54 → 4).
- `modules/tmdb.py` + `stubs/tmdbapis/`: eliminate all 670 pyright errors in `modules/tmdb.py` with a **local stub package** for `tmdbapis`. The root cause: `TMDbObj._parse()` in the `tmdbapis` library has no return-type annotation, so pyright infers a ~49-member union from all its `elif` branches (every TMDb object type — `Movie`, `TVShow`, `Credit`, `Episode`, `int`, `float`, `str`, `datetime`, etc.). Every attribute assigned via `self._parse(...)` then carries that union, and any subscript or iteration on such an attribute emits one error *per union member* — hence 49 errors on a single `results.tv_results[0]` subscript and 40 errors on each `for i in person.movie_cast` comprehension. Fix: `stubs/tmdbapis/` directory (new) with `pyproject.toml: stubPath = "stubs"`. The stub declares `_parse() -> Any` on `TMDbObj`, which collapses all attribute union explosions. Critical attributes that Kometa actually uses are typed precisely — `FindResults.{movie_results,tv_results,tv_episode_results}: list[Movie|TVShow|Episode]`, `Person.{movie_cast,tv_cast,movie_crew,tv_crew}: list[Credit]`, `Credit.{movie,tv_show}: Movie|TVShow`, `TMDbPagination.total_results: int` + `__iter__` — so code that reads those attributes gets the right narrow type. `__getattr__ -> Any` on every class serves as the safe fallback for the hundreds of other attributes Kometa doesn't use. One small code fix in `modules/tmdb.py`: added `self.tvdb_id: None = None` to `TMDbMovie.__init__` (the attribute is only meaningful on `TMDbShow`, but `convert_from()` accesses `item.tvdb_id` on the union type — pyright correctly flagged the missing attribute on the movie branch). Baseline drops 1067 → 397 (`modules/tmdb.py` falls off the per-file table entirely). `tmdbapis` is maintained by the Kometa team; a follow-up PR to add `py.typed` + inline annotations upstream would eliminate the need for these stubs.
- Add a 3-strike retry around Plex `saveMultiEdits()` read/connect timeouts: retry after 2 seconds on attempt 1, retry after 5 seconds on attempt 2, and fail gracefully on attempt 3 while preserving the traceback in `meta.log`.
- Update the Sight & Sound Letterboxd default collection to use the correct `sightsoundmag` owner for the list.
- Fix mixed-library playlist `plex_search` by building the Plex search per library type while logging the criteria once.
- Fix asset-directory logos being ignored during metadata image updates.
- Skip episode rating operations for movie libraries.
- Show the configured assets directory in missing-asset warnings instead of `'None'` when no matching flat asset file is found.
- Suppress full stack traces for a small set of known, non-critical logger patterns so expected Plex not-found noise prints as a warning instead of a stack trace.
- Preserve Plex batch multi-edit state across timeout retries so a transient `saveMultiEdits()` timeout no longer raises `Batch multi-editing mode not enabled` on retry.
- Create the per-library `_backgrounds` and `_logos` image-map tables unconditionally so caches created before those tables existed self-heal on the next run, instead of raising `no such table: image_map_<n>_logos` and failing every collection that sets a logo.
- Report transient TMDb network failures as a warning and a timeout-style error instead of dumping the raw connection traceback into the run summary.
- Asset folder would be reported as `None` when `asset_folders` was set to false
- Skip Recommendation Hub sorting on a targeted `--run-collections` run instead of resorting every pinned collection; a partial run only knows the `hub_priority` values of the collections it rebuilt, so treating the rest as unprioritised pushed them out of position. Also log the configured `hub_priority` value during attribute validation, matching other methods like `collection_order`.
- Extend the `--run-collections` Recommendation Hub sort skip to also cover `--run-files`, which has the same partial-run limitation.
- Warn and skip `item_edition` edits when Plex Pass is unavailable instead of attempting the edit and surfacing a Plex 403 Forbidden error.
- Fix the default AU content-rating overlay so the MA15+ badge matches both Plex rating spellings: canonical `au/MA 15+` from explicit TMDb Australian certifications and Plex-derived `au/MA15+` for titles without an AU certification.
- Fix "Movies Added" report entries missing release year; titles now match the "Movies Missing"/"Movies Removed" format e.g. `The Grapes of Wrath (1939)`.
- Fix `batchMultiEdits()` raising `BadRequest: Cannot mix items of different type` when a batched label/tag/rating write spanned more than one Plex object type (e.g. shows and seasons in the same overlay run).
### Changed
- `modules/imdb.py`: `imdb_search` builder now routes advanced title searches through the Kometa IMDb Service (`POST /search/advanced`) instead of reaching IMDb GraphQL directly. The service runs a single paginated query server-side, caches results by constraint hash with a 1-day TTL, and returns the full ordered result list — eliminating client-side cursor pagination. Lookups with no fallback dataset (parental guide, charts, keywords, and now advanced search) degrade with a logged warning when the service is unavailable.
- `modules/imdb.py`: IMDb-backed mass rating, genre, episode-rating, parental guide, chart, and keyword lookups now prefer the Kometa IMDb Service (`utilities.kometa.wiki/imdb-service`). If the service is unresponsive, Kometa logs the failure and falls back: ratings/genres/episode-ratings to the public IMDb TSV datasets (`title.ratings.tsv.gz`, `title.basics.tsv.gz`, `title.episode.tsv.gz`), parental guide/charts/keywords to GraphQL queries or HTML scraping.
- Update requirements
- `modules/imdb.py`: the `imdb_search` `interests:` catalog (interest name → IMDb `in########` id) is now fetched at runtime from `Kometa-Team/IMDb-Interests` (`INTERESTS.json`) and cached per run, with the bundled snapshot used only as a fallback when the fetch fails. This lets new IMDb interests become usable without a Kometa release. The bundled fallback is also refreshed from 211 to the current 313 interests, and the `imdb_search` Interests Options documentation table is refreshed to match and now points at `INTERESTS.json` as the authoritative source. A staged generator script and scheduled workflow (`.github/imdb-interests-repo-files/`) keep `INTERESTS.json` up to date in the IMDb-Interests repo.
- Playlist `libraries` is now optional; when omitted, playlists use every library processed as part of the run. Defining `libraries` on a playlist still overrides that default.
- `modules/request.py`: every outbound HTTP request now sends a 30-second per-socket timeout (`DEFAULT_TIMEOUT`), so a stalled external server can no longer hang a run indefinitely. Retries on `Requests.get`/`post` switch from a fixed 10-second wait (up to 50s of sleeping per failing URL) to exponential backoff capped at 10 seconds (~25s worst case, much less for transient blips).
- `modules/request.py`: `get_stream` throttles its download-progress log updates to ~4 per second (plus a final 100% line) instead of logging once per 8 KB chunk.
- `modules/cache.py`: the cache now holds one shared SQLite connection (WAL journal mode) for the life of the run instead of opening a new connection for every query — previously each of the ~60 cache methods opened a connection per call and never closed it, which added measurable overhead on large overlay runs. Transaction-per-block commit behaviour is unchanged.
- Updated assets to accept all filenames and filetype extensions that Plex allows as per <https://support.plex.tv/articles/200220677-local-media-assets-movies/>
- Internal: replace `mypy` (which was running with `continue-on-error: true` and producing output nobody read) with `pyright` using a ratcheting baseline. `.pyright-baseline.json` pins the current per-file error counts; the new `pyright` CI job (powered by `scripts/pyright_baseline.py --check`) fails any PR that introduces new errors in a file but lets maintainers chip away at existing errors at their own pace. Today's baseline: 1223 errors across `modules/` + `kometa.py`. See `scripts/README.md` for the `--update` workflow.
### Security
- `modules/cache.py`: dynamic table/column names interpolated into SQL are now validated by a `sql_identifier()` guard at every method that accepts them. All current callers pass internal identifiers, so this hardens against future misuse rather than fixing an exploitable path (and addresses bandit B608).
- `modules/request.py`: a per-library `verify_ssl: false` (e.g. on a Plex connection) no longer disables `InsecureRequestWarning` process-wide; only the config-level global SSL opt-out does.
- `modules/letterboxd.py`: short `boxd.it` URLs must use http/https; other schemes now fail with a clear error before any network call.
- `modules/logs.py`: registered secrets are also redacted in their URL-encoded (`quote`/`quote_plus`) forms, so tokens embedded in logged query strings no longer slip past redaction.
- Internal: replace `mypy` (which was running with `continue-on-error: true` and producing output nobody read) with `pyright` using a ratcheting baseline. `.pyright-baseline.json` pins the current per-file error counts; the new `pyright` CI job (powered by `scripts/pyright_baseline.py --check`) fails any PR that introduces new errors in a file but lets maintainers chip away at existing errors at their own pace. Today's baseline: 1223 errors across `modules/` + `kometa.py`. See `scripts/README.md` for the `--update` workflow.
## [v2.4.4] - 2026-06-25
### Fixed
- Fixed CI: downgrade `actions/checkout` from `v7` to `v6` across all workflows to restore PR validation for fork pull requests (still an upgrade over Kometa 2.3.1)
- Downgrade "Skipping `<name>`: Item not found" log message from `ERROR` to `WARNING` in metadata file processing when a mapped item cannot be found in the Plex library.
- Allow `builder_level` to work with playlists. Fixes #2267
- Consolidate repeated warning messages in the warning summary, including asset folder and poster/background/square-art lookup warnings, builder/overlay validation noise, and Trakt list warnings; keep convert-summary ID lists hidden unless trace or request-logging is enabled, and suppress duplicate raw TMDb collection lookup noise outside trace.
- Fix `ModuleNotFoundError: No module named 'resource'` crash on Windows at startup. The file-descriptor limit fix introduced in #3235 used the POSIX-only `resource` module unconditionally. Now wrapped in a `try`/`except ImportError` so the bump is applied on POSIX systems and skipped cleanly on Windows. (#3244)
- Fix `kometa.py` crashing with `TypeError: HEAD is a detached symbolic reference` when run from a detached-HEAD checkout (release-tag checkouts, CI runners that check out by SHA, etc.). (#3232)
- Fix `cache.update_anime_map()` writing the AniDB id into the AniList column on UPDATE. The original `anime_map` row was written correctly on INSERT but each subsequent update would clobber `anilist_id` with the AniDB value. (#3232)
- Defer cache eviction until all batched Plex edits complete so genre updates (or other non-batched updates) no longer evict items before later rating and originally available date, etc writes run.
- Remove TMDb ID `717095` from the Ice Age franchise default after TMDb no longer returns the collection.
### Changed
- Internal: add a comprehensive pytest test suite (580 tests, up from 203) covering most of `modules/`, plus a regression-test convention (`test_issue_NNNN_*`) for documented bugs. (#3232)
- Internal: add a GitHub Actions test workflow with separate jobs for `lint` (black + isort + flake8), `test` (pytest with 20% coverage gate), `regression` (regression-only suite), `schema` (JSON Schema validation of `json-schema/*.json` + kitchen-sink config), `imports` (auto-discovered import smoke check across all 40 modules), `perf` (slow-test reporting via `pytest --durations-min`), and `smoke` (`kometa.py --help` + minimal-config dry-run). (#3232)
- Internal: add `.gitattributes` to normalize line endings to LF on commit for all text sources, flag image/font/PSD files as binary so Git won't try to diff them, and add `linguist-vendored`/`linguist-generated` hints for cleaner GitHub language stats. Renormalized `defaults/overlays/languages.yml` which had been checked in with CRLF endings since a 2025 community PR. Also adds `export-ignore` entries so `git archive` source tarballs no longer ship `tests/`, `.github/`, or other dev-only files.
- Internal: extend the `test`, `imports`, and `smoke` CI jobs to a Linux + Windows OS matrix so POSIX-only regressions (the class of bug that produced #3244) get caught before merge instead of at user launch. Adds an AST-based regression test (`test_issue_3244_*`) that statically pins the `try/except ImportError` guard around `import resource` in `kometa.py` and asserts every `resource.<attr>` reference at module scope sits inside an `if resource is not None:` block.
- Prevent Kometa from creating duplicate collections when Plex search misses an existing same-named collection by falling back to the full collection inventory before creating.
- Add an end-of-run warning when duplicate collection titles are detected in a Plex library and suggest checking Plex DBRepair if the duplicates are unexpected.
## [v2.4.3] - 2026-06-22
### Fixed
- Fix image size validation during metadata uploads so local poster/background/square art files are checked using their actual file size instead of the string `compare` field, avoiding a `TypeError` in operations.
## [v2.4.2] - 2026-06-22
### Added
- Add jsonschema 4.23.0.
- Add letterboxdpy 6.5.0.
- Report deactivated Trakt accounts [Trakt returns a 410].
- Add E4 to network overlay and show network collection. #2975
- Adds new `text_file` builder.
- Initial support for SIMKL.com; trending lists and dvd-releases list.
- Add Apprise as a webhook notification provider.
- Added `language` parameter to `mass_poster_update` and `mass_background_update` operations to override TMDb language for image fetching (e.g. `xx` for textless posters).
- Added `plex_csm` and `plex_csm0` sources to `mass_content_rating_update` (reads CSM age rating from Plex's own cached `commonSenseMedia.ageRatings`, no external API key required).
- Add Plex square art support: upload `square.ext` assets for movies, shows, seasons, albums, and playlists via `find_and_upload_assets`. Extends metadata file items and library operations.
- Add comprehensive config validation with `--validate`, `--validate-level`, `--validate-schema`, and `--schema-path` CLI flags. Supports syntax, structure, full, and schema-level validation with a gap report showing missing/extra fields.
- Add `--validate-file` and `--validate-dir` CLI flags for standalone and batch YAML validation against auto-detected schema types (config, collection, metadata, overlay, playlist). Output written to `validate.log`.
- Add `modules/validator.py` with `ConfigValidator` (multi-level config validation) and `FileSetValidator` (standalone/batch YAML validation with schema type detection).
- Expand `json-schema/config-schema.json` with full coverage and add per-type schemas: `collection-schema.json`, `metadata-schema.json`, `overlay-schema.json`, `playlist-schema.json`.
- Add `apprise` and `schedule_overlays` to the config JSON schema.
- Add `auto_sort_hubs` global/library setting to sort Recommendation Hub rows on the Plex home screen after all collections are processed. Accepts `sort_title`, `sort_title.desc`, `alpha`, `alpha.desc`, `configured`, `configured.desc`, or `random`.
- Add `hub_priority` per-collection attribute to pin a collection's hub row to a specific position (lower number = higher up). Collections with `hub_priority` are sorted first; the remainder follow in `auto_sort_hubs` order.
- Add `monthly(last)` schedule option: fires on the last day of every month regardless of its length (Feb 28/29, Apr/Jun/Sep/Nov 30, or the 31st in 31-day months).
- Add `hub_priority` and `hub_priority_<<key>>` template variables to the shared default templates so collections can set hub row priority from templates.
- Add `hub_priority` to the collection config schema so the new template variable validates correctly.
- Add support for boxd.it urls, used for letterboxd share lists.
- Allow metadata updates to proceed on existing collections even if the builder returns 0 items.
- Add new operations attributes `respect_ignore_ids` to skip operations processing items in `ignore_ids` or `ignore_imdb_ids`, and `ignore_labels` to skip operations processing items with the specified label(s).
- Add new TMDb release date types for `mass_originally_available_update` and `mass_added_at_update` operations: `tmdb_premiere`, `tmdb_theatrical`, `tmdb_theatricallimited`, `tmdb_digital`, `tmdb_physical`, `tmdb_tv`.
### Changed
- Bump lxml to 6.0.4.
- Bump packaging to 26.1.
- Bump gitpython to 3.1.50.
- Bump letterboxdpy to 6.5.7.
- Bump lxml to 6.1.1.
- Bump packaging to 26.2.
- Bump plexapi to 4.18.1.
- Bump python-dotenv to 1.2.2.
- Bump pillow to 12.2.0.
- Bump pywin32 to 312.
- Bump requests to 2.34.2.
- Bump setuptools to 82.0.1.
- Bump prek to 0.4.5.
- Switch to GraphQL for IMDb charts retrieval, with a fallback to the old method if the GraphQL request fails. #3006
- Replace Kometa's internal Letterboxd scraping with a letterboxdpy-backed adapter while preserving existing Letterboxd builder names.
- Process collection alterations [add/remove] in batches of 100 to avoid "400 Bad Request" on mile-long URLs.
- Exit early if more than one of the `--SOMETHING-only` flags are set.
- Cache people data for media items since it's unlikely to change.
- The `tmdb_discover` YAML validator now enforces: unknown keys are rejected, value types are checked, `certification_country`/`certification` must co-occur, `watch_region` must accompany any watch-provider or monetization key, and movie-only and TV-only parameters are mutually exclusive. Note: `with_watch_providers` and `without_watch_providers` now require `watch_region` at validation time (the TMDb Discover API requires this), which is stricter than the runtime check in `builder.py`.
- `--validate` now implies immediate run (like `--run-libraries`).
- Use the SQLite cache for TMDb queries in `check_filters` and `check_missing_filters` instead of bypassing it with `ignore_cache=True`.
- Only force-reload Plex items in the overlay loop when `reapply_overlays` is set; reuse the session cache on normal runs.
- Use set lookup instead of list scan when checking collection membership in `add_to_collection` and `run_collections_again`, reducing complexity from O(n×m) to O(n).
- Extend default "Metacritic Must See" collection to work with TV.
- Improve JSON schemas and add prototype YAML configuration files for all builders.
- Add translations for Genre Defaults posters.
- Updates to Quickstart docs about limitations.
- Updates to Trakt and MAL flow pages.
- Update Letterboxd builder docs to reflect letterboxdpy-backed filtering, ordering, and incremental behavior.
- Document the valid `font_style` values for the default Inter font and note that valid styles are font-specific. #2925
- Call out the schema file as supporting only `config.yml` explicitly.
- Sync `pyproject.toml` pinned dependencies to match `requirements.txt` (lxml, requests, pillow, packaging, gitpython, pywin32; add letterboxdpy).
- Align Black `target-version` to `["py312"]` in `pyproject.toml` to match the project's existing `requires-python = ">=3.12"`.
- Standardise GitHub Actions checkout version to `@v6` in `spellcheck.yml` and `lint.yml`.
- Fix `lint.yml` target paths from `src/` to `modules/` to reflect actual codebase structure.
- Sync `.pre-commit-config.yaml` hook revisions to match `dev-requirements.txt` (black 26.5.1, isort 8.0.1, flake8 7.3.0).
- Fix `.dockerignore` entry `CHANGELOG` → `CHANGELOG.md` after changelog file rename.
- Normalise `AGENTS.md` and `.dockerignore` line endings from CRLF to LF.
- Remove stale `Dockerfile.lxml` entry from `.dockerignore` (file was removed from the repo long ago).
- Update `CLAUDE.md` test file list to reflect all current test files in `tests/`.
- Add "Chore" PR type option to pull request template to match the project's existing `chore/` branch convention.
- Remove `reciperr_list` from `collection-schema.json` (Reciperr builder was removed).
- Update spellcheck configuration: exclude `docs/kometa/acknowledgements.md` (credits file) and update wordlist.
- Add spellcheck to pre-commit configuration.
### Removed
- Reciperr builder removed; the Reciperr site has been unresponsive for an extended period.
### Fixed
- Dynamic collection titles with unresolved template variables in `title_format` (e.g. `<<limit>>` from a template `default:` block, or `<<key>>` from per-key dynamic variables) no longer cause `delete_collections: configured: true` to incorrectly treat those collections as unconfigured and delete them. #1904
- When `run_order` places `operations` before `collections`, configured collections are no longer incorrectly reported as unconfigured by `show_unconfigured` or targeted by `delete_collections: configured: true`. #1968
- Fix "Movies Removed" report entries missing release year; titles now match the "Movies Missing" format e.g. `The Grapes of Wrath (1939)`. Fixes #2028
- Invalidate cached Plex items after each batch edit in operations so that the overlay phase reads post-operations values (e.g. updated ratings) rather than stale pre-operations values. Fixes rating overlays not updating when `mass_critic_rating_update` (or audience/user equivalents) runs before overlays in the same Kometa pass. #2357
- Overlay backdrops with `back_line_color` set but no `back_line_width` no longer crash; `back_line_width` now defaults to 1 in that case. #2645
- Fix `monthly(N)` schedule: values greater than the current month's last day (e.g. `monthly(31)` in November) no longer fire on the last day of the month as a fallback. `monthly(N)` now only triggers when today is exactly day N. A warning is logged when a configured day does not exist in the current month, directing users to the new `monthly(last)` option. Fixes #2884
- Streaming default's `run_definition` now honors `use_all: false`; previously dynamic keys without an explicit `use_<key>` ran regardless. #2922
- Catch KeyError on Plex NFO build.
- Fixing "list index out of range" error when processing IMDb charts. #3006
- TMDB language now reflected in `mass_poster_update`; cache extension.
- Reduce size of FILMIN logo.
- Reduce size of FILMIN white logo.
- Replace top-ten-pirated default list with an updated, Kometa-controlled one.
- Align Kometa's AniList season translation from `current` to match AniList.
- Fix an issue where `tvdb_to_imdb` writes the wrong IMDb ID when a TVDb movie ID collides with a TVDb series ID.
- Update Letterboxd list/watchlist parser for the React-based `LazyPoster` markup, which replaced the `data-film-id` nodes the `letterboxd_list`, `letterboxd_user_films`, and `letterboxd_user_reviews` builders relied on.
- Update Letterboxd URL parsing to handle unlisted lists and shuffle argument in the URL.
- Restore support for Letterboxd `/detail/` list views and `/USERNAME/films/reviews/` URLs.
- Update Letterboxd Top 100 Western to use official list.
- Fix `imdb_awards` failing when any one of the `award_filters` or `category_filters` does not exist; this now produces a warning and the builder will only fail if all of the `award_filters` or all of the `category_filters` do not exist.
- Fix `imdb_watchlist` rejecting the new IMDb user ID format (`p.xxxxxxx`). Now accepts both `ur########` (legacy) and `p.xxxxxxx` formats, as a bare ID or full watchlist URL.
- TVDb 4xx responses (series/movie removed or merged on TVDb) now raise a dedicated `tvdb.NotFound` so the missing-show iteration and TVDb-filter paths log them at debug instead of error. Stale TVDb IDs returned by TMDb `external_ids` no longer spam logs or trigger webhook failure notifications. #3047
- Replace `mass_imdb_parental_labels` HTML scrape of `/parentalguide` with IMDb's GraphQL API, which is not WAF-gated and returns structured parental guide data directly. Fixes silent label failures caused by IMDb's CloudFront WAF returning HTTP 202 (empty body) to scraper requests. #3053
- Fix trakt_chart watched/collected URLs to use `/movies(shows)/watched(collected)/{period}` and default period to "weekly"; fix recommended to use `/recommendations/movies(shows)`. #3057
- TMDb collection requests that 404 (collection deleted upstream — TMDb now only permits collections for true movie sequels) now raise a dedicated `tmdb.NotFound`. When the ID was auto-discovered by a default (e.g. the `franchise` default scanning `tmdb_collection` IDs from the library), the collection is skipped as "Ignored" instead of failing with a critical error and webhook notification. IDs in user-authored config files still raise as before.
- TMDb collection, movie, and show 404 responses now raise a dedicated `tmdb.NotFound` (mirroring the existing `tvdb.NotFound` pattern); `validate_tmdb_ids` logs an actionable hint for stale collection IDs pointing franchise-default users to the `exclude` template variable.
- Fix MDBList Sync Progress percentage overflowing for lists over 1000 items (e.g. `6378/378 (1687%)`). Fetch list metadata upfront to get the true total item count. #3157
- Fix `ZeroDivisionError` crash in `plex_update_in_batches` when running `mass_genre_update` or `mass_imdb_parental_labels` (and any operation producing multiple label/genre groups per item). Cache eviction introduced by #3156 is now scoped to rating updates only via an `evict_cache` parameter; a defensive guard prevents any future empty-list crash. #3163
- Guard against `_graph_request` returning `None` for items with no IMDb record (e.g. `Looney Tunes`); previously the unguarded `.get()` call raised an `AttributeError` that escaped the `except Failed` handler in `operations.py` and aborted processing for the entire library. #3165
- Fix `mass_imdb_parental_labels` crash when IMDb GraphQL returns `null` for the title node (e.g. items whose only Plex GUIDs are `tmdb://` or `tvdb://`, such as `Looney Tunes`); `.get("title", {})` returns `None` when the key exists with a null value, causing `AttributeError` on the next `.get()` call. Replaced the chained one-liner with explicit variable extraction using `or {}` at each level. Added a pre-call guard in `operations.py` to skip items with no IMDb ID entirely. #3165
- `delete_collections: configured: false` no longer incorrectly deletes configured Kometa-managed collections when `run_order` places `operations` before `collections`. The check previously relied on `library.collections` (populated during the collection run), which is always empty when operations executes first — so every managed collection appeared unconfigured and was deleted. The fix uses `collection_names` (pre-populated from config before the run-order loop) expanded with English-translated titles, so default collections whose YAML key differs from their Plex title (e.g. `award/spirit` → `Spirit Best Feature Winners`) are also correctly recognised as configured. #3168
- Addresses edge case where missing `settings:minimum_items` would make Kometa redact `1` in the log.
- Hide some traceback errors for "known good" errors that don't require a full trace.
- Fix an issue where collections would not be recognised as configured if they were not scheduled to run.
- Fix an issue where Plex would fail to update collections if the casing of the collection name did not match the YAML (e.g. "Dogs And Cats" vs "Dogs and Cats").
- Fix shared `ignore_ids` and `ignore_imdb_ids` template variables for builders that resolve Plex `ratingKey`s first.
- Fix dynamic collection sync loop when `translation_key` and `title_format`/`name_format` are both set — `meta.py` sync key now follows the same name-resolution priority as `builder.py` (`name_format` → translation → `title_format`), eliminating the orphan-delete loop on every run. If the translation name still contains unresolvable library-context variables (e.g. `<<library_translationU>>`), `meta.py` falls back to `title_format` (which has `<<library_typeU>>` pre-resolved) to ensure the sync key matches what `builder.py` writes to Plex. #3189
- Fix `title_format` fallback value in `award/sag.yml` to match its translation name (`Screen Actors Guild Awards <<key_name>>`); fix accent typo in `award/cesar.yml` (`César <<key_name>>`). #3189
- Fix inconsistent asset poster selection and overlay cache invalidation when replacing local asset files. #3196
- Fix `audio_language`/`subtitle_language` `plex_search` on show libraries returning fewer results than expected when Plex returns regional language codes (e.g. `en-US`) instead of base codes (`en`). #2777
- Sanitize PR branch names in CI workflows (`validate-pull.yml`, `increment-build.yml`, `tag-cleanup.yml`) by replacing non-Docker-safe characters with `-`; branch names containing `/` (e.g. `feature/my-fix`) previously caused Docker Hub tag pushes and deletes to fail. #3207
- Fix `tag-cleanup.yml` output key mismatch (`tag_name` vs `tag-name`) that caused the Docker Hub tag DELETE to always target an empty tag name, leaving stale tester tags behind. #3207
- Fix sync-based collections with `minimum_items: 1` and `delete_below_minimum: true` so they remove the last stale item and delete the collection when the source list drops to zero items. #3209
- Invalidate cached Plex items after batched edits in operations so metadata files can override values in the same run instead of flip-flopping on later runs. #3211
- Fix `other_name` in dynamic collections: `<<library_type>>` and `<<library_typeU>>` placeholders are now resolved (they were applied to `title_format` but not to `other_name`). Also fix a long-standing copy-paste bug where `other_name` supplied via `template_variables` was read from `self.temp_vars["remove_suffix"]` instead of `self.temp_vars["other_name"]`. #3215
- Fix an issue with AniList search date modifiers not working. #3220
- Fix Studio Defaults capitalization issues. #3221
## [v2.3.1] - 2026-04-01
Prior history is captured in [GitHub Releases](https://github.com/Kometa-Team/Kometa/releases).
[unreleased]: https://github.com/Kometa-Team/Kometa/compare/v2.5.4...HEAD
[v2.5.4]: https://github.com/Kometa-Team/Kometa/compare/v2.5.3...v2.5.4
[v2.5.3]: https://github.com/Kometa-Team/Kometa/compare/v2.5.2...v2.5.3
[v2.5.2]: https://github.com/Kometa-Team/Kometa/compare/v2.5.1...v2.5.2
[v2.5.1]: https://github.com/Kometa-Team/Kometa/compare/v2.5.0...v2.5.1
[v2.5.0]: https://github.com/Kometa-Team/Kometa/compare/v2.4.9...v2.5.0
[v2.4.9]: https://github.com/Kometa-Team/Kometa/compare/v2.4.8...v2.4.9
[v2.4.8]: https://github.com/Kometa-Team/Kometa/compare/v2.4.7...v2.4.8
[v2.4.7]: https://github.com/Kometa-Team/Kometa/compare/v2.4.6...v2.4.7
[v2.4.6]: https://github.com/Kometa-Team/Kometa/compare/v2.4.5...v2.4.6
[v2.4.5]: https://github.com/Kometa-Team/Kometa/compare/v2.4.4...v2.4.5
[v2.4.4]: https://github.com/Kometa-Team/Kometa/compare/v2.4.3...v2.4.4
[v2.4.3]: https://github.com/Kometa-Team/Kometa/compare/v2.4.2...v2.4.3
[v2.4.2]: https://github.com/Kometa-Team/Kometa/compare/v2.3.1...v2.4.2
[v2.3.1]: https://github.com/Kometa-Team/Kometa/releases/tag/v2.3.1