diff --git a/lib/services/seerr/seerr_client.dart b/lib/services/seerr/seerr_client.dart index cbf8def9..c3c2a0f4 100644 --- a/lib/services/seerr/seerr_client.dart +++ b/lib/services/seerr/seerr_client.dart @@ -89,10 +89,20 @@ class SeerrClient { // ---------- Discover / search ---------- /// `/discover/movies` — popular movies. - Future> getPopularMovies({int page = 1}) => _mediaPage('/discover/movies', page, 'movie'); + /// + /// Deliberately unlocalized. Overseerr and Jellyseerr bind this route's + /// `language` query parameter to `originalLanguage`, i.e. TMDB's + /// `with_original_language`, so sending the app locale narrows the shelf to + /// titles *originally made* in that language (#1763). The display language + /// here comes from the instance/user locale, which already wins over the + /// query value, so omitting it costs nothing. + Future> getPopularMovies({int page = 1}) => + _mediaPage('/discover/movies', page, 'movie', localized: false); - /// `/discover/tv` — popular series. - Future> getPopularTv({int page = 1}) => _mediaPage('/discover/tv', page, 'tv'); + /// `/discover/tv` — popular series. Unlocalized for the same reason as + /// [getPopularMovies]. + Future> getPopularTv({int page = 1}) => + _mediaPage('/discover/tv', page, 'tv', localized: false); Future> getUpcomingMovies({int page = 1}) => _mediaPage('/discover/movies/upcoming', page, 'movie'); @@ -118,8 +128,16 @@ class SeerrClient { Future> getTvRecommendations(int tmdbId, {int page = 1}) => _mediaPage('/tv/$tmdbId/recommendations', page, 'tv'); - Future> _mediaPage(String path, int page, String? coerceMediaType) async { - final data = await _request('GET', path, query: {'page': page, 'language': _language}); + /// [localized] adds the app locale as `language`. Only the two paged + /// discover routes opt out; everywhere else Seerr treats it as the display + /// language, which is what we want. + Future> _mediaPage( + String path, + int page, + String? coerceMediaType, { + bool localized = true, + }) async { + final data = await _request('GET', path, query: {'page': page, if (localized) 'language': _language}); return _parseMediaPage(data, coerceMediaType); } diff --git a/test/services/seerr/seerr_client_test.dart b/test/services/seerr/seerr_client_test.dart index f3f3c8c0..88f7091c 100644 --- a/test/services/seerr/seerr_client_test.dart +++ b/test/services/seerr/seerr_client_test.dart @@ -297,7 +297,7 @@ void main() { expect(page.totalResults, 37); }); - test('adds the current Plezy locale to every catalog GET', () async { + test('adds the current Plezy locale to the catalog GETs Seerr localizes', () async { final urls = []; final client = clientWith( MockClient((request) async { @@ -309,8 +309,6 @@ void main() { }), ); - await client.getPopularMovies(); - await client.getPopularTv(); await client.getUpcomingMovies(); await client.getUpcomingTv(); await client.getTrending(); @@ -321,8 +319,6 @@ void main() { await client.getTv(4); expect(urls.map((url) => url.path).toSet(), { - '/api/v1/discover/movies', - '/api/v1/discover/tv', '/api/v1/discover/movies/upcoming', '/api/v1/discover/tv/upcoming', '/api/v1/discover/trending', @@ -338,6 +334,29 @@ void main() { } }); + test('popular rows omit language so Seerr cannot filter them by original language', () async { + // Overseerr and Jellyseerr pass `/discover/movies` and `/discover/tv`'s + // `language` straight into `originalLanguage`, i.e. TMDB's + // `with_original_language`. Sending the app locale collapsed both shelves + // to titles originally made in that language (#1763). + final urls = []; + final client = clientWith( + MockClient((request) async { + urls.add(request.url); + return _json({'page': 1, 'totalPages': 1, 'results': []}); + }), + ); + + await client.getPopularMovies(page: 2); + await client.getPopularTv(); + + expect(urls.map((url) => url.path).toList(), ['/api/v1/discover/movies', '/api/v1/discover/tv']); + for (final url in urls) { + expect(url.queryParameters.containsKey('language'), isFalse, reason: url.path); + } + expect(urls.first.queryParameters['page'], '2', reason: 'paging must survive the locale opt-out'); + }); + test('createRequest posts the movie payload without seasons', () async { late Map body; final client = clientWith(