This release includes breaking changes for platform teams planning a safe upgrade.
Published 4d
Media Servers
✓ No known CVEs patched
✓ No known CVEs patched in this version
Topics
kometa
Affected surfaces
deps
breaking_upgrade
Summary
AI summaryBroad release touches None, Internal, deduped, and unchanged.
Full changelog
Added
- Add semantic
letterboxd_crew,letterboxd_studio,letterboxd_country,letterboxd_language,letterboxd_genre,letterboxd_theme,letterboxd_similar, andletterboxd_collectionbuilders for Letterboxd discovery pages. - Add
value_filteroverlay-file attribute to filter items at selection time based on a runtime-fetched numeric value; supports comparatorsgte,gt,lt,lteon anyrating_sourcesvariable using the normalised 0–10 scale. - Add
overlay_value_cachetable (replacesoverlay_special_text2) with aUNIQUE(rating_key, type)constraint and anexpiration_datecolumn; values refresh automatically aftercache_expirationdays. - Add
_overlay_stateand_overlay_imagesper-library tables replacing the dual-useoverlay TEXTcolumn in_overlays; one row per overlay per item, written only on successful resolution. - Add one-time migration that moves rows from
overlay_special_text2intooverlay_value_cache(deduped), resets overlay application state for a full reprocess, and logs a visible "Overlay Cache Upgraded" separator. - Rework
defaults/overlays/ratings.ymlto 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 viavalue_filter, aDirectimage level for tomatoes/tomatoesaudience, and new image assets underdefaults/overlays/images/rating/. - Accept HTTP(S) URLs anywhere a
text_filebuilder 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 singletext_filebuilder are supported. - Add
logoandsquare/square_artasset detection and setting viaurl_andfile_methods for Collection builders, as well as Defaults viaurl_*_<<key>>template variables. - Add Plex-compatible local asset names for posters, backgrounds, logos, and square art, including
.tbnfiles and numbered variants such asposter-2.png; exact filenames such asposter.jpgtake priority over numbered variants. - Add
traktas a source for posters, backgrounds, and logos. Square art remains unsupported. - Add
tvdbas 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_listandyamtrack_list_detailsbuilders. - Add
yamtrack_trackedbuilder 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: fixget_image()anddelete_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 underlyingrequestsexception and raiseFailed, matching how every other network-facing call in these modules already degrades.modules/tvdb.py: add a distinctUnavailableexception for TVDb requests that exhaust their retry budget without ever returning usable content (e.g. repeated HTTP 202/empty-body "still generating" responses), separate fromNotFound'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. Allget_tvdb_obj()call sites (modules/builder.py,modules/operations.py) now logNotFoundat debug (unchanged),Unavailableat warning (previously logged at error via the genericFailedhandler), and any otherFailedat error (unchanged).- Fix custom
rating<n>_file(andgit/repo/url) overlay images being silently replaced by a built-indefault/pmmasset. - 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 | Messageformat as warning/error summaries; add a visual divider before the summary intro text; rename the overlay section toOverlay 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_text2on every run due to a missingUNIQUE(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: fixtmdb:entries in playlist/mixed mode (whereis_movie=None) being silently dropped. The oldis_movie is not Falsecheck treatedNoneasTrue, tagging TV show TMDb IDs as"tmdb"type which only checksmovie_map. Now returns both(id, "tmdb")and(id, "tmdb_show")so the builder resolves against both movie and show maps. Also updatesmdblistandsimklthree-state contract comments and docs.modules/poster.py: fix 18 Optional-member-access errors flagged by pyright. TheImageData.__init__triple-nested-ternary is unwound into an explicitif/elif/else(also skips a redundantos.stat()call whencompare=is provided explicitly).apply_varsandadjust_text_widthearly-return onself.text is None(matches the callers' existingif component.text:guard).get_generated_layerraisesFailedwith a descriptive message instead of crashing with'NoneType' object is not subscriptablewhenImage.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.letterboxdpyis a pinned hard dependency inrequirements.txt, so the speculativetry/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 twoutil.get_list(...)for-loops invalidate_letterboxd_lists/validate_letterboxd_user_pagesnow passreturn_none=Falseand fall back to[], so they never try to iterateNone. Net diff: -19 lines, no behavioural change for any real install. Baseline drops 1205 → 1196 (andmodules/letterboxd.pyfalls out of the per-file table entirely).modules/anidb.py: switchfrom lxml import etreetoimport lxml.etree as etreeso pyright can resolve the symbol.lxml.etreeis a C extension (no Python wrapper module), and thefrom X import Yform trips pyright'sreportAttributeAccessIssuewhile the equivalentimport X.Y as Yform 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-repeatedre.search("-(.*)", check_id).group(1)pattern into a_hama_suffix()static helper that raisesMappingConvertError("Malformed Hama ID ...")if the regex doesn't match, instead of letting anAttributeErrorescape from.group(1)onNone. Restructured theint(self.tmdb_to_tvdb(tmdb_id, fail=True))call to extract the result first and check forNone(the surroundingexcept Failed: passonly catches the documented failure mode;int(None)would have been anAttributeErrorslipping past it). The twoutil.get_list(...)for-loops now passreturn_none=Falseand fall back to[]so they never try to iterateNone(matches the fix already used inmodules/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_imdbiteratesutil.get_list(...)with the now-familiarreturn_none=False or []belt-and-suspenders. (2)main_data = imdb_dict[X].strip()becomesstr(imdb_dict[X]).strip()so a non-string YAML value (e.g. a bare numeric id) doesn't crash withAttributeError. (3) In_graphql_json.check_constraint, theif range_data:block readsrange_name[i]— butrange_namedefaults toNone, 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_keywordsnow guards onif result is not None, falling back to(0, 0)(matches theelsebranch already used for inputs without"of"). Also added@overloadsignatures toIMDb._interface(interface)so"ratings"/"basics"returndictand"episode"returnslist[list[str]], fixing theself.ratings[imdb_id]type-mismatch at the cached-property layer. Baseline drops 1188 → 1181 (andmodules/imdb.pyfalls 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 inletterboxd.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; theor []belt-and-suspenders was the missing piece sincereturn_none=Falsealone 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: switchexcept ryaml.error.YAMLErrortoexcept ryaml.YAMLErrorat both call sites (lines 144 and 291). Same C-extension import-form trap asmodules/anidb.pyandmodules/request.py: pyright resolves the top-levelruamel.yaml.YAMLErroralias but not the.error.YAMLErrorchain. Identical runtime behaviour —ruamel.yamlre-exportsYAMLErrorfromruamel.yaml.error. Baseline drops 1181 → 1179 (andmodules/validator.pyfalls 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"))becomesint(str(...))to satisfy pyright'sint(str | datetime)complaint —validate_datereturnsstrwhenreturn_asis provided but the helper is not typed to reflect that. (2) Themod == "gte"/"lte"blocks didvalue -= 1/value += 1where pyright sawvalueasint | LiteralString | Unknown. The invariant 'gte/ltemodifiers only apply to numeric attributes (start/end/episodes/duration/score/popularity)' is true at runtime but not visible across the dispatch table. Cast withint(value)to make the contract explicit. (3)validate(name, data)iteratedutil.get_list(data)(possibly None) and calledd.lower()on items pyright saw asdict | int | str. Addedreturn_none=False or []+ wrappeddinstr(...). Added 1 test (numeric input doesn'tAttributeError). Baseline drops 1181 → 1174.modules/request.py: fix 7 pyright errors across three patterns and harden three latent bugs. (1)get_streamdiddl / total_length * 100inside the per-chunk log line, buttotal_lengthisNonewhen the server doesn't send aContent-Lengthheader — division wouldTypeError. Added anif total_length:branch that falls back to a bytes-only progress line. (2)get_headerhad an implicitreturn Nonepath that theget_cloudscrape_htmlcaller would crash on withcloud_headers.pop("User-Agent"). Made the fallbackreturn {}so callers always get a dict (semantically identical toNoneforrequests.get(headers=...)). (3) TheYAMLclass letself.path=Nonesilently flow intoos.path.exists()/open()when the caller forgot to pass eitherpathorinput_data. Added an explicitraise Failed("Either path or input_data must be provided")guard at the start of the no-input-data branch. Also rewrote theexcept ruamel.yaml.error.YAMLErrorclause to useruamel.yaml.YAMLError(which pyright can resolve) and replacede.problemdirect access withgetattr(e, "problem", None)sinceproblemonly exists onMarkedYAMLError(a bareYAMLErrorwould have crashed the error handler withAttributeError). Added 4 tests (get_headermatrix +YAML()failure path). Baseline drops 1181 → 1174 (andmodules/request.pyfalls out of the per-file table entirely).modules/logs.py: fix 5 pyright errors across two clusters. (1)remove_main_handlerandremove_playlists_handlercalledself._logger.removeHandler(self.main_handler)directly, butmain_handler/playlists_handlerare initialised toNonein__init__and only assigned in the correspondingadd_*method. Callingremove_*beforeadd_*would crash with a type error fromlogging.removeHandler(None). Added explicitif self.main_handler is not None:guards. (2)MyLogger.findCallerusedwhile hasattr(f, "f_code"):to walk the frame chain — semantically correct (every non-Noneframe hasf_code) but pyright doesn't narrowOptional[FrameType]throughhasattr. Switched to the equivalentwhile f is not None:which is both clearer and pyright-friendly. Baseline drops 1156 → 1151 (andmodules/logs.pyfalls out of the per-file table entirely).- Singles bundle: fix 4 pyright errors across three small files (one error each in
modules/ntfy.pyandmodules/tvdb.py, two inmodules/ergast.py). (a)modules/ntfy.py: changetitle: str = Nonetotitle: str | None = None— classic PEP 484 Optional-default annotation. (b)modules/tvdb.py: wrapreleasedinstr(...)beforedatetime.strptime— the localparse_pagehelper returnslist | str | Noneand the existingif released else releasedternary handles None, but pyright can't see the str-only branch. (c)modules/ergast.py:Race.session_infosubtractedtimedeltafromself.dateunconditionally, butself.dateis set toNonewhen the API returns a malformed/missing date string (existingexcept (ValueError, TypeError): self.date = None). Added an explicitif self.date is None: video_date = Noneearly branch — real latent bug that would crash withTypeErrorrather than gracefully degrade. Added 1 regression test intests/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 theLibrary(ABC)base — the method is concrete onPlexbut wasn't declared abstract, so pyright sawself.get_ids(item)as an attribute access onLibrary*(the unbound subclass type) and flagged it. (2) Added-> listreturn annotations to the existing@abstractmethoddeclarations forget_allanditem_labelsand replaced theirpassbodies with...— the implicitNonereturn type was being propagated to call-sites likefor la in self.item_labels(item)andfor item in itemswhich then crashedreportOptionalIterable. (3) Matched the existingposter.compare if poster else ""defensive ternary for the parallelbackground,logo, andsquare_artcache-update calls inimage_update— theX_uploadedflag guaranteesXis non-Noneat runtime but pyright can't follow that invariant across the dispatch chain. Baseline drops 1147 → 1140 (andmodules/library.pyfalls 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@overloaddeclarations toutil.get_listso pyright knowsreturn_none=Falseguaranteeslist(neverNone). This single change fixed errors in 8 caller modules (kometa.py, anilist, builder, icheckmovies, imdb, mdblist, meta.py dropped 59 → 25,textfile.pydropped off). (2) The module-levellogger: MyLogger = Nonewas lying — the runtime default is None but the annotation saidMyLogger. Switched to aTYPE_CHECKINGsplit (logger: MyLoggerfor pyright,logger = Nonefor runtime) so consumers can still calllogger.info(...)without an Optional guard. (3) Hardened two real latent bugs in the process:HTTPError.responseisResponse | Noneperrequeststyping, but theretry_if_http_429_errorpredicate andwait_for_retry_after_header.__call__both used it unconditionally — added explicitis not Nonechecks. Andparse()'s error-message branch didfor o in optionswhenoptionscould beNone(theelifcondition allowed entry via thetranslationclause withoutoptionsbeing set) — now falls back totranslationkeys. Added 4 regression tests. Baseline drops 1181 → 1100 across 9 files; util.py andtextfile.pydrop off the per-file table entirely.modules/radarr.py+modules/sonarr.py+stubs/arrapi/: eliminate 108 pyright errors acrossradarr.pyandsonarr.py(54 each) with a local stub package forarrapi. Root cause and fix are identical to thetmdbapisstubs in the same PR:BaseObj._parse()in thearrapilibrary (also Kometa-maintained,Kometa-Team/ArrAPI) has no return-type annotation, producing the same ~N-member union explosion on every attribute of everyMovie,Series, andTagobject. 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_moviesandSonarrAPI.add_multiple_seriesare fully annotated with their actual positional parameter lists (they had 8 and 11 positional params respectively);_raw: Anyis declared as an instance attribute onSonarrAPI(it's an internal raw-API object with.v3/.v4/.new_codebaseattributes, not a method). Four remainingOptional.id/nameerrors in each file (qp: QualityProfile | Noneloop-assigned variable used without a None guard) are pre-existing real bugs and left for a separate PR. Baseline drops 397 → 297 (radarr.pyandsonarr.pyeach drop 54 → 4).modules/tmdb.py+stubs/tmdbapis/: eliminate all 670 pyright errors inmodules/tmdb.pywith a local stub package fortmdbapis. The root cause:TMDbObj._parse()in thetmdbapislibrary has no return-type annotation, so pyright infers a ~49-member union from all itselifbranches (every TMDb object type —Movie,TVShow,Credit,Episode,int,float,str,datetime, etc.). Every attribute assigned viaself._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 singleresults.tv_results[0]subscript and 40 errors on eachfor i in person.movie_castcomprehension. Fix:stubs/tmdbapis/directory (new) withpyproject.toml: stubPath = "stubs". The stub declares_parse() -> AnyonTMDbObj, 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__ -> Anyon every class serves as the safe fallback for the hundreds of other attributes Kometa doesn't use. One small code fix inmodules/tmdb.py: addedself.tvdb_id: None = NonetoTMDbMovie.__init__(the attribute is only meaningful onTMDbShow, butconvert_from()accessesitem.tvdb_idon the union type — pyright correctly flagged the missing attribute on the movie branch). Baseline drops 1067 → 397 (modules/tmdb.pyfalls off the per-file table entirely).tmdbapisis maintained by the Kometa team; a follow-up PR to addpy.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 inmeta.log. - Update the Sight & Sound Letterboxd default collection to use the correct
sightsoundmagowner for the list. - Fix mixed-library playlist
plex_searchby 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 raisesBatch multi-editing mode not enabledon retry. - Create the per-library
_backgroundsand_logosimage-map tables unconditionally so caches created before those tables existed self-heal on the next run, instead of raisingno such table: image_map_<n>_logosand 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
Nonewhenasset_folderswas set to false - Skip Recommendation Hub sorting on a targeted
--run-collectionsrun instead of resorting every pinned collection; a partial run only knows thehub_priorityvalues of the collections it rebuilt, so treating the rest as unprioritised pushed them out of position. Also log the configuredhub_priorityvalue during attribute validation, matching other methods likecollection_order. - Extend the
--run-collectionsRecommendation Hub sort skip to also cover--run-files, which has the same partial-run limitation. - Warn and skip
item_editionedits 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-derivedau/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).
Changed
- Update requirements
- Playlist
librariesis now optional; when omitted, playlists use every library processed as part of the run. Defininglibrarieson 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 onRequests.get/postswitch 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_streamthrottles 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 withcontinue-on-error: trueand producing output nobody read) withpyrightusing a ratcheting baseline..pyright-baseline.jsonpins the current per-file error counts; the newpyrightCI job (powered byscripts/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 acrossmodules/+kometa.py. Seescripts/README.mdfor the--updateworkflow.
Security
modules/cache.py: dynamic table/column names interpolated into SQL are now validated by asql_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-libraryverify_ssl: false(e.g. on a Plex connection) no longer disablesInsecureRequestWarningprocess-wide; only the config-level global SSL opt-out does.modules/letterboxd.py: shortboxd.itURLs 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 withcontinue-on-error: trueand producing output nobody read) withpyrightusing a ratcheting baseline..pyright-baseline.jsonpins the current per-file error counts; the newpyrightCI job (powered byscripts/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 acrossmodules/+kometa.py. Seescripts/README.mdfor the--updateworkflow.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Kometa
Python script to update metadata information for items in plex as well as automatically build collections and playlists. The Wiki Documentation is linked below.
Related context
Related tools
Beta — feedback welcome: [email protected]