From 2cb2c3eb9519c58044a6b5173fb957ddcb6e5558 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:59:56 +0200 Subject: [PATCH] feat(ratings): show every rating source the server already sent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plezy rendered exactly one score per item. MediaRatingBadge._ratingDataFor took `rating` and fell back to `audienceRating` only when it was null, so a Plex movie carrying four attributed scores surfaced one, and which one was whatever the server happened to put in the scalar slot. #1755 asked for a setting to choose the source; showing all of them answers it without one. The data was already on the wire and being thrown away. `/library/metadata/ {id}` returns a `Rating[]` child array — IMDb, both Rotten Tomatoes panels, TMDB — with no extra query parameter, but PlexMetadataDto declared no field for it, so json_serializable dropped the key. The identical parse already existed in plex_catalog_source for the Explore tab and had simply never been wired to library items. Model the scores as a list rather than widening the scalar pair. The neutral MediaItem gains `ratings`; PlexMediaItem loses audienceRating, ratingImage and audienceRatingImage, which the list subsumes — Plex sends those images on listings too, so the same field covers both response shapes and no caller narrows to a backend type to read a score any more. CatalogRatingSource is promoted to lib/media as MediaRatingSource instead of growing a second near-identical type beside it, and plex_catalog_source's _ratingsFor becomes the shared plexRatingSources so one implementation serves both paths. There is no persistence to migrate: MediaItem.toJson has no production caller, the offline path re-parses raw Plex JSON through the same mapper, and Plex's audienceRating sort is server-supplied data, not a model read. Cards and the dashboard still show fewer scores than detail screens, and that part is a real Plex limit rather than a shortcut. Section listings send only the scalar pair; includeRatings, includeElements=Rating, includeFields=Rating, includeChildren and includeExtras were each probed against a live server and none surfaced the array, while includeGuids=1 demonstrably does add Guid[] — the probe works, the parameter does not exist. Hydrating every card would be one request per row, so listings render whatever their own response carried, which is one or two attributed scores rather than the single one they showed before. Jellyfin has no per-source array at all: the server collapses whatever its fetchers found into CommunityRating and CriticRating. CommunityRating's provenance is unknowable from the DTO — TMDB vote_average, IMDb via OMDb or a local NFO, last writer wins — so it stays the generic `audience` source with no brand mark. CriticRating is the Rotten Tomatoes Tomatometer as a 0-100 percent and is divided by ten explicitly rather than folded by magnitude, because a Tomatometer of 9 means 9% and range-sniffing would have promoted a rotten score to fresh. Photo rows are skipped, since Jellyfin reuses CommunityRating for the EXIF 0-5 star. The badges share one slot on every surface. On the phone hero the scores go in a single pill because that chip row is a height-clipped Wrap and a chip per source would push year, certification and runtime out of the visible band on short heroes; on the TV detail line and the dashboard spotlight the group occupies the one metadata slot so bullet separators do not multiply. The group announces itself as a single semantics node naming each source, because a bare row of four percentages tells a screen reader nothing about which score is which. rating_utils drops parseRatingImage and isRottenTomatoes — the URI vocabulary now lives only in the Plex mapper — and the source-key resolver and label map, previously private to the Explore detail screen, become the shared pair both screens use. The label strings move from explore.ratingSource to common.ratingSource accordingly, which costs no translations because every non-English value was empty; running clean_translations also scaffolds startup.quitPlezy and startup.restartRequiredBody, which were already drifted. Verified against the live server the probes came from: a detail response now yields TMDB 83%, IMDb 8.3 and Rotten Tomatoes audience 96% through the production mapper and badge resolver, and the listing response for the same title yields TMDB 83% alone. Both payloads are pinned verbatim as fixtures. Coverage adds mapper ordering, dedupe against the array's repeat of the scalar, out-of-range rejection, the Jellyfin scale and photo guard, the CatalogItem conversion that feeds Explore's dashboard hubs, and the three render surfaces including the semantics announcement. close #1755 --- lib/i18n/az.i18n.json | 50 ++-- lib/i18n/bg.i18n.json | 50 ++-- lib/i18n/da.i18n.json | 50 ++-- lib/i18n/de.i18n.json | 50 ++-- lib/i18n/en.i18n.json | 48 ++-- lib/i18n/es.i18n.json | 50 ++-- lib/i18n/fr.i18n.json | 50 ++-- lib/i18n/hu.i18n.json | 50 ++-- lib/i18n/it.i18n.json | 50 ++-- lib/i18n/ja.i18n.json | 50 ++-- lib/i18n/kk.i18n.json | 50 ++-- lib/i18n/ko.i18n.json | 50 ++-- lib/i18n/nb.i18n.json | 50 ++-- lib/i18n/nl.i18n.json | 50 ++-- lib/i18n/pl.i18n.json | 50 ++-- lib/i18n/pt.i18n.json | 50 ++-- lib/i18n/ru.i18n.json | 50 ++-- lib/i18n/strings_en.g.dart | 180 +++++++-------- lib/i18n/sv.i18n.json | 50 ++-- lib/i18n/tr.i18n.json | 50 ++-- lib/i18n/uz.i18n.json | 50 ++-- lib/i18n/zh-Hant.i18n.json | 50 ++-- lib/i18n/zh.i18n.json | 50 ++-- lib/media/media_item.dart | 26 ++- lib/media/media_item.freezed.dart | 71 +++--- lib/media/media_item.g.dart | 10 +- lib/media/media_rating.dart | 37 +++ lib/models/catalog/catalog_item.dart | 6 +- lib/models/catalog/catalog_metadata.dart | 29 --- lib/screens/catalog_item_detail_screen.dart | 24 +- lib/screens/media_detail_screen.dart | 122 ++-------- .../catalog/anilist_catalog_source.dart | 5 +- lib/services/catalog/plex_catalog_source.dart | 57 ++--- .../catalog/simkl_catalog_source.dart | 7 +- lib/services/jellyfin_mappers.dart | 32 +++ lib/services/plex_mappers.dart | 112 ++++++++- lib/services/plex_mappers.g.dart | 12 + lib/utils/rating_utils.dart | 71 +++--- lib/widgets/media_rating_badge.dart | 216 +++++++++++------- lib/widgets/tv_spotlight_background.dart | 6 +- test/media/media_item_test.dart | 26 ++- test/models/catalog_item_test.dart | 23 ++ .../catalog_item_detail_screen_test.dart | 19 +- test/screens/media_detail_screen_test.dart | 23 +- .../catalog/plex_catalog_source_test.dart | 3 +- test/services/jellyfin_mappers_test.dart | 38 +++ test/services/plex_mappers_test.dart | 66 +++++- .../plex_rating_payload_shapes_test.dart | 50 ++++ test/utils/rating_utils_test.dart | 111 ++++----- test/widgets/tv_spotlight_ratings_test.dart | 134 +++++++++++ 50 files changed, 1546 insertions(+), 1068 deletions(-) create mode 100644 lib/media/media_rating.dart create mode 100644 test/services/plex_rating_payload_shapes_test.dart create mode 100644 test/widgets/tv_spotlight_ratings_test.dart diff --git a/lib/i18n/az.i18n.json b/lib/i18n/az.i18n.json index 09e597e4..ee4c29c2 100644 --- a/lib/i18n/az.i18n.json +++ b/lib/i18n/az.i18n.json @@ -64,7 +64,20 @@ "loading": "Yüklənir...", "fullscreen": "Tam ekran", "exitFullscreen": "Tam ekrandan çıx", - "pressBackAgainToExit": "Çıxmaq üçün geri düyməsinə bir daha basın" + "pressBackAgainToExit": "Çıxmaq üçün geri düyməsinə bir daha basın", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Lisenziyalar", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 4aa99bbf..d284b586 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -64,7 +64,20 @@ "loading": "Зареждане...", "fullscreen": "На цял екран", "exitFullscreen": "Изход от цял екран", - "pressBackAgainToExit": "Натиснете Назад отново, за да излезете" + "pressBackAgainToExit": "Натиснете Назад отново, за да излезете", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Лицензи", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 7b4bd25b..2a01d5a4 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -64,7 +64,20 @@ "loading": "Indlæser...", "fullscreen": "Fuldskærm", "exitFullscreen": "Forlad fuldskærm", - "pressBackAgainToExit": "Tryk på tilbage igen for at afslutte" + "pressBackAgainToExit": "Tryk på tilbage igen for at afslutte", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licenser", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 086d35ac..ba8a06fe 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -64,7 +64,20 @@ "loading": "Wird geladen …", "fullscreen": "Vollbild", "exitFullscreen": "Vollbild verlassen", - "pressBackAgainToExit": "Zum Beenden erneut Zurück drücken" + "pressBackAgainToExit": "Zum Beenden erneut Zurück drücken", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Lizenzen", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index babe1463..d2ad04f3 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -64,7 +64,20 @@ "loading": "Loading...", "fullscreen": "Fullscreen", "exitFullscreen": "Exit fullscreen", - "pressBackAgainToExit": "Press back again to exit" + "pressBackAgainToExit": "Press back again to exit", + "ratingSource": { + "critic": "Critics", + "audience": "Audience", + "imdb": "IMDb", + "tmdb": "TMDB", + "rottenTomatoes": "Rotten Tomatoes", + "simkl": "Simkl", + "mal": "MyAnimeList", + "anilist": "AniList", + "trakt": "Trakt", + "rottenTomatoesCritic": "Rotten Tomatoes critics", + "rottenTomatoesAudience": "Rotten Tomatoes audience" + } }, "screens": { "licenses": "Licenses", @@ -1134,18 +1147,16 @@ "creator": "Creator", "composer": "Composer" }, - "ratingSource": { - "critic": "Critics", - "audience": "Audience", - "imdb": "IMDb", - "tmdb": "TMDB", - "rottenTomatoes": "Rotten Tomatoes", - "simkl": "Simkl", - "mal": "MyAnimeList", - "anilist": "AniList", - "trakt": "Trakt", - "rottenTomatoesCritic": "Rotten Tomatoes critics", - "rottenTomatoesAudience": "Rotten Tomatoes audience" + "relation": { + "prequel": "Prequel", + "sequel": "Sequel", + "sideStory": "Side story", + "spinOff": "Spin-off", + "alternativeVersion": "Alternative version", + "summary": "Summary", + "parentStory": "Parent story", + "adaptation": "Adaptation", + "other": "Related" }, "broadcast": "Airs ${day} at ${time}", "broadcastWithZone": "Airs ${day} at ${time} ${timezone}", @@ -1186,17 +1197,6 @@ "totalResults": { "one": "${n} result", "other": "${n} results" - }, - "relation": { - "prequel": "Prequel", - "sequel": "Sequel", - "sideStory": "Side story", - "spinOff": "Spin-off", - "alternativeVersion": "Alternative version", - "summary": "Summary", - "parentStory": "Parent story", - "adaptation": "Adaptation", - "other": "Related" } }, "liveTv": { diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 4dbe2ba7..433015ea 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -64,7 +64,20 @@ "loading": "Cargando...", "fullscreen": "Pantalla completa", "exitFullscreen": "Salir de pantalla completa", - "pressBackAgainToExit": "Pulsa Atrás de nuevo para salir" + "pressBackAgainToExit": "Pulsa Atrás de nuevo para salir", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licencias", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 00f0024d..257f12e9 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -64,7 +64,20 @@ "loading": "Chargement...", "fullscreen": "Plein écran", "exitFullscreen": "Quitter le plein écran", - "pressBackAgainToExit": "Appuyez à nouveau sur retour pour quitter" + "pressBackAgainToExit": "Appuyez à nouveau sur retour pour quitter", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licences", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index a4900aa4..b5dbb015 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -64,7 +64,20 @@ "loading": "Betöltés...", "fullscreen": "Teljes képernyő", "exitFullscreen": "Kilépés a teljes képernyőből", - "pressBackAgainToExit": "A kilépéshez nyomd meg újra a Vissza gombot" + "pressBackAgainToExit": "A kilépéshez nyomd meg újra a Vissza gombot", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licencek", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 9257001a..167c8ed5 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -64,7 +64,20 @@ "loading": "Caricamento...", "fullscreen": "Schermo intero", "exitFullscreen": "Esci dalla modalità a schermo intero", - "pressBackAgainToExit": "Premi di nuovo Indietro per uscire" + "pressBackAgainToExit": "Premi di nuovo Indietro per uscire", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licenze", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 4666d3de..1c603718 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -64,7 +64,20 @@ "loading": "読み込み中…", "fullscreen": "フルスクリーン", "exitFullscreen": "フルスクリーンを終了", - "pressBackAgainToExit": "もう一度押すと終了します" + "pressBackAgainToExit": "もう一度押すと終了します", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "ライセンス", @@ -987,6 +1000,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1129,18 +1144,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1179,17 +1192,6 @@ }, "totalResults": { "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/kk.i18n.json b/lib/i18n/kk.i18n.json index c34044a5..2e37d939 100644 --- a/lib/i18n/kk.i18n.json +++ b/lib/i18n/kk.i18n.json @@ -64,7 +64,20 @@ "loading": "Жүктелуде...", "fullscreen": "Толық экран", "exitFullscreen": "Толық экраннан шығу", - "pressBackAgainToExit": "Шығу үшін артқа түймесін қайтадан басыңыз" + "pressBackAgainToExit": "Шығу үшін артқа түймесін қайтадан басыңыз", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Лицензиялар", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 4dc0a1ab..ba597532 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -64,7 +64,20 @@ "loading": "로딩 중...", "fullscreen": "전체 화면", "exitFullscreen": "전체 화면 종료", - "pressBackAgainToExit": "한 번 더 누르면 종료됩니다" + "pressBackAgainToExit": "한 번 더 누르면 종료됩니다", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "라이선스", @@ -987,6 +1000,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1129,18 +1144,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1179,17 +1192,6 @@ }, "totalResults": { "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index f690cfee..377eee6f 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -64,7 +64,20 @@ "loading": "Laster...", "fullscreen": "Fullskjerm", "exitFullscreen": "Avslutt fullskjerm", - "pressBackAgainToExit": "Trykk på Tilbake en gang til for å avslutte" + "pressBackAgainToExit": "Trykk på Tilbake en gang til for å avslutte", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Lisenser", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 2e8c4aae..cace869a 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -64,7 +64,20 @@ "loading": "Laden...", "fullscreen": "Volledig scherm", "exitFullscreen": "Volledig scherm verlaten", - "pressBackAgainToExit": "Druk nogmaals op terug om af te sluiten" + "pressBackAgainToExit": "Druk nogmaals op terug om af te sluiten", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licenties", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index bacf2f9a..eddf2fc9 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -64,7 +64,20 @@ "loading": "Ładowanie...", "fullscreen": "Pełny ekran", "exitFullscreen": "Wyjdź z pełnego ekranu", - "pressBackAgainToExit": "Naciśnij ponownie przycisk Wstecz, aby wyjść" + "pressBackAgainToExit": "Naciśnij ponownie przycisk Wstecz, aby wyjść", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licencje", @@ -990,6 +1003,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1138,18 +1153,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1194,17 +1207,6 @@ "few": "", "many": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index a29462f4..ca2da9d3 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -64,7 +64,20 @@ "loading": "Carregando...", "fullscreen": "Tela cheia", "exitFullscreen": "Sair da tela cheia", - "pressBackAgainToExit": "Pressione voltar novamente para sair" + "pressBackAgainToExit": "Pressione voltar novamente para sair", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licenças", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 714528d2..0bd332b7 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -64,7 +64,20 @@ "loading": "Загрузка...", "fullscreen": "Полноэкранный режим", "exitFullscreen": "Выйти из полноэкранного режима", - "pressBackAgainToExit": "Нажмите ещё раз для выхода" + "pressBackAgainToExit": "Нажмите ещё раз для выхода", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Лицензии", @@ -990,6 +1003,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1138,18 +1153,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1194,17 +1207,6 @@ "few": "", "many": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index e2ad47b0..af39dd55 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -300,6 +300,8 @@ class Translations$common$en { /// en: 'Press back again to exit' String get pressBackAgainToExit => 'Press back again to exit'; + + late final Translations$common$ratingSource$en ratingSource = Translations$common$ratingSource$en.internal(_root); } // Path: screens @@ -3040,7 +3042,7 @@ class Translations$explore$en { late final Translations$explore$format$en format = Translations$explore$format$en.internal(_root); late final Translations$explore$sourceMaterial$en sourceMaterial = Translations$explore$sourceMaterial$en.internal(_root); late final Translations$explore$creditRole$en creditRole = Translations$explore$creditRole$en.internal(_root); - late final Translations$explore$ratingSource$en ratingSource = Translations$explore$ratingSource$en.internal(_root); + late final Translations$explore$relation$en relation = Translations$explore$relation$en.internal(_root); /// en: 'Airs ${day} at ${time}' String broadcast({required Object day, required Object time}) => 'Airs ${day} at ${time}'; @@ -3055,8 +3057,6 @@ class Translations$explore$en { one: '${n} result', other: '${n} results', ); - - late final Translations$explore$relation$en relation = Translations$explore$relation$en.internal(_root); } // Path: liveTv @@ -4820,6 +4820,48 @@ class Translations$addServer$en { String get borrowFromAnotherProfileSubtitle => 'Reuse another profile\'s connection. PIN-protected profiles require a PIN.'; } +// Path: common.ratingSource +class Translations$common$ratingSource$en { + Translations$common$ratingSource$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Critics' + String get critic => 'Critics'; + + /// en: 'Audience' + String get audience => 'Audience'; + + /// en: 'IMDb' + String get imdb => 'IMDb'; + + /// en: 'TMDB' + String get tmdb => 'TMDB'; + + /// en: 'Rotten Tomatoes' + String get rottenTomatoes => 'Rotten Tomatoes'; + + /// en: 'Simkl' + String get simkl => 'Simkl'; + + /// en: 'MyAnimeList' + String get mal => 'MyAnimeList'; + + /// en: 'AniList' + String get anilist => 'AniList'; + + /// en: 'Trakt' + String get trakt => 'Trakt'; + + /// en: 'Rotten Tomatoes critics' + String get rottenTomatoesCritic => 'Rotten Tomatoes critics'; + + /// en: 'Rotten Tomatoes audience' + String get rottenTomatoesAudience => 'Rotten Tomatoes audience'; +} + // Path: hotkeys.actions class Translations$hotkeys$actions$en { Translations$hotkeys$actions$en.internal(this._root); @@ -5414,46 +5456,40 @@ class Translations$explore$creditRole$en { String get composer => 'Composer'; } -// Path: explore.ratingSource -class Translations$explore$ratingSource$en { - Translations$explore$ratingSource$en.internal(this._root); +// Path: explore.relation +class Translations$explore$relation$en { + Translations$explore$relation$en.internal(this._root); final Translations _root; // ignore: unused_field // Translations - /// en: 'Critics' - String get critic => 'Critics'; + /// en: 'Prequel' + String get prequel => 'Prequel'; - /// en: 'Audience' - String get audience => 'Audience'; + /// en: 'Sequel' + String get sequel => 'Sequel'; - /// en: 'IMDb' - String get imdb => 'IMDb'; + /// en: 'Side story' + String get sideStory => 'Side story'; - /// en: 'TMDB' - String get tmdb => 'TMDB'; + /// en: 'Spin-off' + String get spinOff => 'Spin-off'; - /// en: 'Rotten Tomatoes' - String get rottenTomatoes => 'Rotten Tomatoes'; + /// en: 'Alternative version' + String get alternativeVersion => 'Alternative version'; - /// en: 'Simkl' - String get simkl => 'Simkl'; + /// en: 'Summary' + String get summary => 'Summary'; - /// en: 'MyAnimeList' - String get mal => 'MyAnimeList'; + /// en: 'Parent story' + String get parentStory => 'Parent story'; - /// en: 'AniList' - String get anilist => 'AniList'; + /// en: 'Adaptation' + String get adaptation => 'Adaptation'; - /// en: 'Trakt' - String get trakt => 'Trakt'; - - /// en: 'Rotten Tomatoes critics' - String get rottenTomatoesCritic => 'Rotten Tomatoes critics'; - - /// en: 'Rotten Tomatoes audience' - String get rottenTomatoesAudience => 'Rotten Tomatoes audience'; + /// en: 'Related' + String get other => 'Related'; } // Path: explore.detail @@ -5555,42 +5591,6 @@ class Translations$explore$detail$en { String get background => 'Background'; } -// Path: explore.relation -class Translations$explore$relation$en { - Translations$explore$relation$en.internal(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Prequel' - String get prequel => 'Prequel'; - - /// en: 'Sequel' - String get sequel => 'Sequel'; - - /// en: 'Side story' - String get sideStory => 'Side story'; - - /// en: 'Spin-off' - String get spinOff => 'Spin-off'; - - /// en: 'Alternative version' - String get alternativeVersion => 'Alternative version'; - - /// en: 'Summary' - String get summary => 'Summary'; - - /// en: 'Parent story' - String get parentStory => 'Parent story'; - - /// en: 'Adaptation' - String get adaptation => 'Adaptation'; - - /// en: 'Related' - String get other => 'Related'; -} - // Path: downloads.backgroundWarning class Translations$downloads$backgroundWarning$en { Translations$downloads$backgroundWarning$en.internal(this._root); @@ -6083,6 +6083,17 @@ extension on Translations { 'common.fullscreen' => 'Fullscreen', 'common.exitFullscreen' => 'Exit fullscreen', 'common.pressBackAgainToExit' => 'Press back again to exit', + 'common.ratingSource.critic' => 'Critics', + 'common.ratingSource.audience' => 'Audience', + 'common.ratingSource.imdb' => 'IMDb', + 'common.ratingSource.tmdb' => 'TMDB', + 'common.ratingSource.rottenTomatoes' => 'Rotten Tomatoes', + 'common.ratingSource.simkl' => 'Simkl', + 'common.ratingSource.mal' => 'MyAnimeList', + 'common.ratingSource.anilist' => 'AniList', + 'common.ratingSource.trakt' => 'Trakt', + 'common.ratingSource.rottenTomatoesCritic' => 'Rotten Tomatoes critics', + 'common.ratingSource.rottenTomatoesAudience' => 'Rotten Tomatoes audience', 'screens.licenses' => 'Licenses', 'screens.switchProfile' => 'Switch Profile', 'screens.subtitleStyling' => 'Subtitle Styling', @@ -6523,6 +6534,8 @@ extension on Translations { 'rateSheet.notAvailable' => 'No match found', 'rateSheet.noConnectedServices' => 'Connect a service in Settings to rate there.', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie', + _ => null, + } ?? switch (path) { 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', 'accessibility.mediaCardSeason' => ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}', @@ -6534,8 +6547,6 @@ extension on Translations { 'accessibility.increase' => 'Increase', 'accessibility.decreaseValue' => ({required Object label}) => 'Decrease ${label}', 'accessibility.increaseValue' => ({required Object label}) => 'Increase ${label}', - _ => null, - } ?? switch (path) { 'accessibility.hue' => 'Hue', 'accessibility.saturation' => 'Saturation', 'accessibility.brightness' => 'Brightness', @@ -7037,6 +7048,8 @@ extension on Translations { 'explore.season.fall' => 'Fall', 'explore.season.withYear' => ({required Object season, required Object year}) => '${season} ${year}', 'explore.format.tv' => 'TV', + _ => null, + } ?? switch (path) { 'explore.format.tvShort' => 'TV Short', 'explore.format.movie' => 'Movie', 'explore.format.special' => 'Special', @@ -7048,8 +7061,6 @@ extension on Translations { 'explore.sourceMaterial.manga' => 'Manga', 'explore.sourceMaterial.lightNovel' => 'Light novel', 'explore.sourceMaterial.novel' => 'Novel', - _ => null, - } ?? switch (path) { 'explore.sourceMaterial.visualNovel' => 'Visual novel', 'explore.sourceMaterial.game' => 'Game', 'explore.sourceMaterial.webComic' => 'Web comic', @@ -7060,17 +7071,15 @@ extension on Translations { 'explore.creditRole.producer' => 'Producer', 'explore.creditRole.creator' => 'Creator', 'explore.creditRole.composer' => 'Composer', - 'explore.ratingSource.critic' => 'Critics', - 'explore.ratingSource.audience' => 'Audience', - 'explore.ratingSource.imdb' => 'IMDb', - 'explore.ratingSource.tmdb' => 'TMDB', - 'explore.ratingSource.rottenTomatoes' => 'Rotten Tomatoes', - 'explore.ratingSource.simkl' => 'Simkl', - 'explore.ratingSource.mal' => 'MyAnimeList', - 'explore.ratingSource.anilist' => 'AniList', - 'explore.ratingSource.trakt' => 'Trakt', - 'explore.ratingSource.rottenTomatoesCritic' => 'Rotten Tomatoes critics', - 'explore.ratingSource.rottenTomatoesAudience' => 'Rotten Tomatoes audience', + 'explore.relation.prequel' => 'Prequel', + 'explore.relation.sequel' => 'Sequel', + 'explore.relation.sideStory' => 'Side story', + 'explore.relation.spinOff' => 'Spin-off', + 'explore.relation.alternativeVersion' => 'Alternative version', + 'explore.relation.summary' => 'Summary', + 'explore.relation.parentStory' => 'Parent story', + 'explore.relation.adaptation' => 'Adaptation', + 'explore.relation.other' => 'Related', 'explore.broadcast' => ({required Object day, required Object time}) => 'Airs ${day} at ${time}', 'explore.broadcastWithZone' => ({required Object day, required Object time, required Object timezone}) => 'Airs ${day} at ${time} ${timezone}', 'explore.detail.originalTitle' => 'Original title', @@ -7103,15 +7112,6 @@ extension on Translations { 'explore.detail.relatedTitles' => 'Related titles', 'explore.detail.background' => 'Background', 'explore.totalResults' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} result', other: '${n} results', ), - 'explore.relation.prequel' => 'Prequel', - 'explore.relation.sequel' => 'Sequel', - 'explore.relation.sideStory' => 'Side story', - 'explore.relation.spinOff' => 'Spin-off', - 'explore.relation.alternativeVersion' => 'Alternative version', - 'explore.relation.summary' => 'Summary', - 'explore.relation.parentStory' => 'Parent story', - 'explore.relation.adaptation' => 'Adaptation', - 'explore.relation.other' => 'Related', 'liveTv.title' => 'Live TV', 'liveTv.guide' => 'Guide', 'liveTv.noChannels' => 'No channels available', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 7feb36b7..a7b8a69d 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -64,7 +64,20 @@ "loading": "Laddar...", "fullscreen": "Helskärm", "exitFullscreen": "Avsluta helskärm", - "pressBackAgainToExit": "Tryck bakåt igen för att avsluta" + "pressBackAgainToExit": "Tryck bakåt igen för att avsluta", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Licenser", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/tr.i18n.json b/lib/i18n/tr.i18n.json index bfb16b50..f0f76d80 100644 --- a/lib/i18n/tr.i18n.json +++ b/lib/i18n/tr.i18n.json @@ -64,7 +64,20 @@ "loading": "Yükleniyor...", "fullscreen": "Tam Ekran", "exitFullscreen": "Tam Ekrandan Çık", - "pressBackAgainToExit": "Çıkmak için tekrar geri tuşuna basın" + "pressBackAgainToExit": "Çıkmak için tekrar geri tuşuna basın", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Lisanslar", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/uz.i18n.json b/lib/i18n/uz.i18n.json index e469286e..5579b6e2 100644 --- a/lib/i18n/uz.i18n.json +++ b/lib/i18n/uz.i18n.json @@ -64,7 +64,20 @@ "loading": "Yuklanmoqda...", "fullscreen": "Toʻliq ekran", "exitFullscreen": "Toʻliq ekrandan chiqish", - "pressBackAgainToExit": "Chiqish uchun orqaga tugmasini yana bir bor bosing" + "pressBackAgainToExit": "Chiqish uchun orqaga tugmasini yana bir bor bosing", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "Litsenziyalar", @@ -988,6 +1001,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1132,18 +1147,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1184,17 +1197,6 @@ "totalResults": { "one": "", "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 43b23719..af91c149 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -64,7 +64,20 @@ "loading": "載入中…", "fullscreen": "全螢幕", "exitFullscreen": "退出全螢幕", - "pressBackAgainToExit": "再按一次返回以退出" + "pressBackAgainToExit": "再按一次返回以退出", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "授權條款", @@ -987,6 +1000,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1129,18 +1144,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1179,17 +1192,6 @@ }, "totalResults": { "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 8d0225ad..babe0ba5 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -64,7 +64,20 @@ "loading": "加载中…", "fullscreen": "全屏", "exitFullscreen": "退出全屏", - "pressBackAgainToExit": "再按一次返回键退出" + "pressBackAgainToExit": "再按一次返回键退出", + "ratingSource": { + "critic": "", + "audience": "", + "imdb": "", + "tmdb": "", + "rottenTomatoes": "", + "simkl": "", + "mal": "", + "anilist": "", + "trakt": "", + "rottenTomatoesCritic": "", + "rottenTomatoesAudience": "" + } }, "screens": { "licenses": "许可证", @@ -987,6 +1000,8 @@ "repairConfirm": "", "repairSucceeded": "", "repairNeedsRestart": "", + "restartRequiredBody": "", + "quitPlezy": "", "repairFailed": "", "repairKeptSignIns": "", "repairLostSignIns": "", @@ -1129,18 +1144,16 @@ "creator": "", "composer": "" }, - "ratingSource": { - "critic": "", - "audience": "", - "imdb": "", - "tmdb": "", - "rottenTomatoes": "", - "simkl": "", - "mal": "", - "anilist": "", - "trakt": "", - "rottenTomatoesCritic": "", - "rottenTomatoesAudience": "" + "relation": { + "prequel": "", + "sequel": "", + "sideStory": "", + "spinOff": "", + "alternativeVersion": "", + "summary": "", + "parentStory": "", + "adaptation": "", + "other": "" }, "broadcast": "", "broadcastWithZone": "", @@ -1179,17 +1192,6 @@ }, "totalResults": { "other": "" - }, - "relation": { - "prequel": "", - "sequel": "", - "sideStory": "", - "spinOff": "", - "alternativeVersion": "", - "summary": "", - "parentStory": "", - "adaptation": "", - "other": "" } }, "liveTv": { diff --git a/lib/media/media_item.dart b/lib/media/media_item.dart index 4ff0ba92..bf876f8f 100644 --- a/lib/media/media_item.dart +++ b/lib/media/media_item.dart @@ -10,6 +10,7 @@ import 'media_backend.dart'; import 'media_kind.dart'; import 'media_role.dart'; import 'media_version.dart'; +import 'media_rating.dart'; part 'media_item.freezed.dart'; part 'media_item.g.dart'; @@ -66,6 +67,7 @@ sealed class MediaItem with _$MediaItem { int? addedAt, int? updatedAt, double? rating, + List? ratings, double? userRating, bool? isFavorite, List? genres, @@ -128,6 +130,7 @@ sealed class MediaItem with _$MediaItem { addedAt: addedAt, updatedAt: updatedAt, rating: rating, + ratings: ratings, userRating: userRating, isFavorite: isFavorite, genres: genres, @@ -189,6 +192,7 @@ sealed class MediaItem with _$MediaItem { addedAt: addedAt, updatedAt: updatedAt, rating: rating, + ratings: ratings, userRating: userRating, isFavorite: isFavorite, genres: genres, @@ -257,11 +261,14 @@ sealed class MediaItem with _$MediaItem { @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, - @JsonKey(fromJson: flexibleDouble) double? audienceRating, @JsonKey(fromJson: flexibleDouble) double? userRating, + + /// Every attributed score the response carried, headline first. Plex + /// listings yield one or two (the `rating`/`audienceRating` pair with + /// their source images); `/library/metadata/{id}` adds the `Rating[]` + /// array, so IMDb and TMDB join Rotten Tomatoes on detail screens. + @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, - String? ratingImage, - String? audienceRatingImage, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @@ -335,6 +342,10 @@ sealed class MediaItem with _$MediaItem { @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, + + /// `CommunityRating` and the `CriticRating` Tomatometer, in that order. + /// Jellyfin exposes no per-source array, so this is at most two entries. + @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @@ -749,4 +760,13 @@ List? _mediaItemVersionsFromJson(Object? raw) { : null; } +List? _mediaItemRatingsFromJson(Object? raw) { + return raw is List + ? [ + for (final rating in raw) + if (rating is Map) ?MediaRatingSource.fromJson(rating), + ] + : null; +} + Map? _mediaItemRawFromJson(Object? raw) => raw is Map ? Map.from(raw) : null; diff --git a/lib/media/media_item.freezed.dart b/lib/media/media_item.freezed.dart index ee9b951a..d6c15fcc 100644 --- a/lib/media/media_item.freezed.dart +++ b/lib/media/media_item.freezed.dart @@ -14,7 +14,11 @@ T _$identity(T value) => value; /// @nodoc mixin _$MediaItem { -@JsonKey(readValue: readStringField, defaultValue: '') String get id;@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind get kind; String? get guid; String? get title; String? get titleSort; String? get summary; String? get tagline; String? get originalTitle; String? get studio;@JsonKey(fromJson: flexibleInt) int? get year; String? get originallyAvailableAt; String? get contentRating; String? get parentId; String? get parentTitle; String? get parentThumbPath;@JsonKey(fromJson: flexibleInt) int? get parentIndex;@JsonKey(fromJson: flexibleInt) int? get index; String? get grandparentId; String? get grandparentTitle; String? get grandparentThumbPath; String? get grandparentArtPath; List? get grandparentBackdropPaths; String? get thumbPath; String? get artPath; List? get backdropPaths; String? get clearLogoPath; String? get backgroundSquarePath;@JsonKey(fromJson: flexibleInt) int? get durationMs;@JsonKey(fromJson: flexibleInt) int? get viewOffsetMs;@JsonKey(fromJson: flexibleInt) int? get viewCount;@JsonKey(fromJson: flexibleInt) int? get lastViewedAt;@JsonKey(fromJson: flexibleInt) int? get leafCount;@JsonKey(fromJson: flexibleInt) int? get viewedLeafCount;@JsonKey(fromJson: flexibleInt) int? get childCount;@JsonKey(fromJson: flexibleInt) int? get addedAt;@JsonKey(fromJson: flexibleInt) int? get updatedAt;@JsonKey(fromJson: flexibleDouble) double? get rating;@JsonKey(fromJson: flexibleDouble) double? get userRating; bool? get isFavorite;@JsonKey(fromJson: _mediaItemStringList) List? get genres;@JsonKey(fromJson: _mediaItemStringList) List? get directors;@JsonKey(fromJson: _mediaItemStringList) List? get writers;@JsonKey(fromJson: _mediaItemStringList) List? get producers;@JsonKey(fromJson: _mediaItemStringList) List? get countries;@JsonKey(fromJson: _mediaItemStringList) List? get collections;@JsonKey(fromJson: _mediaItemStringList) List? get labels;@JsonKey(fromJson: _mediaItemStringList) List? get styles;@JsonKey(fromJson: _mediaItemStringList) List? get moods;@JsonKey(fromJson: _mediaItemRolesFromJson) List? get roles;@JsonKey(fromJson: _mediaItemVersionsFromJson) List? get mediaVersions; String? get libraryId; String? get libraryTitle; String? get audioLanguage;/// Jellyfin playlist entry id used by playlist write endpoints. +@JsonKey(readValue: readStringField, defaultValue: '') String get id;@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind get kind; String? get guid; String? get title; String? get titleSort; String? get summary; String? get tagline; String? get originalTitle; String? get studio;@JsonKey(fromJson: flexibleInt) int? get year; String? get originallyAvailableAt; String? get contentRating; String? get parentId; String? get parentTitle; String? get parentThumbPath;@JsonKey(fromJson: flexibleInt) int? get parentIndex;@JsonKey(fromJson: flexibleInt) int? get index; String? get grandparentId; String? get grandparentTitle; String? get grandparentThumbPath; String? get grandparentArtPath; List? get grandparentBackdropPaths; String? get thumbPath; String? get artPath; List? get backdropPaths; String? get clearLogoPath; String? get backgroundSquarePath;@JsonKey(fromJson: flexibleInt) int? get durationMs;@JsonKey(fromJson: flexibleInt) int? get viewOffsetMs;@JsonKey(fromJson: flexibleInt) int? get viewCount;@JsonKey(fromJson: flexibleInt) int? get lastViewedAt;@JsonKey(fromJson: flexibleInt) int? get leafCount;@JsonKey(fromJson: flexibleInt) int? get viewedLeafCount;@JsonKey(fromJson: flexibleInt) int? get childCount;@JsonKey(fromJson: flexibleInt) int? get addedAt;@JsonKey(fromJson: flexibleInt) int? get updatedAt;@JsonKey(fromJson: flexibleDouble) double? get rating;@JsonKey(fromJson: flexibleDouble) double? get userRating;/// Every attributed score the response carried, headline first. Plex +/// listings yield one or two (the `rating`/`audienceRating` pair with +/// their source images); `/library/metadata/{id}` adds the `Rating[]` +/// array, so IMDb and TMDB join Rotten Tomatoes on detail screens. +@JsonKey(fromJson: _mediaItemRatingsFromJson) List? get ratings; bool? get isFavorite;@JsonKey(fromJson: _mediaItemStringList) List? get genres;@JsonKey(fromJson: _mediaItemStringList) List? get directors;@JsonKey(fromJson: _mediaItemStringList) List? get writers;@JsonKey(fromJson: _mediaItemStringList) List? get producers;@JsonKey(fromJson: _mediaItemStringList) List? get countries;@JsonKey(fromJson: _mediaItemStringList) List? get collections;@JsonKey(fromJson: _mediaItemStringList) List? get labels;@JsonKey(fromJson: _mediaItemStringList) List? get styles;@JsonKey(fromJson: _mediaItemStringList) List? get moods;@JsonKey(fromJson: _mediaItemRolesFromJson) List? get roles;@JsonKey(fromJson: _mediaItemVersionsFromJson) List? get mediaVersions; String? get libraryId; String? get libraryTitle; String? get audioLanguage;/// Jellyfin playlist entry id used by playlist write endpoints. @JsonKey(fromJson: flexibleInt) Object? get playlistItemId; String? get serverId; String? get serverName;/// Relative folder key (`/library/sections/{id}/folder?parent=…`) for /// [MediaKind.folder] rows — what [MediaServerClient.fetchFolderChildren] /// tunes into. Stamped by the folder fetchers, null elsewhere. @@ -31,7 +35,7 @@ $MediaItemCopyWith get copyWith => _$MediaItemCopyWithImpl @override String toString() { - return 'MediaItem(id: $id, kind: $kind, guid: $guid, title: $title, titleSort: $titleSort, summary: $summary, tagline: $tagline, originalTitle: $originalTitle, studio: $studio, year: $year, originallyAvailableAt: $originallyAvailableAt, contentRating: $contentRating, parentId: $parentId, parentTitle: $parentTitle, parentThumbPath: $parentThumbPath, parentIndex: $parentIndex, index: $index, grandparentId: $grandparentId, grandparentTitle: $grandparentTitle, grandparentThumbPath: $grandparentThumbPath, grandparentArtPath: $grandparentArtPath, grandparentBackdropPaths: $grandparentBackdropPaths, thumbPath: $thumbPath, artPath: $artPath, backdropPaths: $backdropPaths, clearLogoPath: $clearLogoPath, backgroundSquarePath: $backgroundSquarePath, durationMs: $durationMs, viewOffsetMs: $viewOffsetMs, viewCount: $viewCount, lastViewedAt: $lastViewedAt, leafCount: $leafCount, viewedLeafCount: $viewedLeafCount, childCount: $childCount, addedAt: $addedAt, updatedAt: $updatedAt, rating: $rating, userRating: $userRating, isFavorite: $isFavorite, genres: $genres, directors: $directors, writers: $writers, producers: $producers, countries: $countries, collections: $collections, labels: $labels, styles: $styles, moods: $moods, roles: $roles, mediaVersions: $mediaVersions, libraryId: $libraryId, libraryTitle: $libraryTitle, audioLanguage: $audioLanguage, playlistItemId: $playlistItemId, serverId: $serverId, serverName: $serverName, backendFolderKey: $backendFolderKey, raw: $raw)'; + return 'MediaItem(id: $id, kind: $kind, guid: $guid, title: $title, titleSort: $titleSort, summary: $summary, tagline: $tagline, originalTitle: $originalTitle, studio: $studio, year: $year, originallyAvailableAt: $originallyAvailableAt, contentRating: $contentRating, parentId: $parentId, parentTitle: $parentTitle, parentThumbPath: $parentThumbPath, parentIndex: $parentIndex, index: $index, grandparentId: $grandparentId, grandparentTitle: $grandparentTitle, grandparentThumbPath: $grandparentThumbPath, grandparentArtPath: $grandparentArtPath, grandparentBackdropPaths: $grandparentBackdropPaths, thumbPath: $thumbPath, artPath: $artPath, backdropPaths: $backdropPaths, clearLogoPath: $clearLogoPath, backgroundSquarePath: $backgroundSquarePath, durationMs: $durationMs, viewOffsetMs: $viewOffsetMs, viewCount: $viewCount, lastViewedAt: $lastViewedAt, leafCount: $leafCount, viewedLeafCount: $viewedLeafCount, childCount: $childCount, addedAt: $addedAt, updatedAt: $updatedAt, rating: $rating, userRating: $userRating, ratings: $ratings, isFavorite: $isFavorite, genres: $genres, directors: $directors, writers: $writers, producers: $producers, countries: $countries, collections: $collections, labels: $labels, styles: $styles, moods: $moods, roles: $roles, mediaVersions: $mediaVersions, libraryId: $libraryId, libraryTitle: $libraryTitle, audioLanguage: $audioLanguage, playlistItemId: $playlistItemId, serverId: $serverId, serverName: $serverName, backendFolderKey: $backendFolderKey, raw: $raw)'; } @@ -42,7 +46,7 @@ abstract mixin class $MediaItemCopyWith<$Res> { factory $MediaItemCopyWith(MediaItem value, $Res Function(MediaItem) _then) = _$MediaItemCopyWithImpl; @useResult $Res call({ -@JsonKey(readValue: readStringField, defaultValue: '') String id,@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio,@JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath,@JsonKey(fromJson: flexibleInt) int? parentIndex,@JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath,@JsonKey(fromJson: flexibleInt) int? durationMs,@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,@JsonKey(fromJson: flexibleInt) int? viewCount,@JsonKey(fromJson: flexibleInt) int? lastViewedAt,@JsonKey(fromJson: flexibleInt) int? leafCount,@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,@JsonKey(fromJson: flexibleInt) int? childCount,@JsonKey(fromJson: flexibleInt) int? addedAt,@JsonKey(fromJson: flexibleInt) int? updatedAt,@JsonKey(fromJson: flexibleDouble) double? rating,@JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite,@JsonKey(fromJson: _mediaItemStringList) List? genres,@JsonKey(fromJson: _mediaItemStringList) List? directors,@JsonKey(fromJson: _mediaItemStringList) List? writers,@JsonKey(fromJson: _mediaItemStringList) List? producers,@JsonKey(fromJson: _mediaItemStringList) List? countries,@JsonKey(fromJson: _mediaItemStringList) List? collections,@JsonKey(fromJson: _mediaItemStringList) List? labels,@JsonKey(fromJson: _mediaItemStringList) List? styles,@JsonKey(fromJson: _mediaItemStringList) List? moods,@JsonKey(fromJson: _mediaItemRolesFromJson) List? roles,@JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? serverId, String? serverName, String? backendFolderKey,@JsonKey(fromJson: _mediaItemRawFromJson) Map? raw +@JsonKey(readValue: readStringField, defaultValue: '') String id,@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio,@JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath,@JsonKey(fromJson: flexibleInt) int? parentIndex,@JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath,@JsonKey(fromJson: flexibleInt) int? durationMs,@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,@JsonKey(fromJson: flexibleInt) int? viewCount,@JsonKey(fromJson: flexibleInt) int? lastViewedAt,@JsonKey(fromJson: flexibleInt) int? leafCount,@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,@JsonKey(fromJson: flexibleInt) int? childCount,@JsonKey(fromJson: flexibleInt) int? addedAt,@JsonKey(fromJson: flexibleInt) int? updatedAt,@JsonKey(fromJson: flexibleDouble) double? rating,@JsonKey(fromJson: flexibleDouble) double? userRating,@JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite,@JsonKey(fromJson: _mediaItemStringList) List? genres,@JsonKey(fromJson: _mediaItemStringList) List? directors,@JsonKey(fromJson: _mediaItemStringList) List? writers,@JsonKey(fromJson: _mediaItemStringList) List? producers,@JsonKey(fromJson: _mediaItemStringList) List? countries,@JsonKey(fromJson: _mediaItemStringList) List? collections,@JsonKey(fromJson: _mediaItemStringList) List? labels,@JsonKey(fromJson: _mediaItemStringList) List? styles,@JsonKey(fromJson: _mediaItemStringList) List? moods,@JsonKey(fromJson: _mediaItemRolesFromJson) List? roles,@JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? serverId, String? serverName, String? backendFolderKey,@JsonKey(fromJson: _mediaItemRawFromJson) Map? raw }); @@ -59,7 +63,7 @@ class _$MediaItemCopyWithImpl<$Res> /// Create a copy of MediaItem /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? kind = null,Object? guid = freezed,Object? title = freezed,Object? titleSort = freezed,Object? summary = freezed,Object? tagline = freezed,Object? originalTitle = freezed,Object? studio = freezed,Object? year = freezed,Object? originallyAvailableAt = freezed,Object? contentRating = freezed,Object? parentId = freezed,Object? parentTitle = freezed,Object? parentThumbPath = freezed,Object? parentIndex = freezed,Object? index = freezed,Object? grandparentId = freezed,Object? grandparentTitle = freezed,Object? grandparentThumbPath = freezed,Object? grandparentArtPath = freezed,Object? grandparentBackdropPaths = freezed,Object? thumbPath = freezed,Object? artPath = freezed,Object? backdropPaths = freezed,Object? clearLogoPath = freezed,Object? backgroundSquarePath = freezed,Object? durationMs = freezed,Object? viewOffsetMs = freezed,Object? viewCount = freezed,Object? lastViewedAt = freezed,Object? leafCount = freezed,Object? viewedLeafCount = freezed,Object? childCount = freezed,Object? addedAt = freezed,Object? updatedAt = freezed,Object? rating = freezed,Object? userRating = freezed,Object? isFavorite = freezed,Object? genres = freezed,Object? directors = freezed,Object? writers = freezed,Object? producers = freezed,Object? countries = freezed,Object? collections = freezed,Object? labels = freezed,Object? styles = freezed,Object? moods = freezed,Object? roles = freezed,Object? mediaVersions = freezed,Object? libraryId = freezed,Object? libraryTitle = freezed,Object? audioLanguage = freezed,Object? serverId = freezed,Object? serverName = freezed,Object? backendFolderKey = freezed,Object? raw = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? kind = null,Object? guid = freezed,Object? title = freezed,Object? titleSort = freezed,Object? summary = freezed,Object? tagline = freezed,Object? originalTitle = freezed,Object? studio = freezed,Object? year = freezed,Object? originallyAvailableAt = freezed,Object? contentRating = freezed,Object? parentId = freezed,Object? parentTitle = freezed,Object? parentThumbPath = freezed,Object? parentIndex = freezed,Object? index = freezed,Object? grandparentId = freezed,Object? grandparentTitle = freezed,Object? grandparentThumbPath = freezed,Object? grandparentArtPath = freezed,Object? grandparentBackdropPaths = freezed,Object? thumbPath = freezed,Object? artPath = freezed,Object? backdropPaths = freezed,Object? clearLogoPath = freezed,Object? backgroundSquarePath = freezed,Object? durationMs = freezed,Object? viewOffsetMs = freezed,Object? viewCount = freezed,Object? lastViewedAt = freezed,Object? leafCount = freezed,Object? viewedLeafCount = freezed,Object? childCount = freezed,Object? addedAt = freezed,Object? updatedAt = freezed,Object? rating = freezed,Object? userRating = freezed,Object? ratings = freezed,Object? isFavorite = freezed,Object? genres = freezed,Object? directors = freezed,Object? writers = freezed,Object? producers = freezed,Object? countries = freezed,Object? collections = freezed,Object? labels = freezed,Object? styles = freezed,Object? moods = freezed,Object? roles = freezed,Object? mediaVersions = freezed,Object? libraryId = freezed,Object? libraryTitle = freezed,Object? audioLanguage = freezed,Object? serverId = freezed,Object? serverName = freezed,Object? backendFolderKey = freezed,Object? raw = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,kind: null == kind ? _self.kind : kind // ignore: cast_nullable_to_non_nullable @@ -99,7 +103,8 @@ as int?,addedAt: freezed == addedAt ? _self.addedAt : addedAt // ignore: cast_nu as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as int?,rating: freezed == rating ? _self.rating : rating // ignore: cast_nullable_to_non_nullable as double?,userRating: freezed == userRating ? _self.userRating : userRating // ignore: cast_nullable_to_non_nullable -as double?,isFavorite: freezed == isFavorite ? _self.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable +as double?,ratings: freezed == ratings ? _self.ratings : ratings // ignore: cast_nullable_to_non_nullable +as List?,isFavorite: freezed == isFavorite ? _self.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool?,genres: freezed == genres ? _self.genres : genres // ignore: cast_nullable_to_non_nullable as List?,directors: freezed == directors ? _self.directors : directors // ignore: cast_nullable_to_non_nullable as List?,writers: freezed == writers ? _self.writers : writers // ignore: cast_nullable_to_non_nullable @@ -203,11 +208,11 @@ return jellyfin(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? audienceRating, @JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, String? ratingImage, String? audienceRatingImage, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage, @JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey, @JsonKey(fromJson: flexibleInt) int? playlistItemId, @JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype, @JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? plex,TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? jellyfin,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage, @JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey, @JsonKey(fromJson: flexibleInt) int? playlistItemId, @JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype, @JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? plex,TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? jellyfin,required TResult orElse(),}) {final _that = this; switch (_that) { case PlexMediaItem() when plex != null: -return plex(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.editionTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.audienceRating,_that.userRating,_that.isFavorite,_that.ratingImage,_that.audienceRatingImage,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.subtitleLanguage,_that.subtitleMode,_that.trailerKey,_that.playlistItemId,_that.playQueueItemId,_that.subtype,_that.extraType,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case JellyfinMediaItem() when jellyfin != null: -return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.playlistItemId,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case _: +return plex(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.editionTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.ratings,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.subtitleLanguage,_that.subtitleMode,_that.trailerKey,_that.playlistItemId,_that.playQueueItemId,_that.subtype,_that.extraType,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case JellyfinMediaItem() when jellyfin != null: +return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.ratings,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.playlistItemId,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case _: return orElse(); } @@ -225,11 +230,11 @@ return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? audienceRating, @JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, String? ratingImage, String? audienceRatingImage, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage, @JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey, @JsonKey(fromJson: flexibleInt) int? playlistItemId, @JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype, @JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw) plex,required TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw) jellyfin,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage, @JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey, @JsonKey(fromJson: flexibleInt) int? playlistItemId, @JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype, @JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw) plex,required TResult Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw) jellyfin,}) {final _that = this; switch (_that) { case PlexMediaItem(): -return plex(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.editionTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.audienceRating,_that.userRating,_that.isFavorite,_that.ratingImage,_that.audienceRatingImage,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.subtitleLanguage,_that.subtitleMode,_that.trailerKey,_that.playlistItemId,_that.playQueueItemId,_that.subtype,_that.extraType,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case JellyfinMediaItem(): -return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.playlistItemId,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);} +return plex(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.editionTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.ratings,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.subtitleLanguage,_that.subtitleMode,_that.trailerKey,_that.playlistItemId,_that.playQueueItemId,_that.subtype,_that.extraType,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case JellyfinMediaItem(): +return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.ratings,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.playlistItemId,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);} } /// A variant of `when` that fallback to returning `null` /// @@ -243,11 +248,11 @@ return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? audienceRating, @JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, String? ratingImage, String? audienceRatingImage, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage, @JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey, @JsonKey(fromJson: flexibleInt) int? playlistItemId, @JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype, @JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? plex,TResult? Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? jellyfin,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage, @JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey, @JsonKey(fromJson: flexibleInt) int? playlistItemId, @JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype, @JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? plex,TResult? Function(@JsonKey(readValue: readStringField, defaultValue: '') String id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio, @JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath, @JsonKey(fromJson: flexibleInt) int? parentIndex, @JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath, @JsonKey(fromJson: flexibleInt) int? durationMs, @JsonKey(fromJson: flexibleInt) int? viewOffsetMs, @JsonKey(fromJson: flexibleInt) int? viewCount, @JsonKey(fromJson: flexibleInt) int? lastViewedAt, @JsonKey(fromJson: flexibleInt) int? leafCount, @JsonKey(fromJson: flexibleInt) int? viewedLeafCount, @JsonKey(fromJson: flexibleInt) int? childCount, @JsonKey(fromJson: flexibleInt) int? addedAt, @JsonKey(fromJson: flexibleInt) int? updatedAt, @JsonKey(fromJson: flexibleDouble) double? rating, @JsonKey(fromJson: flexibleDouble) double? userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite, @JsonKey(fromJson: _mediaItemStringList) List? genres, @JsonKey(fromJson: _mediaItemStringList) List? directors, @JsonKey(fromJson: _mediaItemStringList) List? writers, @JsonKey(fromJson: _mediaItemStringList) List? producers, @JsonKey(fromJson: _mediaItemStringList) List? countries, @JsonKey(fromJson: _mediaItemStringList) List? collections, @JsonKey(fromJson: _mediaItemStringList) List? labels, @JsonKey(fromJson: _mediaItemStringList) List? styles, @JsonKey(fromJson: _mediaItemStringList) List? moods, @JsonKey(fromJson: _mediaItemRolesFromJson) List? roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) Map? raw)? jellyfin,}) {final _that = this; switch (_that) { case PlexMediaItem() when plex != null: -return plex(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.editionTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.audienceRating,_that.userRating,_that.isFavorite,_that.ratingImage,_that.audienceRatingImage,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.subtitleLanguage,_that.subtitleMode,_that.trailerKey,_that.playlistItemId,_that.playQueueItemId,_that.subtype,_that.extraType,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case JellyfinMediaItem() when jellyfin != null: -return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.playlistItemId,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case _: +return plex(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.editionTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.ratings,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.subtitleLanguage,_that.subtitleMode,_that.trailerKey,_that.playlistItemId,_that.playQueueItemId,_that.subtype,_that.extraType,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case JellyfinMediaItem() when jellyfin != null: +return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that.summary,_that.tagline,_that.originalTitle,_that.studio,_that.year,_that.originallyAvailableAt,_that.contentRating,_that.parentId,_that.parentTitle,_that.parentThumbPath,_that.parentIndex,_that.index,_that.grandparentId,_that.grandparentTitle,_that.grandparentThumbPath,_that.grandparentArtPath,_that.grandparentBackdropPaths,_that.thumbPath,_that.artPath,_that.backdropPaths,_that.clearLogoPath,_that.backgroundSquarePath,_that.durationMs,_that.viewOffsetMs,_that.viewCount,_that.lastViewedAt,_that.leafCount,_that.viewedLeafCount,_that.childCount,_that.addedAt,_that.updatedAt,_that.rating,_that.userRating,_that.ratings,_that.isFavorite,_that.genres,_that.directors,_that.writers,_that.producers,_that.countries,_that.collections,_that.labels,_that.styles,_that.moods,_that.roles,_that.mediaVersions,_that.libraryId,_that.libraryTitle,_that.audioLanguage,_that.playlistItemId,_that.serverId,_that.serverName,_that.backendFolderKey,_that.raw);case _: return null; } @@ -259,7 +264,7 @@ return jellyfin(_that.id,_that.kind,_that.guid,_that.title,_that.titleSort,_that @JsonSerializable(includeIfNull: false, explicitToJson: true) class PlexMediaItem extends MediaItem { - const PlexMediaItem({@JsonKey(readValue: readStringField, defaultValue: '') required this.id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required this.kind, this.guid, this.title, this.titleSort, this.summary, this.tagline, this.originalTitle, this.editionTitle, this.studio, @JsonKey(fromJson: flexibleInt) this.year, this.originallyAvailableAt, this.contentRating, this.parentId, this.parentTitle, this.parentThumbPath, @JsonKey(fromJson: flexibleInt) this.parentIndex, @JsonKey(fromJson: flexibleInt) this.index, this.grandparentId, this.grandparentTitle, this.grandparentThumbPath, this.grandparentArtPath, this.grandparentBackdropPaths, this.thumbPath, this.artPath, this.backdropPaths, this.clearLogoPath, this.backgroundSquarePath, @JsonKey(fromJson: flexibleInt) this.durationMs, @JsonKey(fromJson: flexibleInt) this.viewOffsetMs, @JsonKey(fromJson: flexibleInt) this.viewCount, @JsonKey(fromJson: flexibleInt) this.lastViewedAt, @JsonKey(fromJson: flexibleInt) this.leafCount, @JsonKey(fromJson: flexibleInt) this.viewedLeafCount, @JsonKey(fromJson: flexibleInt) this.childCount, @JsonKey(fromJson: flexibleInt) this.addedAt, @JsonKey(fromJson: flexibleInt) this.updatedAt, @JsonKey(fromJson: flexibleDouble) this.rating, @JsonKey(fromJson: flexibleDouble) this.audienceRating, @JsonKey(fromJson: flexibleDouble) this.userRating, this.isFavorite, this.ratingImage, this.audienceRatingImage, @JsonKey(fromJson: _mediaItemStringList) this.genres, @JsonKey(fromJson: _mediaItemStringList) this.directors, @JsonKey(fromJson: _mediaItemStringList) this.writers, @JsonKey(fromJson: _mediaItemStringList) this.producers, @JsonKey(fromJson: _mediaItemStringList) this.countries, @JsonKey(fromJson: _mediaItemStringList) this.collections, @JsonKey(fromJson: _mediaItemStringList) this.labels, @JsonKey(fromJson: _mediaItemStringList) this.styles, @JsonKey(fromJson: _mediaItemStringList) this.moods, @JsonKey(fromJson: _mediaItemRolesFromJson) this.roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) this.mediaVersions, this.libraryId, this.libraryTitle, this.audioLanguage, this.subtitleLanguage, @JsonKey(fromJson: flexibleInt) this.subtitleMode, this.trailerKey, @JsonKey(fromJson: flexibleInt) this.playlistItemId, @JsonKey(fromJson: flexibleInt) this.playQueueItemId, this.subtype, @JsonKey(fromJson: flexibleInt) this.extraType, this.serverId, this.serverName, this.backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) this.raw}): super._(); + const PlexMediaItem({@JsonKey(readValue: readStringField, defaultValue: '') required this.id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required this.kind, this.guid, this.title, this.titleSort, this.summary, this.tagline, this.originalTitle, this.editionTitle, this.studio, @JsonKey(fromJson: flexibleInt) this.year, this.originallyAvailableAt, this.contentRating, this.parentId, this.parentTitle, this.parentThumbPath, @JsonKey(fromJson: flexibleInt) this.parentIndex, @JsonKey(fromJson: flexibleInt) this.index, this.grandparentId, this.grandparentTitle, this.grandparentThumbPath, this.grandparentArtPath, this.grandparentBackdropPaths, this.thumbPath, this.artPath, this.backdropPaths, this.clearLogoPath, this.backgroundSquarePath, @JsonKey(fromJson: flexibleInt) this.durationMs, @JsonKey(fromJson: flexibleInt) this.viewOffsetMs, @JsonKey(fromJson: flexibleInt) this.viewCount, @JsonKey(fromJson: flexibleInt) this.lastViewedAt, @JsonKey(fromJson: flexibleInt) this.leafCount, @JsonKey(fromJson: flexibleInt) this.viewedLeafCount, @JsonKey(fromJson: flexibleInt) this.childCount, @JsonKey(fromJson: flexibleInt) this.addedAt, @JsonKey(fromJson: flexibleInt) this.updatedAt, @JsonKey(fromJson: flexibleDouble) this.rating, @JsonKey(fromJson: flexibleDouble) this.userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) this.ratings, this.isFavorite, @JsonKey(fromJson: _mediaItemStringList) this.genres, @JsonKey(fromJson: _mediaItemStringList) this.directors, @JsonKey(fromJson: _mediaItemStringList) this.writers, @JsonKey(fromJson: _mediaItemStringList) this.producers, @JsonKey(fromJson: _mediaItemStringList) this.countries, @JsonKey(fromJson: _mediaItemStringList) this.collections, @JsonKey(fromJson: _mediaItemStringList) this.labels, @JsonKey(fromJson: _mediaItemStringList) this.styles, @JsonKey(fromJson: _mediaItemStringList) this.moods, @JsonKey(fromJson: _mediaItemRolesFromJson) this.roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) this.mediaVersions, this.libraryId, this.libraryTitle, this.audioLanguage, this.subtitleLanguage, @JsonKey(fromJson: flexibleInt) this.subtitleMode, this.trailerKey, @JsonKey(fromJson: flexibleInt) this.playlistItemId, @JsonKey(fromJson: flexibleInt) this.playQueueItemId, this.subtype, @JsonKey(fromJson: flexibleInt) this.extraType, this.serverId, this.serverName, this.backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) this.raw}): super._(); @override@JsonKey(readValue: readStringField, defaultValue: '') final String id; @@ -301,11 +306,13 @@ class PlexMediaItem extends MediaItem { @override@JsonKey(fromJson: flexibleInt) final int? addedAt; @override@JsonKey(fromJson: flexibleInt) final int? updatedAt; @override@JsonKey(fromJson: flexibleDouble) final double? rating; -@JsonKey(fromJson: flexibleDouble) final double? audienceRating; @override@JsonKey(fromJson: flexibleDouble) final double? userRating; +/// Every attributed score the response carried, headline first. Plex +/// listings yield one or two (the `rating`/`audienceRating` pair with +/// their source images); `/library/metadata/{id}` adds the `Rating[]` +/// array, so IMDb and TMDB join Rotten Tomatoes on detail screens. +@override@JsonKey(fromJson: _mediaItemRatingsFromJson) final List? ratings; @override final bool? isFavorite; - final String? ratingImage; - final String? audienceRatingImage; @override@JsonKey(fromJson: _mediaItemStringList) final List? genres; @override@JsonKey(fromJson: _mediaItemStringList) final List? directors; @override@JsonKey(fromJson: _mediaItemStringList) final List? writers; @@ -347,7 +354,7 @@ $PlexMediaItemCopyWith get copyWith => _$PlexMediaItemCopyWithImp @override String toString() { - return 'MediaItem.plex(id: $id, kind: $kind, guid: $guid, title: $title, titleSort: $titleSort, summary: $summary, tagline: $tagline, originalTitle: $originalTitle, editionTitle: $editionTitle, studio: $studio, year: $year, originallyAvailableAt: $originallyAvailableAt, contentRating: $contentRating, parentId: $parentId, parentTitle: $parentTitle, parentThumbPath: $parentThumbPath, parentIndex: $parentIndex, index: $index, grandparentId: $grandparentId, grandparentTitle: $grandparentTitle, grandparentThumbPath: $grandparentThumbPath, grandparentArtPath: $grandparentArtPath, grandparentBackdropPaths: $grandparentBackdropPaths, thumbPath: $thumbPath, artPath: $artPath, backdropPaths: $backdropPaths, clearLogoPath: $clearLogoPath, backgroundSquarePath: $backgroundSquarePath, durationMs: $durationMs, viewOffsetMs: $viewOffsetMs, viewCount: $viewCount, lastViewedAt: $lastViewedAt, leafCount: $leafCount, viewedLeafCount: $viewedLeafCount, childCount: $childCount, addedAt: $addedAt, updatedAt: $updatedAt, rating: $rating, audienceRating: $audienceRating, userRating: $userRating, isFavorite: $isFavorite, ratingImage: $ratingImage, audienceRatingImage: $audienceRatingImage, genres: $genres, directors: $directors, writers: $writers, producers: $producers, countries: $countries, collections: $collections, labels: $labels, styles: $styles, moods: $moods, roles: $roles, mediaVersions: $mediaVersions, libraryId: $libraryId, libraryTitle: $libraryTitle, audioLanguage: $audioLanguage, subtitleLanguage: $subtitleLanguage, subtitleMode: $subtitleMode, trailerKey: $trailerKey, playlistItemId: $playlistItemId, playQueueItemId: $playQueueItemId, subtype: $subtype, extraType: $extraType, serverId: $serverId, serverName: $serverName, backendFolderKey: $backendFolderKey, raw: $raw)'; + return 'MediaItem.plex(id: $id, kind: $kind, guid: $guid, title: $title, titleSort: $titleSort, summary: $summary, tagline: $tagline, originalTitle: $originalTitle, editionTitle: $editionTitle, studio: $studio, year: $year, originallyAvailableAt: $originallyAvailableAt, contentRating: $contentRating, parentId: $parentId, parentTitle: $parentTitle, parentThumbPath: $parentThumbPath, parentIndex: $parentIndex, index: $index, grandparentId: $grandparentId, grandparentTitle: $grandparentTitle, grandparentThumbPath: $grandparentThumbPath, grandparentArtPath: $grandparentArtPath, grandparentBackdropPaths: $grandparentBackdropPaths, thumbPath: $thumbPath, artPath: $artPath, backdropPaths: $backdropPaths, clearLogoPath: $clearLogoPath, backgroundSquarePath: $backgroundSquarePath, durationMs: $durationMs, viewOffsetMs: $viewOffsetMs, viewCount: $viewCount, lastViewedAt: $lastViewedAt, leafCount: $leafCount, viewedLeafCount: $viewedLeafCount, childCount: $childCount, addedAt: $addedAt, updatedAt: $updatedAt, rating: $rating, userRating: $userRating, ratings: $ratings, isFavorite: $isFavorite, genres: $genres, directors: $directors, writers: $writers, producers: $producers, countries: $countries, collections: $collections, labels: $labels, styles: $styles, moods: $moods, roles: $roles, mediaVersions: $mediaVersions, libraryId: $libraryId, libraryTitle: $libraryTitle, audioLanguage: $audioLanguage, subtitleLanguage: $subtitleLanguage, subtitleMode: $subtitleMode, trailerKey: $trailerKey, playlistItemId: $playlistItemId, playQueueItemId: $playQueueItemId, subtype: $subtype, extraType: $extraType, serverId: $serverId, serverName: $serverName, backendFolderKey: $backendFolderKey, raw: $raw)'; } @@ -358,7 +365,7 @@ abstract mixin class $PlexMediaItemCopyWith<$Res> implements $MediaItemCopyWith< factory $PlexMediaItemCopyWith(PlexMediaItem value, $Res Function(PlexMediaItem) _then) = _$PlexMediaItemCopyWithImpl; @override @useResult $Res call({ -@JsonKey(readValue: readStringField, defaultValue: '') String id,@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio,@JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath,@JsonKey(fromJson: flexibleInt) int? parentIndex,@JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath,@JsonKey(fromJson: flexibleInt) int? durationMs,@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,@JsonKey(fromJson: flexibleInt) int? viewCount,@JsonKey(fromJson: flexibleInt) int? lastViewedAt,@JsonKey(fromJson: flexibleInt) int? leafCount,@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,@JsonKey(fromJson: flexibleInt) int? childCount,@JsonKey(fromJson: flexibleInt) int? addedAt,@JsonKey(fromJson: flexibleInt) int? updatedAt,@JsonKey(fromJson: flexibleDouble) double? rating,@JsonKey(fromJson: flexibleDouble) double? audienceRating,@JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite, String? ratingImage, String? audienceRatingImage,@JsonKey(fromJson: _mediaItemStringList) List? genres,@JsonKey(fromJson: _mediaItemStringList) List? directors,@JsonKey(fromJson: _mediaItemStringList) List? writers,@JsonKey(fromJson: _mediaItemStringList) List? producers,@JsonKey(fromJson: _mediaItemStringList) List? countries,@JsonKey(fromJson: _mediaItemStringList) List? collections,@JsonKey(fromJson: _mediaItemStringList) List? labels,@JsonKey(fromJson: _mediaItemStringList) List? styles,@JsonKey(fromJson: _mediaItemStringList) List? moods,@JsonKey(fromJson: _mediaItemRolesFromJson) List? roles,@JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage,@JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey,@JsonKey(fromJson: flexibleInt) int? playlistItemId,@JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype,@JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey,@JsonKey(fromJson: _mediaItemRawFromJson) Map? raw +@JsonKey(readValue: readStringField, defaultValue: '') String id,@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? editionTitle, String? studio,@JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath,@JsonKey(fromJson: flexibleInt) int? parentIndex,@JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath,@JsonKey(fromJson: flexibleInt) int? durationMs,@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,@JsonKey(fromJson: flexibleInt) int? viewCount,@JsonKey(fromJson: flexibleInt) int? lastViewedAt,@JsonKey(fromJson: flexibleInt) int? leafCount,@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,@JsonKey(fromJson: flexibleInt) int? childCount,@JsonKey(fromJson: flexibleInt) int? addedAt,@JsonKey(fromJson: flexibleInt) int? updatedAt,@JsonKey(fromJson: flexibleDouble) double? rating,@JsonKey(fromJson: flexibleDouble) double? userRating,@JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite,@JsonKey(fromJson: _mediaItemStringList) List? genres,@JsonKey(fromJson: _mediaItemStringList) List? directors,@JsonKey(fromJson: _mediaItemStringList) List? writers,@JsonKey(fromJson: _mediaItemStringList) List? producers,@JsonKey(fromJson: _mediaItemStringList) List? countries,@JsonKey(fromJson: _mediaItemStringList) List? collections,@JsonKey(fromJson: _mediaItemStringList) List? labels,@JsonKey(fromJson: _mediaItemStringList) List? styles,@JsonKey(fromJson: _mediaItemStringList) List? moods,@JsonKey(fromJson: _mediaItemRolesFromJson) List? roles,@JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? subtitleLanguage,@JsonKey(fromJson: flexibleInt) int? subtitleMode, String? trailerKey,@JsonKey(fromJson: flexibleInt) int? playlistItemId,@JsonKey(fromJson: flexibleInt) int? playQueueItemId, String? subtype,@JsonKey(fromJson: flexibleInt) int? extraType, String? serverId, String? serverName, String? backendFolderKey,@JsonKey(fromJson: _mediaItemRawFromJson) Map? raw }); @@ -375,7 +382,7 @@ class _$PlexMediaItemCopyWithImpl<$Res> /// Create a copy of MediaItem /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? kind = null,Object? guid = freezed,Object? title = freezed,Object? titleSort = freezed,Object? summary = freezed,Object? tagline = freezed,Object? originalTitle = freezed,Object? editionTitle = freezed,Object? studio = freezed,Object? year = freezed,Object? originallyAvailableAt = freezed,Object? contentRating = freezed,Object? parentId = freezed,Object? parentTitle = freezed,Object? parentThumbPath = freezed,Object? parentIndex = freezed,Object? index = freezed,Object? grandparentId = freezed,Object? grandparentTitle = freezed,Object? grandparentThumbPath = freezed,Object? grandparentArtPath = freezed,Object? grandparentBackdropPaths = freezed,Object? thumbPath = freezed,Object? artPath = freezed,Object? backdropPaths = freezed,Object? clearLogoPath = freezed,Object? backgroundSquarePath = freezed,Object? durationMs = freezed,Object? viewOffsetMs = freezed,Object? viewCount = freezed,Object? lastViewedAt = freezed,Object? leafCount = freezed,Object? viewedLeafCount = freezed,Object? childCount = freezed,Object? addedAt = freezed,Object? updatedAt = freezed,Object? rating = freezed,Object? audienceRating = freezed,Object? userRating = freezed,Object? isFavorite = freezed,Object? ratingImage = freezed,Object? audienceRatingImage = freezed,Object? genres = freezed,Object? directors = freezed,Object? writers = freezed,Object? producers = freezed,Object? countries = freezed,Object? collections = freezed,Object? labels = freezed,Object? styles = freezed,Object? moods = freezed,Object? roles = freezed,Object? mediaVersions = freezed,Object? libraryId = freezed,Object? libraryTitle = freezed,Object? audioLanguage = freezed,Object? subtitleLanguage = freezed,Object? subtitleMode = freezed,Object? trailerKey = freezed,Object? playlistItemId = freezed,Object? playQueueItemId = freezed,Object? subtype = freezed,Object? extraType = freezed,Object? serverId = freezed,Object? serverName = freezed,Object? backendFolderKey = freezed,Object? raw = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? kind = null,Object? guid = freezed,Object? title = freezed,Object? titleSort = freezed,Object? summary = freezed,Object? tagline = freezed,Object? originalTitle = freezed,Object? editionTitle = freezed,Object? studio = freezed,Object? year = freezed,Object? originallyAvailableAt = freezed,Object? contentRating = freezed,Object? parentId = freezed,Object? parentTitle = freezed,Object? parentThumbPath = freezed,Object? parentIndex = freezed,Object? index = freezed,Object? grandparentId = freezed,Object? grandparentTitle = freezed,Object? grandparentThumbPath = freezed,Object? grandparentArtPath = freezed,Object? grandparentBackdropPaths = freezed,Object? thumbPath = freezed,Object? artPath = freezed,Object? backdropPaths = freezed,Object? clearLogoPath = freezed,Object? backgroundSquarePath = freezed,Object? durationMs = freezed,Object? viewOffsetMs = freezed,Object? viewCount = freezed,Object? lastViewedAt = freezed,Object? leafCount = freezed,Object? viewedLeafCount = freezed,Object? childCount = freezed,Object? addedAt = freezed,Object? updatedAt = freezed,Object? rating = freezed,Object? userRating = freezed,Object? ratings = freezed,Object? isFavorite = freezed,Object? genres = freezed,Object? directors = freezed,Object? writers = freezed,Object? producers = freezed,Object? countries = freezed,Object? collections = freezed,Object? labels = freezed,Object? styles = freezed,Object? moods = freezed,Object? roles = freezed,Object? mediaVersions = freezed,Object? libraryId = freezed,Object? libraryTitle = freezed,Object? audioLanguage = freezed,Object? subtitleLanguage = freezed,Object? subtitleMode = freezed,Object? trailerKey = freezed,Object? playlistItemId = freezed,Object? playQueueItemId = freezed,Object? subtype = freezed,Object? extraType = freezed,Object? serverId = freezed,Object? serverName = freezed,Object? backendFolderKey = freezed,Object? raw = freezed,}) { return _then(PlexMediaItem( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,kind: null == kind ? _self.kind : kind // ignore: cast_nullable_to_non_nullable @@ -415,12 +422,10 @@ as int?,childCount: freezed == childCount ? _self.childCount : childCount // ign as int?,addedAt: freezed == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as int?,rating: freezed == rating ? _self.rating : rating // ignore: cast_nullable_to_non_nullable -as double?,audienceRating: freezed == audienceRating ? _self.audienceRating : audienceRating // ignore: cast_nullable_to_non_nullable as double?,userRating: freezed == userRating ? _self.userRating : userRating // ignore: cast_nullable_to_non_nullable -as double?,isFavorite: freezed == isFavorite ? _self.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable -as bool?,ratingImage: freezed == ratingImage ? _self.ratingImage : ratingImage // ignore: cast_nullable_to_non_nullable -as String?,audienceRatingImage: freezed == audienceRatingImage ? _self.audienceRatingImage : audienceRatingImage // ignore: cast_nullable_to_non_nullable -as String?,genres: freezed == genres ? _self.genres : genres // ignore: cast_nullable_to_non_nullable +as double?,ratings: freezed == ratings ? _self.ratings : ratings // ignore: cast_nullable_to_non_nullable +as List?,isFavorite: freezed == isFavorite ? _self.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable +as bool?,genres: freezed == genres ? _self.genres : genres // ignore: cast_nullable_to_non_nullable as List?,directors: freezed == directors ? _self.directors : directors // ignore: cast_nullable_to_non_nullable as List?,writers: freezed == writers ? _self.writers : writers // ignore: cast_nullable_to_non_nullable as List?,producers: freezed == producers ? _self.producers : producers // ignore: cast_nullable_to_non_nullable @@ -456,7 +461,7 @@ as Map?, @JsonSerializable(includeIfNull: false, explicitToJson: true) class JellyfinMediaItem extends MediaItem { - const JellyfinMediaItem({@JsonKey(readValue: readStringField, defaultValue: '') required this.id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required this.kind, this.guid, this.title, this.titleSort, this.summary, this.tagline, this.originalTitle, this.studio, @JsonKey(fromJson: flexibleInt) this.year, this.originallyAvailableAt, this.contentRating, this.parentId, this.parentTitle, this.parentThumbPath, @JsonKey(fromJson: flexibleInt) this.parentIndex, @JsonKey(fromJson: flexibleInt) this.index, this.grandparentId, this.grandparentTitle, this.grandparentThumbPath, this.grandparentArtPath, this.grandparentBackdropPaths, this.thumbPath, this.artPath, this.backdropPaths, this.clearLogoPath, this.backgroundSquarePath, @JsonKey(fromJson: flexibleInt) this.durationMs, @JsonKey(fromJson: flexibleInt) this.viewOffsetMs, @JsonKey(fromJson: flexibleInt) this.viewCount, @JsonKey(fromJson: flexibleInt) this.lastViewedAt, @JsonKey(fromJson: flexibleInt) this.leafCount, @JsonKey(fromJson: flexibleInt) this.viewedLeafCount, @JsonKey(fromJson: flexibleInt) this.childCount, @JsonKey(fromJson: flexibleInt) this.addedAt, @JsonKey(fromJson: flexibleInt) this.updatedAt, @JsonKey(fromJson: flexibleDouble) this.rating, @JsonKey(fromJson: flexibleDouble) this.userRating, this.isFavorite, @JsonKey(fromJson: _mediaItemStringList) this.genres, @JsonKey(fromJson: _mediaItemStringList) this.directors, @JsonKey(fromJson: _mediaItemStringList) this.writers, @JsonKey(fromJson: _mediaItemStringList) this.producers, @JsonKey(fromJson: _mediaItemStringList) this.countries, @JsonKey(fromJson: _mediaItemStringList) this.collections, @JsonKey(fromJson: _mediaItemStringList) this.labels, @JsonKey(fromJson: _mediaItemStringList) this.styles, @JsonKey(fromJson: _mediaItemStringList) this.moods, @JsonKey(fromJson: _mediaItemRolesFromJson) this.roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) this.mediaVersions, this.libraryId, this.libraryTitle, this.audioLanguage, this.playlistItemId, this.serverId, this.serverName, this.backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) this.raw}): super._(); + const JellyfinMediaItem({@JsonKey(readValue: readStringField, defaultValue: '') required this.id, @JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) required this.kind, this.guid, this.title, this.titleSort, this.summary, this.tagline, this.originalTitle, this.studio, @JsonKey(fromJson: flexibleInt) this.year, this.originallyAvailableAt, this.contentRating, this.parentId, this.parentTitle, this.parentThumbPath, @JsonKey(fromJson: flexibleInt) this.parentIndex, @JsonKey(fromJson: flexibleInt) this.index, this.grandparentId, this.grandparentTitle, this.grandparentThumbPath, this.grandparentArtPath, this.grandparentBackdropPaths, this.thumbPath, this.artPath, this.backdropPaths, this.clearLogoPath, this.backgroundSquarePath, @JsonKey(fromJson: flexibleInt) this.durationMs, @JsonKey(fromJson: flexibleInt) this.viewOffsetMs, @JsonKey(fromJson: flexibleInt) this.viewCount, @JsonKey(fromJson: flexibleInt) this.lastViewedAt, @JsonKey(fromJson: flexibleInt) this.leafCount, @JsonKey(fromJson: flexibleInt) this.viewedLeafCount, @JsonKey(fromJson: flexibleInt) this.childCount, @JsonKey(fromJson: flexibleInt) this.addedAt, @JsonKey(fromJson: flexibleInt) this.updatedAt, @JsonKey(fromJson: flexibleDouble) this.rating, @JsonKey(fromJson: flexibleDouble) this.userRating, @JsonKey(fromJson: _mediaItemRatingsFromJson) this.ratings, this.isFavorite, @JsonKey(fromJson: _mediaItemStringList) this.genres, @JsonKey(fromJson: _mediaItemStringList) this.directors, @JsonKey(fromJson: _mediaItemStringList) this.writers, @JsonKey(fromJson: _mediaItemStringList) this.producers, @JsonKey(fromJson: _mediaItemStringList) this.countries, @JsonKey(fromJson: _mediaItemStringList) this.collections, @JsonKey(fromJson: _mediaItemStringList) this.labels, @JsonKey(fromJson: _mediaItemStringList) this.styles, @JsonKey(fromJson: _mediaItemStringList) this.moods, @JsonKey(fromJson: _mediaItemRolesFromJson) this.roles, @JsonKey(fromJson: _mediaItemVersionsFromJson) this.mediaVersions, this.libraryId, this.libraryTitle, this.audioLanguage, this.playlistItemId, this.serverId, this.serverName, this.backendFolderKey, @JsonKey(fromJson: _mediaItemRawFromJson) this.raw}): super._(); @override@JsonKey(readValue: readStringField, defaultValue: '') final String id; @@ -497,6 +502,9 @@ class JellyfinMediaItem extends MediaItem { @override@JsonKey(fromJson: flexibleInt) final int? updatedAt; @override@JsonKey(fromJson: flexibleDouble) final double? rating; @override@JsonKey(fromJson: flexibleDouble) final double? userRating; +/// `CommunityRating` and the `CriticRating` Tomatometer, in that order. +/// Jellyfin exposes no per-source array, so this is at most two entries. +@override@JsonKey(fromJson: _mediaItemRatingsFromJson) final List? ratings; @override final bool? isFavorite; @override@JsonKey(fromJson: _mediaItemStringList) final List? genres; @override@JsonKey(fromJson: _mediaItemStringList) final List? directors; @@ -533,7 +541,7 @@ $JellyfinMediaItemCopyWith get copyWith => _$JellyfinMediaIte @override String toString() { - return 'MediaItem.jellyfin(id: $id, kind: $kind, guid: $guid, title: $title, titleSort: $titleSort, summary: $summary, tagline: $tagline, originalTitle: $originalTitle, studio: $studio, year: $year, originallyAvailableAt: $originallyAvailableAt, contentRating: $contentRating, parentId: $parentId, parentTitle: $parentTitle, parentThumbPath: $parentThumbPath, parentIndex: $parentIndex, index: $index, grandparentId: $grandparentId, grandparentTitle: $grandparentTitle, grandparentThumbPath: $grandparentThumbPath, grandparentArtPath: $grandparentArtPath, grandparentBackdropPaths: $grandparentBackdropPaths, thumbPath: $thumbPath, artPath: $artPath, backdropPaths: $backdropPaths, clearLogoPath: $clearLogoPath, backgroundSquarePath: $backgroundSquarePath, durationMs: $durationMs, viewOffsetMs: $viewOffsetMs, viewCount: $viewCount, lastViewedAt: $lastViewedAt, leafCount: $leafCount, viewedLeafCount: $viewedLeafCount, childCount: $childCount, addedAt: $addedAt, updatedAt: $updatedAt, rating: $rating, userRating: $userRating, isFavorite: $isFavorite, genres: $genres, directors: $directors, writers: $writers, producers: $producers, countries: $countries, collections: $collections, labels: $labels, styles: $styles, moods: $moods, roles: $roles, mediaVersions: $mediaVersions, libraryId: $libraryId, libraryTitle: $libraryTitle, audioLanguage: $audioLanguage, playlistItemId: $playlistItemId, serverId: $serverId, serverName: $serverName, backendFolderKey: $backendFolderKey, raw: $raw)'; + return 'MediaItem.jellyfin(id: $id, kind: $kind, guid: $guid, title: $title, titleSort: $titleSort, summary: $summary, tagline: $tagline, originalTitle: $originalTitle, studio: $studio, year: $year, originallyAvailableAt: $originallyAvailableAt, contentRating: $contentRating, parentId: $parentId, parentTitle: $parentTitle, parentThumbPath: $parentThumbPath, parentIndex: $parentIndex, index: $index, grandparentId: $grandparentId, grandparentTitle: $grandparentTitle, grandparentThumbPath: $grandparentThumbPath, grandparentArtPath: $grandparentArtPath, grandparentBackdropPaths: $grandparentBackdropPaths, thumbPath: $thumbPath, artPath: $artPath, backdropPaths: $backdropPaths, clearLogoPath: $clearLogoPath, backgroundSquarePath: $backgroundSquarePath, durationMs: $durationMs, viewOffsetMs: $viewOffsetMs, viewCount: $viewCount, lastViewedAt: $lastViewedAt, leafCount: $leafCount, viewedLeafCount: $viewedLeafCount, childCount: $childCount, addedAt: $addedAt, updatedAt: $updatedAt, rating: $rating, userRating: $userRating, ratings: $ratings, isFavorite: $isFavorite, genres: $genres, directors: $directors, writers: $writers, producers: $producers, countries: $countries, collections: $collections, labels: $labels, styles: $styles, moods: $moods, roles: $roles, mediaVersions: $mediaVersions, libraryId: $libraryId, libraryTitle: $libraryTitle, audioLanguage: $audioLanguage, playlistItemId: $playlistItemId, serverId: $serverId, serverName: $serverName, backendFolderKey: $backendFolderKey, raw: $raw)'; } @@ -544,7 +552,7 @@ abstract mixin class $JellyfinMediaItemCopyWith<$Res> implements $MediaItemCopyW factory $JellyfinMediaItemCopyWith(JellyfinMediaItem value, $Res Function(JellyfinMediaItem) _then) = _$JellyfinMediaItemCopyWithImpl; @override @useResult $Res call({ -@JsonKey(readValue: readStringField, defaultValue: '') String id,@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio,@JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath,@JsonKey(fromJson: flexibleInt) int? parentIndex,@JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath,@JsonKey(fromJson: flexibleInt) int? durationMs,@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,@JsonKey(fromJson: flexibleInt) int? viewCount,@JsonKey(fromJson: flexibleInt) int? lastViewedAt,@JsonKey(fromJson: flexibleInt) int? leafCount,@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,@JsonKey(fromJson: flexibleInt) int? childCount,@JsonKey(fromJson: flexibleInt) int? addedAt,@JsonKey(fromJson: flexibleInt) int? updatedAt,@JsonKey(fromJson: flexibleDouble) double? rating,@JsonKey(fromJson: flexibleDouble) double? userRating, bool? isFavorite,@JsonKey(fromJson: _mediaItemStringList) List? genres,@JsonKey(fromJson: _mediaItemStringList) List? directors,@JsonKey(fromJson: _mediaItemStringList) List? writers,@JsonKey(fromJson: _mediaItemStringList) List? producers,@JsonKey(fromJson: _mediaItemStringList) List? countries,@JsonKey(fromJson: _mediaItemStringList) List? collections,@JsonKey(fromJson: _mediaItemStringList) List? labels,@JsonKey(fromJson: _mediaItemStringList) List? styles,@JsonKey(fromJson: _mediaItemStringList) List? moods,@JsonKey(fromJson: _mediaItemRolesFromJson) List? roles,@JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey,@JsonKey(fromJson: _mediaItemRawFromJson) Map? raw +@JsonKey(readValue: readStringField, defaultValue: '') String id,@JsonKey(fromJson: _mediaKindFromJson, toJson: _mediaKindToJson) MediaKind kind, String? guid, String? title, String? titleSort, String? summary, String? tagline, String? originalTitle, String? studio,@JsonKey(fromJson: flexibleInt) int? year, String? originallyAvailableAt, String? contentRating, String? parentId, String? parentTitle, String? parentThumbPath,@JsonKey(fromJson: flexibleInt) int? parentIndex,@JsonKey(fromJson: flexibleInt) int? index, String? grandparentId, String? grandparentTitle, String? grandparentThumbPath, String? grandparentArtPath, List? grandparentBackdropPaths, String? thumbPath, String? artPath, List? backdropPaths, String? clearLogoPath, String? backgroundSquarePath,@JsonKey(fromJson: flexibleInt) int? durationMs,@JsonKey(fromJson: flexibleInt) int? viewOffsetMs,@JsonKey(fromJson: flexibleInt) int? viewCount,@JsonKey(fromJson: flexibleInt) int? lastViewedAt,@JsonKey(fromJson: flexibleInt) int? leafCount,@JsonKey(fromJson: flexibleInt) int? viewedLeafCount,@JsonKey(fromJson: flexibleInt) int? childCount,@JsonKey(fromJson: flexibleInt) int? addedAt,@JsonKey(fromJson: flexibleInt) int? updatedAt,@JsonKey(fromJson: flexibleDouble) double? rating,@JsonKey(fromJson: flexibleDouble) double? userRating,@JsonKey(fromJson: _mediaItemRatingsFromJson) List? ratings, bool? isFavorite,@JsonKey(fromJson: _mediaItemStringList) List? genres,@JsonKey(fromJson: _mediaItemStringList) List? directors,@JsonKey(fromJson: _mediaItemStringList) List? writers,@JsonKey(fromJson: _mediaItemStringList) List? producers,@JsonKey(fromJson: _mediaItemStringList) List? countries,@JsonKey(fromJson: _mediaItemStringList) List? collections,@JsonKey(fromJson: _mediaItemStringList) List? labels,@JsonKey(fromJson: _mediaItemStringList) List? styles,@JsonKey(fromJson: _mediaItemStringList) List? moods,@JsonKey(fromJson: _mediaItemRolesFromJson) List? roles,@JsonKey(fromJson: _mediaItemVersionsFromJson) List? mediaVersions, String? libraryId, String? libraryTitle, String? audioLanguage, String? playlistItemId, String? serverId, String? serverName, String? backendFolderKey,@JsonKey(fromJson: _mediaItemRawFromJson) Map? raw }); @@ -561,7 +569,7 @@ class _$JellyfinMediaItemCopyWithImpl<$Res> /// Create a copy of MediaItem /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? kind = null,Object? guid = freezed,Object? title = freezed,Object? titleSort = freezed,Object? summary = freezed,Object? tagline = freezed,Object? originalTitle = freezed,Object? studio = freezed,Object? year = freezed,Object? originallyAvailableAt = freezed,Object? contentRating = freezed,Object? parentId = freezed,Object? parentTitle = freezed,Object? parentThumbPath = freezed,Object? parentIndex = freezed,Object? index = freezed,Object? grandparentId = freezed,Object? grandparentTitle = freezed,Object? grandparentThumbPath = freezed,Object? grandparentArtPath = freezed,Object? grandparentBackdropPaths = freezed,Object? thumbPath = freezed,Object? artPath = freezed,Object? backdropPaths = freezed,Object? clearLogoPath = freezed,Object? backgroundSquarePath = freezed,Object? durationMs = freezed,Object? viewOffsetMs = freezed,Object? viewCount = freezed,Object? lastViewedAt = freezed,Object? leafCount = freezed,Object? viewedLeafCount = freezed,Object? childCount = freezed,Object? addedAt = freezed,Object? updatedAt = freezed,Object? rating = freezed,Object? userRating = freezed,Object? isFavorite = freezed,Object? genres = freezed,Object? directors = freezed,Object? writers = freezed,Object? producers = freezed,Object? countries = freezed,Object? collections = freezed,Object? labels = freezed,Object? styles = freezed,Object? moods = freezed,Object? roles = freezed,Object? mediaVersions = freezed,Object? libraryId = freezed,Object? libraryTitle = freezed,Object? audioLanguage = freezed,Object? playlistItemId = freezed,Object? serverId = freezed,Object? serverName = freezed,Object? backendFolderKey = freezed,Object? raw = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? kind = null,Object? guid = freezed,Object? title = freezed,Object? titleSort = freezed,Object? summary = freezed,Object? tagline = freezed,Object? originalTitle = freezed,Object? studio = freezed,Object? year = freezed,Object? originallyAvailableAt = freezed,Object? contentRating = freezed,Object? parentId = freezed,Object? parentTitle = freezed,Object? parentThumbPath = freezed,Object? parentIndex = freezed,Object? index = freezed,Object? grandparentId = freezed,Object? grandparentTitle = freezed,Object? grandparentThumbPath = freezed,Object? grandparentArtPath = freezed,Object? grandparentBackdropPaths = freezed,Object? thumbPath = freezed,Object? artPath = freezed,Object? backdropPaths = freezed,Object? clearLogoPath = freezed,Object? backgroundSquarePath = freezed,Object? durationMs = freezed,Object? viewOffsetMs = freezed,Object? viewCount = freezed,Object? lastViewedAt = freezed,Object? leafCount = freezed,Object? viewedLeafCount = freezed,Object? childCount = freezed,Object? addedAt = freezed,Object? updatedAt = freezed,Object? rating = freezed,Object? userRating = freezed,Object? ratings = freezed,Object? isFavorite = freezed,Object? genres = freezed,Object? directors = freezed,Object? writers = freezed,Object? producers = freezed,Object? countries = freezed,Object? collections = freezed,Object? labels = freezed,Object? styles = freezed,Object? moods = freezed,Object? roles = freezed,Object? mediaVersions = freezed,Object? libraryId = freezed,Object? libraryTitle = freezed,Object? audioLanguage = freezed,Object? playlistItemId = freezed,Object? serverId = freezed,Object? serverName = freezed,Object? backendFolderKey = freezed,Object? raw = freezed,}) { return _then(JellyfinMediaItem( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,kind: null == kind ? _self.kind : kind // ignore: cast_nullable_to_non_nullable @@ -601,7 +609,8 @@ as int?,addedAt: freezed == addedAt ? _self.addedAt : addedAt // ignore: cast_nu as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as int?,rating: freezed == rating ? _self.rating : rating // ignore: cast_nullable_to_non_nullable as double?,userRating: freezed == userRating ? _self.userRating : userRating // ignore: cast_nullable_to_non_nullable -as double?,isFavorite: freezed == isFavorite ? _self.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable +as double?,ratings: freezed == ratings ? _self.ratings : ratings // ignore: cast_nullable_to_non_nullable +as List?,isFavorite: freezed == isFavorite ? _self.isFavorite : isFavorite // ignore: cast_nullable_to_non_nullable as bool?,genres: freezed == genres ? _self.genres : genres // ignore: cast_nullable_to_non_nullable as List?,directors: freezed == directors ? _self.directors : directors // ignore: cast_nullable_to_non_nullable as List?,writers: freezed == writers ? _self.writers : writers // ignore: cast_nullable_to_non_nullable diff --git a/lib/media/media_item.g.dart b/lib/media/media_item.g.dart index 267dc4a2..d31fd90a 100644 --- a/lib/media/media_item.g.dart +++ b/lib/media/media_item.g.dart @@ -51,11 +51,9 @@ PlexMediaItem _$PlexMediaItemFromJson(Map json) => addedAt: flexibleInt(json['addedAt']), updatedAt: flexibleInt(json['updatedAt']), rating: flexibleDouble(json['rating']), - audienceRating: flexibleDouble(json['audienceRating']), userRating: flexibleDouble(json['userRating']), + ratings: _mediaItemRatingsFromJson(json['ratings']), isFavorite: json['isFavorite'] as bool?, - ratingImage: json['ratingImage'] as String?, - audienceRatingImage: json['audienceRatingImage'] as String?, genres: _mediaItemStringList(json['genres']), directors: _mediaItemStringList(json['directors']), writers: _mediaItemStringList(json['writers']), @@ -123,11 +121,9 @@ Map _$PlexMediaItemToJson(PlexMediaItem instance) => 'addedAt': ?instance.addedAt, 'updatedAt': ?instance.updatedAt, 'rating': ?instance.rating, - 'audienceRating': ?instance.audienceRating, 'userRating': ?instance.userRating, + 'ratings': ?instance.ratings?.map((e) => e.toJson()).toList(), 'isFavorite': ?instance.isFavorite, - 'ratingImage': ?instance.ratingImage, - 'audienceRatingImage': ?instance.audienceRatingImage, 'genres': ?instance.genres, 'directors': ?instance.directors, 'writers': ?instance.writers, @@ -200,6 +196,7 @@ JellyfinMediaItem _$JellyfinMediaItemFromJson(Map json) => updatedAt: flexibleInt(json['updatedAt']), rating: flexibleDouble(json['rating']), userRating: flexibleDouble(json['userRating']), + ratings: _mediaItemRatingsFromJson(json['ratings']), isFavorite: json['isFavorite'] as bool?, genres: _mediaItemStringList(json['genres']), directors: _mediaItemStringList(json['directors']), @@ -262,6 +259,7 @@ Map _$JellyfinMediaItemToJson(JellyfinMediaItem instance) => 'updatedAt': ?instance.updatedAt, 'rating': ?instance.rating, 'userRating': ?instance.userRating, + 'ratings': ?instance.ratings?.map((e) => e.toJson()).toList(), 'isFavorite': ?instance.isFavorite, 'genres': ?instance.genres, 'directors': ?instance.directors, diff --git a/lib/media/media_rating.dart b/lib/media/media_rating.dart new file mode 100644 index 00000000..27c510f9 --- /dev/null +++ b/lib/media/media_rating.dart @@ -0,0 +1,37 @@ +/// One attributed score for a media item. +/// +/// Backends routinely return several: Plex's `/library/metadata/{id}` carries +/// an IMDb score next to both Rotten Tomatoes panels and TMDB, Simkl carries +/// Simkl/IMDb/MAL side by side, Jellyfin carries a community score next to a +/// Tomatometer. `MediaItem.rating` keeps the backend's headline number; this +/// list keeps every score with attribution so the UI can badge and label them. +/// +/// Serialization is hand-written rather than `json_serializable` because +/// catalog objects round-trip through `MediaItem.raw` maps rather than a +/// generated codec. +library; + +class MediaRatingSource { + /// Stable, lowercase source key: `imdb`, `tmdb`, `rottenTomatoesCritic`, + /// `rottenTomatoesAudience`, `simkl`, `mal`, `anilist`, `trakt`, `critic`, + /// `audience`. Rendered through a label map, never raw. + final String source; + + /// Normalized to 0-10 by the mapper, matching `MediaItem.rating`. Percentage + /// sources (Rotten Tomatoes, TMDB) are rendered back as `value * 10`%. + final double value; + + /// How many users the score is based on, when the backend says. + final int? votes; + + const MediaRatingSource({required this.source, required this.value, this.votes}); + + Map toJson() => {'source': source, 'value': value, if (votes != null) 'votes': votes}; + + static MediaRatingSource? fromJson(Map json) { + final source = json['source'] as String?; + final value = (json['value'] as num?)?.toDouble(); + if (source == null || value == null) return null; + return MediaRatingSource(source: source, value: value, votes: json['votes'] as int?); + } +} diff --git a/lib/models/catalog/catalog_item.dart b/lib/models/catalog/catalog_item.dart index c502067b..8ee5ee57 100644 --- a/lib/models/catalog/catalog_item.dart +++ b/lib/models/catalog/catalog_item.dart @@ -1,6 +1,7 @@ import '../../media/media_backend.dart'; import '../../media/media_item.dart'; import '../../media/media_kind.dart'; +import '../../media/media_rating.dart'; import '../../utils/external_ids.dart'; import 'catalog_cast_member.dart'; import 'catalog_metadata.dart'; @@ -213,7 +214,7 @@ class CatalogItem { final String? accentColor; /// Scores beyond [rating], each with its own source label. - final List? ratings; + final List? ratings; /// Leaderboard positions. A list because providers return several windows /// at once (all-time and seasonal). @@ -493,6 +494,7 @@ class CatalogItem { contentRating: certification, durationMs: runtimeMinutes == null ? null : Duration(minutes: runtimeMinutes!).inMilliseconds, rating: rating, + ratings: ratings, genres: genres, thumbPath: posterUrl, artPath: backdropUrl, @@ -594,7 +596,7 @@ class CatalogItem { logoUrl: json['logoUrl'] as String?, bannerUrl: json['bannerUrl'] as String?, accentColor: json['accentColor'] as String?, - ratings: decodeCatalogList(json['ratings'], CatalogRatingSource.fromJson), + ratings: decodeCatalogList(json['ratings'], MediaRatingSource.fromJson), ranks: decodeCatalogList(json['ranks'], CatalogRank.fromJson), audience: _decodeObject(json['audience'], CatalogAudience.fromJson), broadcast: _decodeObject(json['broadcast'], CatalogBroadcast.fromJson), diff --git a/lib/models/catalog/catalog_metadata.dart b/lib/models/catalog/catalog_metadata.dart index 92f050c7..120c7b5d 100644 --- a/lib/models/catalog/catalog_metadata.dart +++ b/lib/models/catalog/catalog_metadata.dart @@ -8,35 +8,6 @@ /// round-trip through `MediaItem.raw` inside a single session. library; -/// One provider score with its own source label. -/// -/// Providers routinely return several: Simkl carries Simkl/IMDb/MAL side by -/// side, Plex carries a critic score next to an audience score. The neutral -/// [CatalogItem.rating] keeps the provider's headline number; this list keeps -/// the rest with attribution so the UI can label them. -class CatalogRatingSource { - /// Stable, lowercase source key: `simkl`, `imdb`, `mal`, `anilist`, `tmdb`, - /// `trakt`, `critic`, `audience`. Rendered through a label map, never raw. - final String source; - - /// Normalized to 0-10 by the mapper, matching [CatalogItem.rating]. - final double value; - - /// How many users the score is based on, when the provider says. - final int? votes; - - const CatalogRatingSource({required this.source, required this.value, this.votes}); - - Map toJson() => {'source': source, 'value': value, if (votes != null) 'votes': votes}; - - static CatalogRatingSource? fromJson(Map json) { - final source = json['source'] as String?; - final value = (json['value'] as num?)?.toDouble(); - if (source == null || value == null) return null; - return CatalogRatingSource(source: source, value: value, votes: json['votes'] as int?); - } -} - /// What a leaderboard position is a position *in*. /// /// MAL returns one rank per ranking endpoint, AniList returns a `rankings` diff --git a/lib/screens/catalog_item_detail_screen.dart b/lib/screens/catalog_item_detail_screen.dart index d0f736cb..1af289cd 100644 --- a/lib/screens/catalog_item_detail_screen.dart +++ b/lib/screens/catalog_item_detail_screen.dart @@ -19,6 +19,7 @@ import '../i18n/app_locale_utils.dart'; import '../i18n/strings.g.dart'; import '../media/media_hub.dart'; import '../media/media_item.dart'; +import '../media/media_rating.dart'; import '../models/catalog/catalog_cast_member.dart'; import '../models/catalog/catalog_item.dart'; import '../models/catalog/catalog_metadata.dart'; @@ -617,23 +618,6 @@ class _CatalogItemDetailScreenState extends State { CatalogCreditRole.composer => t.explore.creditRole.composer, }; - static String? _ratingSourceLabel(String source) => switch (source) { - 'critic' => t.explore.ratingSource.critic, - 'audience' => t.explore.ratingSource.audience, - 'imdb' => t.explore.ratingSource.imdb, - 'tmdb' => t.explore.ratingSource.tmdb, - 'rottenTomatoes' => t.explore.ratingSource.rottenTomatoes, - // Plex splits Rotten Tomatoes into its two panels, so both the provenance - // and the critic/audience distinction survive. - 'rottenTomatoesCritic' => t.explore.ratingSource.rottenTomatoesCritic, - 'rottenTomatoesAudience' => t.explore.ratingSource.rottenTomatoesAudience, - 'simkl' => t.explore.ratingSource.simkl, - 'mal' => t.explore.ratingSource.mal, - 'anilist' => t.explore.ratingSource.anilist, - 'trakt' => t.explore.ratingSource.trakt, - _ => null, - }; - static String _relationLabel(CatalogRelationType type) => switch (type) { CatalogRelationType.prequel => t.explore.relation.prequel, CatalogRelationType.sequel => t.explore.relation.sequel, @@ -795,9 +779,9 @@ class _CatalogItemDetailScreenState extends State { Widget? _buildRatingsSection(ThemeData theme) { final compact = NumberFormat.compact(locale: LocaleSettings.currentLocale.intlLocaleName); final chips = []; - for (final rating in _item.ratings ?? const []) { - final source = _ratingSourceLabel(rating.source); - final badge = catalogRatingInfo(rating.source, rating.value); + for (final rating in _item.ratings ?? const []) { + final source = ratingSourceLabel(rating.source); + final badge = ratingInfoForSource(rating.source, rating.value); if (source == null && badge == null) continue; var label = badge == null ? '$source ${rating.value.toStringAsFixed(1)}' : badge.formattedValue; if (rating.votes case final votes?) { diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index 424f8a8c..aee6041c 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -50,7 +50,6 @@ import '../services/plex_client.dart'; import '../media/media_server_client.dart'; import '../services/media_list_playback_launcher.dart'; import '../utils/content_utils.dart'; -import '../utils/rating_utils.dart'; import '../models/download_models.dart'; import '../services/download_storage_service.dart'; import '../utils/download_version_utils.dart'; @@ -961,59 +960,28 @@ class _MediaDetailScreenState extends State ); } - /// Build a rating chip that shows a source icon when available, - /// falling back to a generic Material icon. - Widget _buildRatingChip(String? imageUri, double value, IconData fallbackIcon) { + /// Every attributed score in one pill, then the tappable user-rating chip. + /// + /// The scores share a single pill rather than taking a chip each: this row + /// is a height-clipped [Wrap], so a per-source chip would push the year, + /// certification, and runtime out of the visible band on short heroes. + List _buildRatingChips(MediaItem metadata) { final colorScheme = Theme.of(context).colorScheme; final isTv = PlatformDetector.isTV(); - return MediaRatingBadge.chip( - imageUri: imageUri, - value: value, - fallbackIcon: fallbackIcon, - foregroundColor: colorScheme.onSecondaryContainer, - backgroundColor: colorScheme.secondaryContainer.withValues(alpha: 0.8), - iconSize: isTv ? 20 : 16, - spacing: isTv ? 6 : 4, - padding: EdgeInsets.symmetric(horizontal: isTv ? 14 : 12, vertical: isTv ? 8 : 6), - textStyle: TextStyle(color: colorScheme.onSecondaryContainer, fontSize: isTv ? 16 : 13, fontWeight: .w600), - ); - } - - /// Build all rating chips for the metadata. - /// When both critic and audience ratings are from Rotten Tomatoes, - /// they are combined into a single badge. - List _buildRatingChips(MediaItem metadata) { - final chips = []; - // Plex-only fields (audienceRating / ratingImage / audienceRatingImage) - // — Jellyfin lacks rating-source attribution. Pull them via a typed - // narrow so the rest of the chip layout stays backend-neutral. - final plex = metadata is PlexMediaItem ? metadata : null; - final audienceRating = plex?.audienceRating; - final ratingImage = plex?.ratingImage; - final audienceRatingImage = plex?.audienceRatingImage; - final bothRT = - metadata.rating != null && - audienceRating != null && - isRottenTomatoes(ratingImage) && - isRottenTomatoes(audienceRatingImage); - - if (bothRT) { - chips.add(_buildCombinedRtChip(ratingImage, metadata.rating!, audienceRatingImage, audienceRating)); - } else { - if (metadata.rating != null) { - chips.add(_buildRatingChip(ratingImage, metadata.rating!, Symbols.star_rounded)); - } - if (audienceRating != null) { - chips.add(_buildRatingChip(audienceRatingImage, audienceRating, Symbols.people_rounded)); - } - } - - // User rating chip (tappable) - if (!widget.isOffline) { - chips.add(_buildUserRatingChip(metadata)); - } - - return chips; + return [ + if (mediaRatingsFor(metadata).isNotEmpty) + MediaRatingBadgeGroup.chip( + item: metadata, + foregroundColor: colorScheme.onSecondaryContainer, + backgroundColor: colorScheme.secondaryContainer.withValues(alpha: 0.8), + iconSize: isTv ? 20 : 16, + spacing: isTv ? 6 : 4, + entrySpacing: isTv ? 12 : 10, + padding: EdgeInsets.symmetric(horizontal: isTv ? 14 : 12, vertical: isTv ? 8 : 6), + textStyle: TextStyle(color: colorScheme.onSecondaryContainer, fontSize: isTv ? 16 : 13, fontWeight: .w600), + ), + if (!widget.isOffline) _buildUserRatingChip(metadata), + ]; } Widget _buildUserRatingChip(MediaItem metadata) { @@ -1111,49 +1079,6 @@ class _MediaDetailScreenState extends State ); } - /// Build a combined RT chip showing critic + audience side by side. - Widget _buildCombinedRtChip( - String? criticImageUri, - double criticValue, - String? audienceImageUri, - double audienceValue, - ) { - final colorScheme = Theme.of(context).colorScheme; - final textStyle = TextStyle(color: colorScheme.onSecondaryContainer, fontSize: 13, fontWeight: .w500); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer.withValues(alpha: 0.8), - borderRadius: const BorderRadius.all(Radius.circular(100)), - ), - child: Row( - mainAxisSize: .min, - children: [ - MediaRatingBadge.inline( - imageUri: criticImageUri, - value: criticValue, - fallbackIcon: Symbols.star_rounded, - foregroundColor: colorScheme.onSecondaryContainer, - iconSize: 16, - spacing: 4, - textStyle: textStyle, - ), - const SizedBox(width: 10), - MediaRatingBadge.inline( - imageUri: audienceImageUri, - value: audienceValue, - fallbackIcon: Symbols.people_rounded, - foregroundColor: colorScheme.onSecondaryContainer, - iconSize: 16, - spacing: 4, - textStyle: textStyle, - ), - ], - ), - ); - } - /// Backend-neutral counterpart of [getServerBoundPlexClient]. Returns a /// [MediaServerClient] for Jellyfin items too, so image URLs use the /// right server's transcoder. @@ -3625,7 +3550,7 @@ class _MediaDetailScreenState extends State final fields = _tvDetailMetadataParts( metadata, text: (value) => value, - rating: (item) => MediaRatingBadge.semanticLabelForMedia(item, fallbackItem: metadata), + rating: (item) => MediaRatingBadgeGroup.semanticLabelForMedia(item, fallbackItem: metadata), ); for (final field in fields) { add(field); @@ -3694,12 +3619,15 @@ class _MediaDetailScreenState extends State final fields = _tvDetailMetadataParts( metadata, text: (value) => Text(value, maxLines: 1, style: textStyle), - rating: (item) => MediaRatingBadge.inlineForMedia( + // Every score shares the one metadata slot, so the bullet separators + // don't multiply with the number of sources. + rating: (item) => MediaRatingBadgeGroup.inlineForMedia( item: item, fallbackItem: metadata, foregroundColor: textStyle.color, iconSize: textStyle.fontSize, spacing: 4 * scale, + entrySpacing: 12 * scale, textStyle: textStyle, ), ); diff --git a/lib/services/catalog/anilist_catalog_source.dart b/lib/services/catalog/anilist_catalog_source.dart index e9c3ec56..7ebe5638 100644 --- a/lib/services/catalog/anilist_catalog_source.dart +++ b/lib/services/catalog/anilist_catalog_source.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; +import '../../media/media_rating.dart'; import '../../media/media_kind.dart'; import '../../models/anilist/anilist_media.dart'; import '../../models/catalog/catalog_cast_member.dart'; @@ -218,8 +219,8 @@ class AnilistCatalogSource with CatalogWatchlistMachinery implements CatalogSour CatalogCastMember(name: character.name, secondary: character.role, imageUrl: character.imageUrl), ]; - static List? _ratingsFor(double? meanRating) => - meanRating == null ? null : [CatalogRatingSource(source: 'anilist', value: meanRating)]; + static List? _ratingsFor(double? meanRating) => + meanRating == null ? null : [MediaRatingSource(source: 'anilist', value: meanRating)]; static CatalogNextEpisode? _nextEpisodeFor(AnilistNextAiringEpisode? airing) => airing == null ? null diff --git a/lib/services/catalog/plex_catalog_source.dart b/lib/services/catalog/plex_catalog_source.dart index 95782497..3c0b5d2e 100644 --- a/lib/services/catalog/plex_catalog_source.dart +++ b/lib/services/catalog/plex_catalog_source.dart @@ -1,3 +1,4 @@ +import '../../media/media_rating.dart'; import '../../media/media_kind.dart'; import '../../models/catalog/catalog_cast_member.dart'; import '../../models/catalog/catalog_item.dart'; @@ -7,6 +8,7 @@ import '../../utils/app_logger.dart'; import '../../utils/country_codes.dart'; import '../../utils/json_utils.dart'; import '../plex_discover_client.dart'; +import '../plex_mappers.dart'; import 'catalog_source.dart'; import 'catalog_watchlist_machinery.dart'; @@ -259,8 +261,8 @@ class PlexCatalogSource with CatalogWatchlistMachinery implements CatalogSource, } } - final headlineRating = _normalizedRating(metadata['rating']); - final audienceRating = _normalizedRating(metadata['audienceRating']); + final headlineRating = normalizedPlexRating(metadata['rating']); + final audienceRating = normalizedPlexRating(metadata['audienceRating']); return CatalogItem( source: CatalogSourceId.plex, kind: kind, @@ -343,46 +345,17 @@ class PlexCatalogSource with CatalogWatchlistMachinery implements CatalogSource, return credits.isEmpty ? null : credits; } - static List? _ratingsFor(Map metadata) { - final ratings = []; - final imdbVotes = flexibleInt(metadata['imdbRatingCount']); - - void add(Object? rawValue, {required String fallbackSource, Object? image, Object? type}) { - final value = _normalizedRating(rawValue); - if (value == null) return; - final source = _ratingSource(image: image, type: type, fallback: fallbackSource); - if (ratings.any((rating) => rating.source == source && rating.value == value)) return; - ratings.add(CatalogRatingSource(source: source, value: value, votes: source == 'imdb' ? imdbVotes : null)); - } - - add(metadata['rating'], fallbackSource: 'critic', image: metadata['ratingImage']); - add(metadata['audienceRating'], fallbackSource: 'audience', image: metadata['audienceRatingImage']); - for (final rating in flexibleMapList(metadata['Rating'])) { - add(rating['value'], fallbackSource: 'audience', image: rating['image'], type: rating['type']); - } - return ratings.isEmpty ? null : ratings; - } - - static String _ratingSource({Object? image, Object? type, required String fallback}) { - final scheme = Uri.tryParse(_nonEmptyString(image) ?? '')?.scheme.toLowerCase(); - if (scheme == 'imdb') return 'imdb'; - if (scheme == 'themoviedb' || scheme == 'tmdb') return 'tmdb'; - final ratingType = _nonEmptyString(type)?.toLowerCase() ?? fallback; - if (scheme == 'rottentomatoes') { - return ratingType == 'critic' ? 'rottenTomatoesCritic' : 'rottenTomatoesAudience'; - } - return switch (ratingType) { - 'critic' => 'critic', - 'audience' => 'audience', - _ => fallback, - }; - } - - static double? _normalizedRating(Object? value) { - final rating = flexibleDouble(value); - if (rating == null || !rating.isFinite || rating < 0 || rating > 100) return null; - return rating > 10 ? rating / 10 : rating; - } + static List? _ratingsFor(Map metadata) => plexRatingSources( + rating: metadata['rating'], + ratingImage: metadata['ratingImage'], + audienceRating: metadata['audienceRating'], + audienceRatingImage: metadata['audienceRatingImage'], + ratingSources: [ + for (final rating in flexibleMapList(metadata['Rating'])) + (image: rating['image'], type: rating['type'], value: rating['value']), + ], + imdbVotes: flexibleInt(metadata['imdbRatingCount']), + ); static String? _overviewFor(Map metadata) { var overview = _nonEmptyString(metadata['summary']); diff --git a/lib/services/catalog/simkl_catalog_source.dart b/lib/services/catalog/simkl_catalog_source.dart index 13889164..04404c51 100644 --- a/lib/services/catalog/simkl_catalog_source.dart +++ b/lib/services/catalog/simkl_catalog_source.dart @@ -1,3 +1,4 @@ +import '../../media/media_rating.dart'; import '../../media/media_kind.dart'; import '../../models/catalog/catalog_metadata.dart'; import '../../models/catalog/catalog_item.dart'; @@ -211,14 +212,14 @@ class SimklCatalogSource with CatalogWatchlistMachinery implements CatalogSource if (type == SimklCatalogType.anime && simklId != null) _animeIds.add(simklId); } - static List? _ratingsFor(SimklRatings? ratings) { + static List? _ratingsFor(SimklRatings? ratings) { if (ratings == null) return null; - final result = []; + final result = []; for (final (source, rating) in [('simkl', ratings.simkl), ('imdb', ratings.imdb), ('mal', ratings.mal)]) { final value = rating?.rating; if (value == null || value < 0 || value > 10) continue; final votes = rating!.votes; - result.add(CatalogRatingSource(source: source, value: value, votes: votes != null && votes >= 0 ? votes : null)); + result.add(MediaRatingSource(source: source, value: value, votes: votes != null && votes >= 0 ? votes : null)); } return result.isEmpty ? null : result; } diff --git a/lib/services/jellyfin_mappers.dart b/lib/services/jellyfin_mappers.dart index ebbb9612..25d6cad7 100644 --- a/lib/services/jellyfin_mappers.dart +++ b/lib/services/jellyfin_mappers.dart @@ -5,6 +5,7 @@ import '../media/media_item.dart'; import '../media/media_kind.dart'; import '../media/media_library.dart'; import '../media/media_part.dart'; +import '../media/media_rating.dart'; import '../media/media_role.dart'; import '../media/media_stream.dart'; import '../media/media_version.dart'; @@ -236,6 +237,7 @@ class JellyfinMappers { addedAt: jellyfinIsoToEpochSeconds(item['DateCreated'] as String?), updatedAt: jellyfinIsoToEpochSeconds(item['DateLastSaved'] as String? ?? item['DateModified'] as String?), rating: (item['CommunityRating'] as num?)?.toDouble(), + ratings: _ratingSources(item, kind), isFavorite: _userData(item)?['IsFavorite'] as bool?, genres: _stringList(item['Genres']), directors: _peopleByType(item['People'], 'Director'), @@ -368,6 +370,36 @@ class JellyfinMappers { return total - unplayed; } + /// Jellyfin's two rating slots, community score first. + /// + /// `BaseItemDto` has no per-source array — the server collapses whatever the + /// metadata fetchers found into these two fields, so this is at most two + /// entries. Both arrive on every response; neither is gated behind `Fields`. + /// + /// `CommunityRating` is 0-10 but its provenance is unknowable from the DTO + /// (TMDB `vote_average`, IMDb via OMDb, or a local NFO — last writer wins), + /// so it stays the generic `audience` source with no brand badge. + /// `CriticRating` is the Rotten Tomatoes Tomatometer as a 0-100 percent, so + /// it is divided rather than range-sniffed: a Tomatometer of 9 means 9%. + static List? _ratingSources(Map item, MediaKind kind) { + // Photo items reuse CommunityRating for the EXIF 0-5 star rating. + if (kind == MediaKind.photo) return null; + + final ratings = []; + if (_finiteRating(item['CommunityRating']) case final community? when community >= 0 && community <= 10) { + ratings.add(MediaRatingSource(source: 'audience', value: community)); + } + if (_finiteRating(item['CriticRating']) case final critic? when critic >= 0 && critic <= 100) { + ratings.add(MediaRatingSource(source: 'rottenTomatoesCritic', value: critic / 10)); + } + return ratings.isEmpty ? null : ratings; + } + + static double? _finiteRating(Object? value) { + final rating = flexibleDouble(value); + return rating != null && rating.isFinite ? rating : null; + } + static String? _firstString(Object? list) { if (list is List && list.isNotEmpty && list.first is String) return list.first as String; return null; diff --git a/lib/services/plex_mappers.dart b/lib/services/plex_mappers.dart index 5859e0ee..55e07945 100644 --- a/lib/services/plex_mappers.dart +++ b/lib/services/plex_mappers.dart @@ -23,6 +23,7 @@ import '../media/media_kind.dart'; import '../media/media_library.dart'; import '../media/media_part.dart'; import '../media/media_playlist.dart'; +import '../media/media_rating.dart'; import '../media/media_role.dart'; import '../media/media_source_info.dart'; import '../media/media_stream.dart'; @@ -300,6 +301,24 @@ class PlexRoleDto { factory PlexRoleDto.fromJson(Map json) => _$PlexRoleDtoFromJson(json); } +/// One entry of Plex's `Rating[]` child array, present on +/// `/library/metadata/{id}` responses but never on section listings. +/// +/// [image] is a source URI (`imdb://image.rating`, +/// `rottentomatoes://image.rating.ripe`, `themoviedb://image.rating`) and +/// [type] is `critic` or `audience`; together they attribute [value]. +@JsonSerializable(createToJson: false) +class PlexRatingDto { + final String? image; + final String? type; + @JsonKey(fromJson: flexibleDouble) + final double? value; + + const PlexRatingDto({this.image, this.type, this.value}); + + factory PlexRatingDto.fromJson(Map json) => _$PlexRatingDtoFromJson(json); +} + @JsonSerializable(createToJson: false) class PlexMediaVersionDto { @JsonKey(fromJson: flexibleIntOrZero) @@ -605,6 +624,10 @@ class PlexMetadataDto { final List? role; @JsonKey(name: 'Media', includeToJson: false) final List? mediaVersions; + @JsonKey(name: 'Rating', includeToJson: false) + final List? ratingSources; + @JsonKey(fromJson: flexibleInt) + final int? imdbRatingCount; @JsonKey(name: 'Genre', fromJson: _tagListFromJson, includeToJson: false) final List? genre; @JsonKey(name: 'Director', fromJson: _tagListFromJson, includeToJson: false) @@ -693,6 +716,8 @@ class PlexMetadataDto { this.childCount, this.role, this.mediaVersions, + this.ratingSources, + this.imdbRatingCount, this.genre, this.director, this.writer, @@ -825,6 +850,8 @@ class PlexMetadataDto { int? childCount, List? role, List? mediaVersions, + List? ratingSources, + int? imdbRatingCount, List? genre, List? director, List? writer, @@ -895,6 +922,8 @@ class PlexMetadataDto { childCount: childCount ?? this.childCount, role: role ?? this.role, mediaVersions: mediaVersions ?? this.mediaVersions, + ratingSources: ratingSources ?? this.ratingSources, + imdbRatingCount: imdbRatingCount ?? this.imdbRatingCount, genre: genre ?? this.genre, director: director ?? this.director, writer: writer ?? this.writer, @@ -937,6 +966,75 @@ Map? _rawMetadata(PlexMetadataDto dto) { return raw.isEmpty ? null : raw; } +/// Every attributed score a Plex payload carries, headline first. +/// +/// Section listings only ever send the scalar pair ([rating]/[ratingImage] and +/// [audienceRating]/[audienceRatingImage]), so they yield one or two entries. +/// `/library/metadata/{id}` additionally sends the `Rating[]` child array — +/// IMDb, both Rotten Tomatoes panels, TMDB — with no extra query parameter, so +/// detail responses yield up to four. Plex offers no listing parameter that +/// includes the array, which is why cards stay shorter than detail screens. +/// +/// Entries are deduped on `(source, value)` because the array repeats whichever +/// source the scalar pair was drawn from. Insertion order keeps the server's +/// own headline first, so `ratings.first` matches the scalar `rating` the +/// cards and sorts already use. +List? plexRatingSources({ + Object? rating, + Object? ratingImage, + Object? audienceRating, + Object? audienceRatingImage, + Iterable<({Object? image, Object? type, Object? value})> ratingSources = const [], + int? imdbVotes, +}) { + final ratings = []; + + void add(Object? rawValue, {required String fallbackSource, Object? image, Object? type}) { + final value = normalizedPlexRating(rawValue); + if (value == null) return; + final source = _plexRatingSource(image: image, type: type, fallback: fallbackSource); + if (ratings.any((rating) => rating.source == source && rating.value == value)) return; + ratings.add(MediaRatingSource(source: source, value: value, votes: source == 'imdb' ? imdbVotes : null)); + } + + add(rating, fallbackSource: 'critic', image: ratingImage); + add(audienceRating, fallbackSource: 'audience', image: audienceRatingImage); + for (final entry in ratingSources) { + add(entry.value, fallbackSource: 'audience', image: entry.image, type: entry.type); + } + return ratings.isEmpty ? null : ratings; +} + +String _plexRatingSource({Object? image, Object? type, required String fallback}) { + final scheme = Uri.tryParse(_nonEmptyRatingString(image) ?? '')?.scheme.toLowerCase(); + if (scheme == 'imdb') return 'imdb'; + if (scheme == 'themoviedb' || scheme == 'tmdb') return 'tmdb'; + final ratingType = _nonEmptyRatingString(type)?.toLowerCase() ?? fallback; + if (scheme == 'rottentomatoes') { + return ratingType == 'critic' ? 'rottenTomatoesCritic' : 'rottenTomatoesAudience'; + } + return switch (ratingType) { + 'critic' => 'critic', + 'audience' => 'audience', + _ => fallback, + }; +} + +/// Coerce a Plex score onto the neutral 0-10 scale, rejecting out-of-range +/// noise. Plex sends percentages as 0-10 already; a stray 0-100 value is +/// folded rather than dropped. +double? normalizedPlexRating(Object? value) { + final rating = flexibleDouble(value); + if (rating == null || !rating.isFinite || rating < 0 || rating > 100) return null; + return rating > 10 ? rating / 10 : rating; +} + +String? _nonEmptyRatingString(Object? value) { + if (value is! String) return null; + final trimmed = value.trim(); + return trimmed.isEmpty ? null : trimmed; +} + /// Pure JSON/DTO→neutral-type mappers for Plex. Mirrors [JellyfinMappers]. /// /// Methods come in two flavours: @@ -1011,10 +1109,18 @@ class PlexMappers { addedAt: dto.addedAt, updatedAt: dto.updatedAt, rating: dto.rating, - audienceRating: dto.audienceRating, userRating: dto.userRating, - ratingImage: dto.ratingImage, - audienceRatingImage: dto.audienceRatingImage, + ratings: plexRatingSources( + rating: dto.rating, + ratingImage: dto.ratingImage, + audienceRating: dto.audienceRating, + audienceRatingImage: dto.audienceRatingImage, + ratingSources: [ + for (final entry in dto.ratingSources ?? const []) + (image: entry.image, type: entry.type, value: entry.value), + ], + imdbVotes: dto.imdbRatingCount, + ), genres: dto.genre, directors: dto.director, writers: dto.writer, diff --git a/lib/services/plex_mappers.g.dart b/lib/services/plex_mappers.g.dart index 03f8d784..9ee1709d 100644 --- a/lib/services/plex_mappers.g.dart +++ b/lib/services/plex_mappers.g.dart @@ -16,6 +16,13 @@ PlexRoleDto _$PlexRoleDtoFromJson(Map json) => PlexRoleDto( count: flexibleInt(json['count']), ); +PlexRatingDto _$PlexRatingDtoFromJson(Map json) => + PlexRatingDto( + image: json['image'] as String?, + type: json['type'] as String?, + value: flexibleDouble(json['value']), + ); + PlexMediaVersionDto _$PlexMediaVersionDtoFromJson(Map json) => PlexMediaVersionDto( id: flexibleIntOrZero(json['id']), @@ -123,6 +130,10 @@ PlexMetadataDto _$PlexMetadataDtoFromJson(Map json) => mediaVersions: (json['Media'] as List?) ?.map((e) => PlexMediaVersionDto.fromJson(e as Map)) .toList(), + ratingSources: (json['Rating'] as List?) + ?.map((e) => PlexRatingDto.fromJson(e as Map)) + .toList(), + imdbRatingCount: flexibleInt(json['imdbRatingCount']), genre: _tagListFromJson(json['Genre']), director: _tagListFromJson(json['Director']), writer: _tagListFromJson(json['Writer']), @@ -191,6 +202,7 @@ Map _$PlexMetadataDtoToJson(PlexMetadataDto instance) => 'leafCount': ?instance.leafCount, 'viewedLeafCount': ?instance.viewedLeafCount, 'childCount': ?instance.childCount, + 'imdbRatingCount': ?instance.imdbRatingCount, 'audioLanguage': ?instance.audioLanguage, 'subtitleLanguage': ?instance.subtitleLanguage, 'subtitleMode': ?instance.subtitleMode, diff --git a/lib/utils/rating_utils.dart b/lib/utils/rating_utils.dart index 2fe2931a..667827fd 100644 --- a/lib/utils/rating_utils.dart +++ b/lib/utils/rating_utils.dart @@ -1,8 +1,9 @@ -/// Resolves the brand badge shown beside a score: Plex's -/// ratingImage/audienceRatingImage URIs, and the attributed source keys the -/// Explore catalog carries instead of them. +/// Resolves the brand badge and written label shown beside a score, keyed by +/// the attributed source name every backend mapper normalizes to. library; +import '../i18n/strings.g.dart'; + class RatingInfo { final String assetPath; final String formattedValue; @@ -10,49 +11,16 @@ class RatingInfo { const RatingInfo(this.assetPath, this.formattedValue); } -/// Parse a ratingImage URI (e.g. "rottentomatoes://image.rating.ripe") -/// together with the numeric rating value into a [RatingInfo]. +/// The brand badge for an attributed source key. /// -/// Returns null if the URI is unrecognised. -RatingInfo? parseRatingImage(String? imageUri, double? value) { - if (imageUri == null || value == null) return null; - - if (imageUri.startsWith('rottentomatoes://image.rating.')) { - final suffix = imageUri.substring('rottentomatoes://image.rating.'.length); - return switch (suffix) { - 'ripe' => RatingInfo(_rtFreshAsset, _percent(value)), - 'rotten' => RatingInfo(_rtRottenAsset, _percent(value)), - 'upright' => RatingInfo(_rtUprightAsset, _percent(value)), - 'spilled' => RatingInfo(_rtSpilledAsset, _percent(value)), - _ => null, - }; - } - - if (imageUri.startsWith('imdb://')) { - return RatingInfo(_imdbAsset, value.toStringAsFixed(1)); - } - - if (imageUri.startsWith('themoviedb://')) { - return RatingInfo(_tmdbAsset, _percent(value)); - } - - return null; -} - -/// Whether the URI is a Rotten Tomatoes rating source. -bool isRottenTomatoes(String? imageUri) => imageUri != null && imageUri.startsWith('rottentomatoes://'); - -/// The same badge for a [CatalogRatingSource]-style source key. -/// -/// Catalog providers attribute their scores by name (`imdb`, `tmdb`, -/// `rottenTomatoesCritic`, …) and publish no badge URI, so the icon is chosen -/// from the key. Rotten Tomatoes picks fresh/rotten and upright/spilled by the -/// 60% threshold the tomatometer itself uses — the same state Plex encodes in -/// `image.rating.ripe` / `.rotten`. +/// Rotten Tomatoes picks fresh/rotten and upright/spilled by the 60% threshold +/// the tomatometer itself uses — the same state Plex encodes in +/// `image.rating.ripe` / `.rotten` and Jellyfin's own web client applies to +/// `CriticRating`. /// /// Returns null for keys with no brand badge (`critic`, `audience`, `simkl`, /// `mal`, `anilist`, `trakt`); those stay labelled with their source name. -RatingInfo? catalogRatingInfo(String source, double value) => switch (source) { +RatingInfo? ratingInfoForSource(String source, double value) => switch (source) { 'imdb' => RatingInfo(_imdbAsset, value.toStringAsFixed(1)), 'tmdb' => RatingInfo(_tmdbAsset, _percent(value)), 'rottenTomatoes' || @@ -64,6 +32,25 @@ RatingInfo? catalogRatingInfo(String source, double value) => switch (source) { _ => null, }; +/// Localized name for an attributed source key, or null when the key is +/// unknown and the score should be dropped rather than labelled raw. +String? ratingSourceLabel(String source) => switch (source) { + 'critic' => t.common.ratingSource.critic, + 'audience' => t.common.ratingSource.audience, + 'imdb' => t.common.ratingSource.imdb, + 'tmdb' => t.common.ratingSource.tmdb, + 'rottenTomatoes' => t.common.ratingSource.rottenTomatoes, + // Plex splits Rotten Tomatoes into its two panels, so both the provenance + // and the critic/audience distinction survive. + 'rottenTomatoesCritic' => t.common.ratingSource.rottenTomatoesCritic, + 'rottenTomatoesAudience' => t.common.ratingSource.rottenTomatoesAudience, + 'simkl' => t.common.ratingSource.simkl, + 'mal' => t.common.ratingSource.mal, + 'anilist' => t.common.ratingSource.anilist, + 'trakt' => t.common.ratingSource.trakt, + _ => null, +}; + const String _imdbAsset = 'assets/rating_icons/imdb.svg'; const String _tmdbAsset = 'assets/rating_icons/tmdb.svg'; const String _rtFreshAsset = 'assets/rating_icons/rt_fresh.svg'; diff --git a/lib/widgets/media_rating_badge.dart b/lib/widgets/media_rating_badge.dart index dde68629..22996670 100644 --- a/lib/widgets/media_rating_badge.dart +++ b/lib/widgets/media_rating_badge.dart @@ -3,132 +3,168 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../media/media_item.dart'; +import '../media/media_rating.dart'; import '../utils/formatters.dart'; import '../utils/rating_utils.dart'; import 'app_icon.dart'; enum MediaRatingBadgeVariant { chip, inline } -class MediaRatingBadge extends StatelessWidget { - const MediaRatingBadge.chip({ +/// Every attributed score for [item], falling back to [fallbackItem] as a +/// whole rather than per-source — a show's ratings never mix with an +/// episode's. +List mediaRatingsFor(MediaItem item, {MediaItem? fallbackItem}) { + final ratings = _ratingsFor(item); + if (ratings.isNotEmpty || fallbackItem == null) return ratings; + return _ratingsFor(fallbackItem); +} + +/// Formatted value for [rating] — the brand badge's own formatting where the +/// source has one, otherwise the neutral 0-10 rendering. +String mediaRatingLabel(MediaRatingSource rating) => + ratingInfoForSource(rating.source, rating.value)?.formattedValue ?? formatRating(rating.value); + +List _ratingsFor(MediaItem item) { + final ratings = item.ratings; + if (ratings != null && ratings.isNotEmpty) return ratings; + // Backends that report a bare score with no provenance (and cached rows + // written before the attributed list existed) still get one badge. + final rating = item.rating; + return rating == null ? const [] : [MediaRatingSource(source: '', value: rating)]; +} + +/// Every attributed score an item carries, side by side. +/// +/// A Plex detail response yields up to four (Rotten Tomatoes critic and +/// audience, IMDb, TMDB); a library listing yields the one or two the server +/// sends with it; Jellyfin yields its community score and Tomatometer. The +/// group renders whatever is there and collapses to a single badge when that +/// is all the response carried — no extra requests are made to lengthen it. +/// +/// `chip` wraps the whole set in one pill so a badge row's element count does +/// not grow with the number of sources. `inline` is the bare row, for +/// single-line metadata strips that already own their own separators. +class MediaRatingBadgeGroup extends StatelessWidget { + const MediaRatingBadgeGroup.chip({ super.key, - required this.value, - required this.fallbackIcon, - this.imageUri, - this.fallbackText, + required this.item, + this.fallbackItem, this.textStyle, this.foregroundColor, this.backgroundColor, this.iconSize, this.padding, this.spacing, + this.entrySpacing, }) : variant = MediaRatingBadgeVariant.chip; - const MediaRatingBadge.inline({ + const MediaRatingBadgeGroup.inline({ super.key, - required this.value, - required this.fallbackIcon, - this.imageUri, - this.fallbackText, + required this.item, + this.fallbackItem, this.textStyle, this.foregroundColor, this.iconSize, this.spacing, + this.entrySpacing, }) : variant = MediaRatingBadgeVariant.inline, backgroundColor = null, padding = EdgeInsets.zero; - final String? imageUri; - final double value; - final IconData fallbackIcon; - final String? fallbackText; + final MediaItem item; + + /// Consulted only when [item] carries no ratings at all — an episode row + /// borrowing its show's scores. + final MediaItem? fallbackItem; final MediaRatingBadgeVariant variant; final TextStyle? textStyle; final Color? foregroundColor; final Color? backgroundColor; final double? iconSize; final EdgeInsetsGeometry? padding; + + /// Gap between a badge's icon and its value. final double? spacing; - static MediaRatingBadge? inlineForMedia({ + /// Gap between adjacent scores. + final double? entrySpacing; + + /// Null when the item has no score at all, so callers can omit the slot + /// instead of rendering an empty pill. + static MediaRatingBadgeGroup? inlineForMedia({ required MediaItem item, MediaItem? fallbackItem, TextStyle? textStyle, Color? foregroundColor, double? iconSize, double? spacing, + double? entrySpacing, }) { - final data = _ratingDataFor(item) ?? (fallbackItem == null ? null : _ratingDataFor(fallbackItem)); - if (data == null) return null; - - return MediaRatingBadge.inline( - imageUri: data.imageUri, - value: data.value, - fallbackIcon: data.fallbackIcon, - fallbackText: data.fallbackText, + if (mediaRatingsFor(item, fallbackItem: fallbackItem).isEmpty) return null; + return MediaRatingBadgeGroup.inline( + item: item, + fallbackItem: fallbackItem, + textStyle: textStyle, foregroundColor: foregroundColor, iconSize: iconSize, spacing: spacing, - textStyle: textStyle, + entrySpacing: entrySpacing, ); } - /// The text exposed by the rendered badge, without building its icon row. + /// Every score read out as ` `, for the metadata-line + /// announcement. Falls back to the bare value where the source is unnamed. static String? semanticLabelForMedia(MediaItem item, {MediaItem? fallbackItem}) { - final data = _ratingDataFor(item) ?? (fallbackItem == null ? null : _ratingDataFor(fallbackItem)); - if (data == null) return null; - return parseRatingImage(data.imageUri, data.value)?.formattedValue ?? data.fallbackText; + final ratings = mediaRatingsFor(item, fallbackItem: fallbackItem); + return ratings.isEmpty ? null : _semanticLabelFor(ratings); } - static _MediaRatingBadgeData? _ratingDataFor(MediaItem item) { - final plex = item is PlexMediaItem ? item : null; - final rating = item.rating; - if (rating != null) { - return _MediaRatingBadgeData( - imageUri: plex?.ratingImage, - value: rating, - fallbackIcon: Symbols.star_rounded, - fallbackText: formatRating(rating), - ); - } - - final audienceRating = plex?.audienceRating; - if (audienceRating != null) { - return _MediaRatingBadgeData( - imageUri: plex?.audienceRatingImage, - value: audienceRating, - fallbackIcon: Symbols.people_rounded, - fallbackText: '${(audienceRating * 10).toStringAsFixed(0)}%', - ); - } - - return null; - } + static String _semanticLabelFor(List ratings) => [ + for (final rating in ratings) + switch (ratingSourceLabel(rating.source)) { + final label? => '$label ${mediaRatingLabel(rating)}', + null => mediaRatingLabel(rating), + }, + ].join(', '); @override Widget build(BuildContext context) { + final ratings = mediaRatingsFor(item, fallbackItem: fallbackItem); + if (ratings.isEmpty) return const SizedBox.shrink(); + final colorScheme = Theme.of(context).colorScheme; final isInline = variant == MediaRatingBadgeVariant.inline; final foreground = foregroundColor ?? (isInline ? colorScheme.onSurface : colorScheme.onSecondaryContainer); - final style = - (textStyle ?? - TextStyle(color: foreground, fontSize: 13, fontWeight: isInline ? FontWeight.w700 : FontWeight.w600)) - .copyWith(color: textStyle?.color ?? foreground); - final size = iconSize ?? style.fontSize ?? 13; - final info = parseRatingImage(imageUri, value); - final label = info?.formattedValue ?? fallbackText ?? '${(value * 10).toStringAsFixed(0)}%'; - final content = Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (info != null) - SvgPicture.asset(info.assetPath, width: size, height: size) - else - AppIcon(fallbackIcon, fill: 1, color: foreground, size: size), - SizedBox(width: spacing ?? (isInline ? 4 : 4)), - Text(label, maxLines: 1, overflow: TextOverflow.clip, style: style), - ], - ); + final style = _resolveTextStyle(textStyle, foreground, isInline); + final gap = entrySpacing ?? 10; + final children = []; + for (final rating in ratings) { + if (children.isNotEmpty) children.add(SizedBox(width: gap)); + children.add( + _buildContent( + source: rating.source, + value: rating.value, + // Plex's own critic/audience split is the only place the generic + // icons still carry meaning; every branded source draws its logo. + fallbackIcon: rating.source == 'audience' ? Symbols.people_rounded : Symbols.star_rounded, + foreground: foreground, + style: style, + iconSize: iconSize, + spacing: spacing, + ), + ); + } + + // A bare row reads as a run of unattributed numbers once more than one + // source is present, so the whole group announces itself as one node + // naming each source. Parents that build their own merged announcement + // (the TV metadata line) exclude this subtree anyway. + final content = Semantics( + label: _semanticLabelFor(ratings), + excludeSemantics: true, + child: Row(mainAxisSize: MainAxisSize.min, children: children), + ); if (isInline) return content; return Container( @@ -142,16 +178,32 @@ class MediaRatingBadge extends StatelessWidget { } } -class _MediaRatingBadgeData { - const _MediaRatingBadgeData({ - required this.value, - required this.fallbackIcon, - required this.fallbackText, - this.imageUri, - }); +TextStyle _resolveTextStyle(TextStyle? textStyle, Color foreground, bool isInline) { + return (textStyle ?? + TextStyle(color: foreground, fontSize: 13, fontWeight: isInline ? FontWeight.w700 : FontWeight.w600)) + .copyWith(color: textStyle?.color ?? foreground); +} - final String? imageUri; - final double value; - final IconData fallbackIcon; - final String fallbackText; +Widget _buildContent({ + required String? source, + required double value, + required IconData fallbackIcon, + required Color foreground, + required TextStyle style, + required double? iconSize, + required double? spacing, +}) { + final size = iconSize ?? style.fontSize ?? 13; + final info = source == null ? null : ratingInfoForSource(source, value); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (info != null) + SvgPicture.asset(info.assetPath, width: size, height: size) + else + AppIcon(fallbackIcon, fill: 1, color: foreground, size: size), + SizedBox(width: spacing ?? 4), + Text(info?.formattedValue ?? formatRating(value), maxLines: 1, overflow: TextOverflow.clip, style: style), + ], + ); } diff --git a/lib/widgets/tv_spotlight_background.dart b/lib/widgets/tv_spotlight_background.dart index 54bd01bb..d406dea3 100644 --- a/lib/widgets/tv_spotlight_background.dart +++ b/lib/widgets/tv_spotlight_background.dart @@ -328,11 +328,15 @@ class TvSpotlightBackground extends StatelessWidget { } else if (media.isShow) { addTextPart(t.discover.tvShow); } - final ratingBadge = MediaRatingBadge.inlineForMedia( + // Hub listings carry the scalar rating pair, so the dashboard spotlight + // shows every score the shelf request already returned — no per-item + // hydration to lengthen it. + final ratingBadge = MediaRatingBadgeGroup.inlineForMedia( item: media, foregroundColor: textStyle.color, iconSize: textStyle.fontSize, spacing: 4 * scale, + entrySpacing: 12 * scale, textStyle: textStyle, ); if (ratingBadge != null) { diff --git a/test/media/media_item_test.dart b/test/media/media_item_test.dart index 1d87c24b..fd5e8bcc 100644 --- a/test/media/media_item_test.dart +++ b/test/media/media_item_test.dart @@ -3,6 +3,7 @@ import 'package:plezy/media/media_backend.dart'; import 'package:plezy/media/media_item.dart'; import 'package:plezy/media/media_kind.dart'; import 'package:plezy/media/media_part.dart'; +import 'package:plezy/media/media_rating.dart'; import 'package:plezy/media/media_role.dart'; import 'package:plezy/media/media_version.dart'; import '../test_helpers/media_items.dart'; @@ -476,9 +477,10 @@ void main() { kind: MediaKind.movie, title: 'Old', editionTitle: 'Director Cut', - audienceRating: 8.9, - ratingImage: 'rottentomatoes://rating', - audienceRatingImage: 'rottentomatoes://audience', + ratings: [ + MediaRatingSource(source: 'rottenTomatoesCritic', value: 9.4), + MediaRatingSource(source: 'imdb', value: 8.9, votes: 1200), + ], subtitleLanguage: 'eng', subtitleMode: 1, trailerKey: '/library/metadata/1', @@ -492,9 +494,8 @@ void main() { expect(copy.title, 'New'); expect(copy.editionTitle, 'Director Cut'); - expect(copy.audienceRating, 8.9); - expect(copy.ratingImage, 'rottentomatoes://rating'); - expect(copy.audienceRatingImage, 'rottentomatoes://audience'); + expect(copy.ratings?.map((rating) => rating.source), ['rottenTomatoesCritic', 'imdb']); + expect(copy.ratings?.last.votes, 1200); expect(copy.subtitleLanguage, 'eng'); expect(copy.subtitleMode, 1); expect(copy.trailerKey, '/library/metadata/1'); @@ -536,9 +537,10 @@ void main() { kind: MediaKind.movie, title: 'Movie', editionTitle: 'Theatrical', - audienceRating: 9.1, - ratingImage: 'rottentomatoes://rating', - audienceRatingImage: 'rottentomatoes://audience', + ratings: [ + MediaRatingSource(source: 'rottenTomatoesCritic', value: 9.1), + MediaRatingSource(source: 'imdb', value: 8.4, votes: 250858), + ], genres: ['Drama'], roles: [MediaRole(id: '1', tag: 'Actor', role: 'Lead', thumbPath: '/photo')], mediaVersions: [ @@ -566,9 +568,9 @@ void main() { expect(decoded, isA()); final plex = decoded as PlexMediaItem; expect(plex.editionTitle, 'Theatrical'); - expect(plex.audienceRating, 9.1); - expect(plex.ratingImage, 'rottentomatoes://rating'); - expect(plex.audienceRatingImage, 'rottentomatoes://audience'); + expect(plex.ratings?.map((rating) => rating.source), ['rottenTomatoesCritic', 'imdb']); + expect(plex.ratings?.first.value, 9.1); + expect(plex.ratings?.last.votes, 250858); expect(plex.genres, ['Drama']); expect(plex.roles?.single.tag, 'Actor'); expect(plex.mediaVersions?.single.parts.single.streamPath, '/stream'); diff --git a/test/models/catalog_item_test.dart b/test/models/catalog_item_test.dart index bed3527a..3785dd48 100644 --- a/test/models/catalog_item_test.dart +++ b/test/models/catalog_item_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_rating.dart'; import 'package:plezy/models/catalog/catalog_item.dart'; import 'package:plezy/models/catalog/catalog_metadata.dart'; import 'package:plezy/utils/external_ids.dart'; @@ -111,6 +112,28 @@ void main() { expect(decoded.ids.entryKey, 'mal:63832'); }); + test('carries every attributed rating onto the synthesized MediaItem', () { + // Explore's dashboard hubs render through this conversion, so a rating + // dropped here means the TV spotlight silently falls back to one score. + const rated = CatalogItem( + source: CatalogSourceId.simkl, + kind: MediaKind.movie, + title: 'Rated', + ids: CatalogItemIds(tmdb: 603), + rating: 8.1, + ratings: [ + MediaRatingSource(source: 'simkl', value: 8.1, votes: 11), + MediaRatingSource(source: 'imdb', value: 7.9, votes: 12), + ], + ); + + final rendered = rated.toMediaItem(); + + expect(rendered.rating, 8.1); + expect(rendered.ratings?.map((rating) => rating.source), ['simkl', 'imdb']); + expect(rendered.ratings?.last.votes, 12); + }); + test('survives an encode/decode cycle that erases the static map types', () { // Persisted/transport JSON comes back as Map and // List; the nested season object must not depend on its diff --git a/test/screens/catalog_item_detail_screen_test.dart b/test/screens/catalog_item_detail_screen_test.dart index 1d48236b..7c28b2aa 100644 --- a/test/screens/catalog_item_detail_screen_test.dart +++ b/test/screens/catalog_item_detail_screen_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:plezy/focus/focusable_action_bar.dart'; import 'package:plezy/i18n/strings.g.dart'; import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_rating.dart'; import 'package:plezy/media/media_item.dart'; import 'package:plezy/models/catalog/catalog_cast_member.dart'; import 'package:plezy/models/catalog/catalog_item.dart'; @@ -328,10 +329,10 @@ void main() { title: 'Rated Movie', ids: CatalogItemIds(tmdb: 11), ratings: [ - CatalogRatingSource(source: 'simkl', value: 8.1, votes: 11), - CatalogRatingSource(source: 'mal', value: 8.3, votes: 13), - CatalogRatingSource(source: 'critic', value: 7.2, votes: 14), - CatalogRatingSource(source: 'audience', value: 8.8, votes: 15), + MediaRatingSource(source: 'simkl', value: 8.1, votes: 11), + MediaRatingSource(source: 'mal', value: 8.3, votes: 13), + MediaRatingSource(source: 'critic', value: 7.2, votes: 14), + MediaRatingSource(source: 'audience', value: 8.8, votes: 15), ], ); final source = _FakeCatalogSource(detail: const CatalogDetail(item: item)); @@ -352,10 +353,10 @@ void main() { title: 'Attributed Movie', ids: CatalogItemIds(tmdb: 21), ratings: [ - CatalogRatingSource(source: 'rottenTomatoesCritic', value: 8.4), - CatalogRatingSource(source: 'rottenTomatoesAudience', value: 4.1), - CatalogRatingSource(source: 'imdb', value: 7.9, votes: 12), - CatalogRatingSource(source: 'tmdb', value: 7.5), + MediaRatingSource(source: 'rottenTomatoesCritic', value: 8.4), + MediaRatingSource(source: 'rottenTomatoesAudience', value: 4.1), + MediaRatingSource(source: 'imdb', value: 7.9, votes: 12), + MediaRatingSource(source: 'tmdb', value: 7.5), ], ); final source = _FakeCatalogSource(detail: const CatalogDetail(item: item)); @@ -381,7 +382,7 @@ void main() { expect(find.text('41%'), findsOneWidget); expect(find.text('7.9 (12 votes)'), findsOneWidget); expect(find.text('75%'), findsOneWidget); - expect(find.text('${t.explore.ratingSource.rottenTomatoesCritic} 8.4'), findsNothing); + expect(find.text('${t.common.ratingSource.rottenTomatoesCritic} 8.4'), findsNothing); }); testWidgets('seasonal rank keeps its season window instead of claiming all-time rank', (tester) async { diff --git a/test/screens/media_detail_screen_test.dart b/test/screens/media_detail_screen_test.dart index b35aa8a9..dca1f983 100644 --- a/test/screens/media_detail_screen_test.dart +++ b/test/screens/media_detail_screen_test.dart @@ -16,6 +16,7 @@ import 'package:plezy/media/media_backend.dart'; import 'package:plezy/media/media_hub.dart'; import 'package:plezy/media/media_item.dart'; import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_rating.dart'; import 'package:plezy/media/media_server_client.dart'; import 'package:plezy/media/server_capabilities.dart'; import 'package:plezy/providers/download_provider.dart'; @@ -175,7 +176,7 @@ void main() { expect(tester.widget(revealGate).opacity, 1); }); - testWidgets('TV detail shows Rotten Tomatoes rating badge in metadata line', (tester) async { + testWidgets('TV detail metadata line shows every rating source the item carries', (tester) async { await SettingsService.getInstance(); tester.view.physicalSize = const Size(1280, 720); tester.view.devicePixelRatio = 1; @@ -185,10 +186,14 @@ void main() { const movie = MediaItem.plex( id: 'movie_1', kind: MediaKind.movie, - title: 'Rotten Tomatoes Movie', - summary: 'The TV detail metadata line should use the rating source badge.', + title: 'Multi Source Movie', + summary: 'The TV detail metadata line should badge each attributed score.', rating: 6.2, - ratingImage: 'rottentomatoes://image.rating.ripe', + ratings: [ + MediaRatingSource(source: 'rottenTomatoesCritic', value: 6.2), + MediaRatingSource(source: 'rottenTomatoesAudience', value: 8.7), + MediaRatingSource(source: 'imdb', value: 7.4), + ], ); await tester.pumpWidget( @@ -205,24 +210,26 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); expect(find.text('62%'), findsOneWidget); - expect(find.byType(SvgPicture), findsOneWidget); + expect(find.text('87%'), findsOneWidget); + expect(find.text('7.4'), findsOneWidget); + expect(find.byType(SvgPicture), findsNWidgets(3)); expect(find.textContaining('★ 6.2', findRichText: true), findsNothing); }); - testWidgets('TV detail falls back to Rotten Tomatoes audience rating in metadata line', (tester) async { + testWidgets('TV detail metadata line still renders a single available rating', (tester) async { await SettingsService.getInstance(); tester.view.physicalSize = const Size(1280, 720); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); + // What a hub listing yields when the server sent only the audience scalar. const movie = MediaItem.plex( id: 'movie_1', kind: MediaKind.movie, title: 'Audience Rating Movie', summary: 'The TV detail metadata line should use the available audience source badge.', - audienceRating: 8.7, - audienceRatingImage: 'rottentomatoes://image.rating.upright', + ratings: [MediaRatingSource(source: 'rottenTomatoesAudience', value: 8.7)], ); await tester.pumpWidget( diff --git a/test/services/catalog/plex_catalog_source_test.dart b/test/services/catalog/plex_catalog_source_test.dart index c2d4243b..e1a2aa3f 100644 --- a/test/services/catalog/plex_catalog_source_test.dart +++ b/test/services/catalog/plex_catalog_source_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_rating.dart'; import 'package:plezy/models/catalog/catalog_item.dart'; import 'package:plezy/models/catalog/catalog_metadata.dart'; import 'package:plezy/services/catalog/catalog_source.dart'; @@ -747,7 +748,7 @@ void main() { expect( detail.item.ratings, contains( - isA() + isA() .having((rating) => rating.source, 'source', 'imdb') .having((rating) => rating.value, 'value', 8.5) .having((rating) => rating.votes, 'votes', 250858), diff --git a/test/services/jellyfin_mappers_test.dart b/test/services/jellyfin_mappers_test.dart index ef069b51..de85ca27 100644 --- a/test/services/jellyfin_mappers_test.dart +++ b/test/services/jellyfin_mappers_test.dart @@ -23,6 +23,7 @@ void main() { 'PremiereDate': '2010-07-16T00:00:00.0000000Z', 'OfficialRating': 'PG-13', 'CommunityRating': 8.8, + 'CriticRating': 88, 'Genres': ['Action', 'Sci-Fi'], 'People': [ {'Type': 'Actor', 'Name': 'Leo', 'Id': 'p1', 'PrimaryImageTag': 'tag1', 'Role': 'Cobb'}, @@ -63,6 +64,9 @@ void main() { expect(item.contentRating, 'PG-13'); expect(item.studio, 'Warner Bros'); expect(item.rating, 8.8); + expect(item.ratings?.map((rating) => rating.source).toList(), ['audience', 'rottenTomatoesCritic']); + // CriticRating is the 0-100 Tomatometer; the neutral scale is 0-10. + expect(item.ratings?.map((rating) => rating.value).toList(), [8.8, 8.8]); expect(item.genres, ['Action', 'Sci-Fi']); expect(item.directors, ['Christopher Nolan']); expect(item.countries, ['United States']); @@ -92,6 +96,40 @@ void main() { expect(item.serverName, 'Home'); }); + test('divides the Tomatometer rather than range-sniffing it', () { + // A CriticRating of 9 means 9%, not 9.0/10 — folding by magnitude would + // silently promote a rotten score to fresh. + final item = JellyfinMappers.mediaItem( + {'Id': 'movie-rotten', 'Type': 'Movie', 'CriticRating': 9}, + serverId: ServerId(_serverId), + absolutizer: null, + )!; + + expect(item.ratings?.single.source, 'rottenTomatoesCritic'); + expect(item.ratings?.single.value, 0.9); + }); + + test('omits ratings for photos, whose CommunityRating is an EXIF 0-5 star', () { + final item = JellyfinMappers.mediaItem( + {'Id': 'photo-1', 'Type': 'Photo', 'CommunityRating': 4}, + serverId: ServerId(_serverId), + absolutizer: null, + )!; + + expect(item.kind, MediaKind.photo); + expect(item.ratings, isNull); + }); + + test('reports no ratings when the server sent neither score', () { + final item = JellyfinMappers.mediaItem( + {'Id': 'movie-bare', 'Type': 'Movie'}, + serverId: ServerId(_serverId), + absolutizer: null, + )!; + + expect(item.ratings, isNull); + }); + test('preserves backdrop indices, deduplicates tags, and absolutizes every valid path', () { const absolutizer = JellyfinImageAbsolutizer(baseUrl: 'https://jellyfin.example', accessToken: 'secret'); final item = JellyfinMappers.mediaItem( diff --git a/test/services/plex_mappers_test.dart b/test/services/plex_mappers_test.dart index 30ce74ae..99f94cbd 100644 --- a/test/services/plex_mappers_test.dart +++ b/test/services/plex_mappers_test.dart @@ -210,6 +210,14 @@ void main() { 'librarySectionTitle': 'Movies', 'ratingImage': 'rottentomatoes://image.rating.ripe', 'audienceRatingImage': 'rottentomatoes://image.rating.upright', + 'imdbRatingCount': 250858, + // The detail endpoint adds the multi-source array; the TMDB entry + // duplicates the audienceRating scalar and must not appear twice. + 'Rating': [ + {'image': 'imdb://image.rating', 'type': 'audience', 'value': 8.4}, + {'image': 'rottentomatoes://image.rating.ripe', 'type': 'critic', 'value': 8.8}, + {'image': 'themoviedb://image.rating', 'type': 'audience', 'value': 9.1}, + ], 'Genre': [ {'tag': 'Action'}, {'tag': 'Sci-Fi'}, @@ -248,10 +256,20 @@ void main() { expect(item.originallyAvailableAt, '2010-07-16'); expect(item.contentRating, 'PG-13'); expect(item.rating, 8.8); - expect(item.audienceRating, 9.1); expect(item.userRating, 9.5); - expect(item.ratingImage, 'rottentomatoes://image.rating.ripe'); - expect(item.audienceRatingImage, 'rottentomatoes://image.rating.upright'); + + // Headline scalar first, then the array, deduped on (source, value): + // the RT critic entry repeats `rating`/`ratingImage`, and the TMDB entry + // is a distinct source so it survives alongside the RT audience scalar. + expect(item.ratings?.map((rating) => rating.source).toList(), [ + 'rottenTomatoesCritic', + 'rottenTomatoesAudience', + 'imdb', + 'tmdb', + ]); + expect(item.ratings?.map((rating) => rating.value).toList(), [8.8, 9.1, 8.4, 9.1]); + // Vote counts are IMDb-only; Plex reports none for the other sources. + expect(item.ratings?.map((rating) => rating.votes).toList(), [null, null, 250858, null]); // Plex stores all temporal fields in milliseconds — pass-through. expect(item.durationMs, 8880000); @@ -290,6 +308,48 @@ void main() { expect(item.serverName, _serverName); }); + test('a section listing yields the scalar pair alone', () { + // Plex sends no `Rating[]` outside /library/metadata/{id}, and no query + // parameter adds it, so listing-backed cards get one or two scores. + final item = PlexMappers.mediaItemFromJson({ + 'ratingKey': 'listing-1', + 'type': 'movie', + 'rating': 9.4, + 'ratingImage': 'rottentomatoes://image.rating.ripe', + 'audienceRating': 8.3, + 'audienceRatingImage': 'themoviedb://image.rating', + }, serverId: ServerId(_serverId)); + + expect(item.ratings?.map((rating) => rating.source).toList(), ['rottenTomatoesCritic', 'tmdb']); + expect(item.ratings?.map((rating) => rating.value).toList(), [9.4, 8.3]); + }); + + test('an unrated item reports no ratings rather than an empty list', () { + final item = PlexMappers.mediaItemFromJson({ + 'ratingKey': 'unrated-1', + 'type': 'movie', + }, serverId: ServerId(_serverId)); + + expect(item.rating, isNull); + expect(item.ratings, isNull); + }); + + test('scores outside the reportable range are dropped, not folded', () { + final item = PlexMappers.mediaItemFromJson({ + 'ratingKey': 'noisy-1', + 'type': 'movie', + 'rating': 140, + 'audienceRating': -1, + 'Rating': [ + {'image': 'imdb://image.rating', 'type': 'audience', 'value': '8.5'}, + ], + }, serverId: ServerId(_serverId)); + + // Only the IMDb entry survives, and its string value still parses. + expect(item.ratings?.single.source, 'imdb'); + expect(item.ratings?.single.value, 8.5); + }); + test('normalizes aggregate watch counts off leaf items', () { final item = PlexMappers.mediaItemFromJson({ 'ratingKey': 'leaf-with-counts', diff --git a/test/services/plex_rating_payload_shapes_test.dart b/test/services/plex_rating_payload_shapes_test.dart new file mode 100644 index 00000000..0335a1a2 --- /dev/null +++ b/test/services/plex_rating_payload_shapes_test.dart @@ -0,0 +1,50 @@ +// Live-payload regression: the exact `/library/metadata/{id}` and +// `/library/sections/{id}/all` shapes a real Plex Media Server returned +// (PMS 1.x, `tv.plex.agents.series`), captured verbatim. Plex sends the +// multi-source `Rating[]` array only on the metadata endpoint — probing +// `includeRatings`, `includeElements=Rating`, and `includeFields=Rating` +// against the listing endpoint all came back without it, while +// `includeGuids=1` demonstrably does add `Guid[]`. That asymmetry is why the +// dashboard shows fewer scores than the detail screen, and this suite pins it. +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/media/ids.dart'; +import 'package:plezy/services/plex_mappers.dart'; + +const _serverId = 'plex-machine-1'; + +void main() { + group('live Plex payloads', () { + test('detail response surfaces IMDb alongside Rotten Tomatoes and TMDB', () { + final item = PlexMappers.mediaItemFromJson({ + 'ratingKey': '2254', + 'type': 'show', + 'title': '【OSHI NO KO】', + 'audienceRating': 8.3, + 'audienceRatingImage': 'themoviedb://image.rating', + 'Rating': [ + {'image': 'imdb://image.rating', 'value': 8.3, 'type': 'audience'}, + {'image': 'rottentomatoes://image.rating.upright', 'value': 9.6, 'type': 'audience'}, + {'image': 'themoviedb://image.rating', 'value': 8.3, 'type': 'audience'}, + ], + }, serverId: ServerId(_serverId)); + + // The headline scalar leads; the array's TMDB entry repeats it and is + // deduped; IMDb — the score issue #1755 asked for — survives. + expect(item.ratings?.map((rating) => rating.source).toList(), ['tmdb', 'imdb', 'rottenTomatoesAudience']); + expect(item.ratings?.map((rating) => rating.value).toList(), [8.3, 8.3, 9.6]); + }); + + test('listing response carries only the scalar the server chose', () { + final item = PlexMappers.mediaItemFromJson({ + 'ratingKey': '2254', + 'type': 'show', + 'title': '【OSHI NO KO】', + 'audienceRating': 8.3, + 'audienceRatingImage': 'themoviedb://image.rating', + }, serverId: ServerId(_serverId)); + + expect(item.ratings?.single.source, 'tmdb'); + expect(item.ratings?.single.value, 8.3); + }); + }); +} diff --git a/test/utils/rating_utils_test.dart b/test/utils/rating_utils_test.dart index 8420e6fc..bf9b6478 100644 --- a/test/utils/rating_utils_test.dart +++ b/test/utils/rating_utils_test.dart @@ -1,88 +1,91 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/i18n/strings.g.dart'; import 'package:plezy/utils/rating_utils.dart'; void main() { - group('parseRatingImage - null/missing', () { - test('returns null when imageUri is null', () { - expect(parseRatingImage(null, 7.5), isNull); - }); + setUpAll(() => LocaleSettings.setLocaleRaw('en')); - test('returns null when value is null', () { - expect(parseRatingImage('imdb://title/tt123', null), isNull); - }); - - test('returns null for unknown scheme', () { - expect(parseRatingImage('unknown://foo', 5.0), isNull); - }); - }); - - group('parseRatingImage - Rotten Tomatoes', () { - test('ripe maps to rt_fresh with percent', () { - final info = parseRatingImage('rottentomatoes://image.rating.ripe', 7.5); - expect(info, isNotNull); + group('ratingInfoForSource - Rotten Tomatoes', () { + test('critic at or above the 60% tomatometer is fresh', () { + final info = ratingInfoForSource('rottenTomatoesCritic', 6.0); expect(info!.assetPath, 'assets/rating_icons/rt_fresh.svg'); - expect(info.formattedValue, '75%'); + expect(info.formattedValue, '60%'); }); - test('rotten maps to rt_rotten', () { - final info = parseRatingImage('rottentomatoes://image.rating.rotten', 3.2); - expect(info, isNotNull); + test('critic below the tomatometer is rotten', () { + final info = ratingInfoForSource('rottenTomatoesCritic', 5.9); expect(info!.assetPath, 'assets/rating_icons/rt_rotten.svg'); - expect(info.formattedValue, '32%'); + expect(info.formattedValue, '59%'); }); - test('upright maps to rt_upright', () { - final info = parseRatingImage('rottentomatoes://image.rating.upright', 8.8); - expect(info!.assetPath, 'assets/rating_icons/rt_upright.svg'); - expect(info.formattedValue, '88%'); + test('audience uses the popcorn pair on the same threshold', () { + expect(ratingInfoForSource('rottenTomatoesAudience', 6.0)!.assetPath, 'assets/rating_icons/rt_upright.svg'); + expect(ratingInfoForSource('rottenTomatoesAudience', 5.9)!.assetPath, 'assets/rating_icons/rt_spilled.svg'); }); - test('spilled maps to rt_spilled', () { - final info = parseRatingImage('rottentomatoes://image.rating.spilled', 2.0); - expect(info!.assetPath, 'assets/rating_icons/rt_spilled.svg'); - expect(info.formattedValue, '20%'); + test('the unsplit key follows the critic pair', () { + expect(ratingInfoForSource('rottenTomatoes', 9.2)!.assetPath, 'assets/rating_icons/rt_fresh.svg'); }); - test('unknown RT suffix returns null', () { - expect(parseRatingImage('rottentomatoes://image.rating.green', 5.0), isNull); - }); - - test('percent rounds to whole number', () { - final info = parseRatingImage('rottentomatoes://image.rating.ripe', 7.57); - expect(info!.formattedValue, '76%'); + test('percent rounds to a whole number', () { + expect(ratingInfoForSource('rottenTomatoesCritic', 7.57)!.formattedValue, '76%'); }); }); - group('parseRatingImage - IMDb', () { - test('formats with one decimal', () { - final info = parseRatingImage('imdb://title/tt123', 7.5); + group('ratingInfoForSource - branded scales', () { + test('IMDb keeps its 0-10 decimal', () { + final info = ratingInfoForSource('imdb', 7.5); expect(info!.assetPath, 'assets/rating_icons/imdb.svg'); expect(info.formattedValue, '7.5'); }); - }); - group('parseRatingImage - TMDB', () { - test('converts value*10 to percent', () { - final info = parseRatingImage('themoviedb://foo', 6.8); + test('TMDB renders as a percentage', () { + final info = ratingInfoForSource('tmdb', 6.8); expect(info!.assetPath, 'assets/rating_icons/tmdb.svg'); expect(info.formattedValue, '68%'); }); }); - group('isRottenTomatoes', () { - test('matches rottentomatoes:// scheme', () { - expect(isRottenTomatoes('rottentomatoes://image.rating.ripe'), isTrue); - expect(isRottenTomatoes('rottentomatoes://anything'), isTrue); + group('ratingInfoForSource - unbranded sources', () { + test('sources without a logo get no badge so they stay label-only', () { + for (final source in ['critic', 'audience', 'simkl', 'mal', 'anilist', 'trakt']) { + expect(ratingInfoForSource(source, 8.0), isNull, reason: source); + } }); - test('false for null', () { - expect(isRottenTomatoes(null), isFalse); + test('an unknown key gets no badge', () { + expect(ratingInfoForSource('letterboxd', 8.0), isNull); + expect(ratingInfoForSource('', 8.0), isNull); + }); + }); + + group('ratingSourceLabel', () { + test('names every source the mappers can emit', () { + const sources = [ + 'critic', + 'audience', + 'imdb', + 'tmdb', + 'rottenTomatoes', + 'rottenTomatoesCritic', + 'rottenTomatoesAudience', + 'simkl', + 'mal', + 'anilist', + 'trakt', + ]; + for (final source in sources) { + expect(ratingSourceLabel(source), isNotEmpty, reason: source); + } }); - test('false for other schemes', () { - expect(isRottenTomatoes('imdb://title'), isFalse); - expect(isRottenTomatoes('themoviedb://foo'), isFalse); - expect(isRottenTomatoes(''), isFalse); + test('keeps the Rotten Tomatoes panels distinguishable', () { + expect(ratingSourceLabel('rottenTomatoesCritic'), isNot(ratingSourceLabel('rottenTomatoesAudience'))); + }); + + test('returns null for an unknown key so it can be dropped rather than shown raw', () { + expect(ratingSourceLabel('letterboxd'), isNull); + expect(ratingSourceLabel(''), isNull); }); }); } diff --git a/test/widgets/tv_spotlight_ratings_test.dart b/test/widgets/tv_spotlight_ratings_test.dart new file mode 100644 index 00000000..b8be078c --- /dev/null +++ b/test/widgets/tv_spotlight_ratings_test.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/i18n/strings.g.dart'; +import 'package:plezy/media/media_item.dart'; +import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_rating.dart'; +import 'package:plezy/services/settings_service.dart'; +import 'package:plezy/utils/platform_detector.dart'; +import 'package:plezy/widgets/tv_spotlight_background.dart'; + +import '../test_helpers/prefs.dart'; + +/// The TV dashboard spotlight (Discover, Explore, and the library recommended +/// tab all render through [TvSpotlightBackground]) shows every score the hub +/// listing already returned. Listings carry the scalar rating pair, so this is +/// normally one or two entries — never a reason to re-fetch an item. +Future _pumpSpotlight(WidgetTester tester, MediaItem item) async { + await SettingsService.getInstance(); + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + TranslationProvider( + child: MaterialApp( + home: Scaffold( + // TvSpotlightScaffold fills the screen with the background; a loose + // Scaffold body would leave its bottom-anchored info block unbounded. + body: SizedBox.expand( + child: TvSpotlightBackground( + item: item, + client: null, + allowNetwork: false, + showPrimaryAction: false, + compact: true, + contentTop: 80, + contentBottom: 200, + ), + ), + ), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + resetSharedPreferencesForTest(); + SettingsService.resetForTesting(); + TvDetectionService.debugSetAppleTVOverride(true); + LocaleSettings.setLocaleSync(AppLocale.en); + }); + + tearDown(() => TvDetectionService.debugSetAppleTVOverride(null)); + + testWidgets('dashboard spotlight badges every rating the listing carried', (tester) async { + await _pumpSpotlight( + tester, + const MediaItem.plex( + id: 'movie_1', + kind: MediaKind.movie, + title: 'Spotlight Movie', + rating: 9.2, + ratings: [ + MediaRatingSource(source: 'rottenTomatoesCritic', value: 9.2), + MediaRatingSource(source: 'rottenTomatoesAudience', value: 8.5), + ], + ), + ); + + expect(find.text('92%'), findsOneWidget); + expect(find.text('85%'), findsOneWidget); + expect(find.byType(SvgPicture), findsNWidgets(2)); + }); + + testWidgets('dashboard spotlight still shows one badge for a single-score listing', (tester) async { + await _pumpSpotlight( + tester, + const MediaItem.jellyfin( + id: 'movie_2', + kind: MediaKind.movie, + title: 'Community Only', + rating: 8.3, + ratings: [MediaRatingSource(source: 'audience', value: 8.3)], + ), + ); + + // No brand logo exists for an unattributed community score, so it keeps + // the generic icon and the neutral 0-10 rendering. + expect(find.text('8.3'), findsOneWidget); + expect(find.byType(SvgPicture), findsNothing); + }); + + testWidgets('dashboard spotlight omits the rating slot when the item has no score', (tester) async { + await _pumpSpotlight( + tester, + const MediaItem.plex(id: 'movie_3', kind: MediaKind.movie, title: 'Unrated', year: 2024), + ); + + expect(find.text('2024'), findsOneWidget); + expect(find.byType(SvgPicture), findsNothing); + }); + + testWidgets('dashboard spotlight announces each rating with its source name', (tester) async { + final semantics = tester.ensureSemantics(); + + await _pumpSpotlight( + tester, + const MediaItem.plex( + id: 'movie_4', + kind: MediaKind.movie, + title: 'Announced Movie', + ratings: [ + MediaRatingSource(source: 'rottenTomatoesCritic', value: 9.2), + MediaRatingSource(source: 'imdb', value: 7.4), + ], + ), + ); + + // Without the group's own label a reader would hear "92%, 7.4" with no + // way to tell which score belongs to which source. + expect( + find.bySemanticsLabel('${t.common.ratingSource.rottenTomatoesCritic} 92%, ${t.common.ratingSource.imdb} 7.4'), + findsOneWidget, + ); + semantics.dispose(); + }); +}