fix(seerr): show worldwide popular titles in Explore again

Overseerr and Jellyseerr bind the `language` query parameter of
`/discover/movies` and `/discover/tv` to `originalLanguage`, which becomes
TMDB's `with_original_language`. Sending the app locale there collapsed both
shelves to titles originally made in that language, so a Portuguese UI saw
only Portuguese films. Those two routes take their display language from the
instance/user locale, which already wins over the query value, so the
parameter was pure filtering with no localization to show for it.

Drop it from the two paged discover routes. Trending, both upcoming rows,
search, details and recommendations keep it: Seerr treats it as the display
language everywhere else.

close #1763
This commit is contained in:
edde746
2026-08-02 15:35:42 +02:00
parent d83d0790ba
commit 95b013e155
2 changed files with 47 additions and 10 deletions
+23 -5
View File
@@ -89,10 +89,20 @@ class SeerrClient {
// ---------- Discover / search ---------- // ---------- Discover / search ----------
/// `/discover/movies` — popular movies. /// `/discover/movies` — popular movies.
Future<SeerrPage<SeerrMedia>> 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<SeerrPage<SeerrMedia>> getPopularMovies({int page = 1}) =>
_mediaPage('/discover/movies', page, 'movie', localized: false);
/// `/discover/tv` — popular series. /// `/discover/tv` — popular series. Unlocalized for the same reason as
Future<SeerrPage<SeerrMedia>> getPopularTv({int page = 1}) => _mediaPage('/discover/tv', page, 'tv'); /// [getPopularMovies].
Future<SeerrPage<SeerrMedia>> getPopularTv({int page = 1}) =>
_mediaPage('/discover/tv', page, 'tv', localized: false);
Future<SeerrPage<SeerrMedia>> getUpcomingMovies({int page = 1}) => Future<SeerrPage<SeerrMedia>> getUpcomingMovies({int page = 1}) =>
_mediaPage('/discover/movies/upcoming', page, 'movie'); _mediaPage('/discover/movies/upcoming', page, 'movie');
@@ -118,8 +128,16 @@ class SeerrClient {
Future<SeerrPage<SeerrMedia>> getTvRecommendations(int tmdbId, {int page = 1}) => Future<SeerrPage<SeerrMedia>> getTvRecommendations(int tmdbId, {int page = 1}) =>
_mediaPage('/tv/$tmdbId/recommendations', page, 'tv'); _mediaPage('/tv/$tmdbId/recommendations', page, 'tv');
Future<SeerrPage<SeerrMedia>> _mediaPage(String path, int page, String? coerceMediaType) async { /// [localized] adds the app locale as `language`. Only the two paged
final data = await _request('GET', path, query: {'page': page, 'language': _language}); /// discover routes opt out; everywhere else Seerr treats it as the display
/// language, which is what we want.
Future<SeerrPage<SeerrMedia>> _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); return _parseMediaPage(data, coerceMediaType);
} }
+24 -5
View File
@@ -297,7 +297,7 @@ void main() {
expect(page.totalResults, 37); 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 = <Uri>[]; final urls = <Uri>[];
final client = clientWith( final client = clientWith(
MockClient((request) async { MockClient((request) async {
@@ -309,8 +309,6 @@ void main() {
}), }),
); );
await client.getPopularMovies();
await client.getPopularTv();
await client.getUpcomingMovies(); await client.getUpcomingMovies();
await client.getUpcomingTv(); await client.getUpcomingTv();
await client.getTrending(); await client.getTrending();
@@ -321,8 +319,6 @@ void main() {
await client.getTv(4); await client.getTv(4);
expect(urls.map((url) => url.path).toSet(), { expect(urls.map((url) => url.path).toSet(), {
'/api/v1/discover/movies',
'/api/v1/discover/tv',
'/api/v1/discover/movies/upcoming', '/api/v1/discover/movies/upcoming',
'/api/v1/discover/tv/upcoming', '/api/v1/discover/tv/upcoming',
'/api/v1/discover/trending', '/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 = <Uri>[];
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 { test('createRequest posts the movie payload without seasons', () async {
late Map<String, dynamic> body; late Map<String, dynamic> body;
final client = clientWith( final client = clientWith(