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(); + }); +}