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.