From e5922f48700a700b930b749abca826daa654f860 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 12 May 2026 05:32:58 +0200 Subject: [PATCH] fix(plex): restore library filters and sorts close #1020 --- .../libraries/tabs/library_browse_tab.dart | 2 + lib/services/plex_client.dart | 60 +--------- test/services/plex_library_details_test.dart | 110 +++++++++++------- 3 files changed, 74 insertions(+), 98 deletions(-) diff --git a/lib/screens/libraries/tabs/library_browse_tab.dart b/lib/screens/libraries/tabs/library_browse_tab.dart index bee2e6b0..720ec72a 100644 --- a/lib/screens/libraries/tabs/library_browse_tab.dart +++ b/lib/screens/libraries/tabs/library_browse_tab.dart @@ -541,6 +541,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState filters; - final List sorts; - - const _LibrarySectionDetails({required this.filters, required this.sorts}); -} - /// Process hub response in an isolate. /// Top-level function so it can be passed to [Isolate.run]. List _processHubResponse( @@ -195,8 +188,6 @@ class PlexClient with MediaServerCacheMixin, _PlexLiveTvClientMethods implements /// Server-level preferences fetched from /:/prefs Map _serverPrefs = {}; - final Map> _librarySectionDetails = {}; - /// Get all fetched server preferences Map get serverPrefs => Map.unmodifiable(_serverPrefs); @@ -1461,52 +1452,8 @@ class PlexClient with MediaServerCacheMixin, _PlexLiveTvClientMethods implements /// Get available filters for a library section Future> getLibraryFilters(String sectionId) async { if (sectionId == 'shared') return []; - final details = await _getLibrarySectionDetails(sectionId); - return details.filters; - } - - Future<_LibrarySectionDetails> _getLibrarySectionDetails(String sectionId) { - return _librarySectionDetails.putIfAbsent(sectionId, () async { - try { - final response = await _getWithFailover('/library/sections/$sectionId', queryParameters: {'includeDetails': 1}); - return _extractLibrarySectionDetails(response); - } catch (_) { - _librarySectionDetails.remove(sectionId)?.ignore(); - rethrow; - } - }); - } - - _LibrarySectionDetails _extractLibrarySectionDetails(MediaServerResponse response) { - final container = _getMediaContainer(response); - if (container == null) return const _LibrarySectionDetails(filters: [], sorts: []); - return _LibrarySectionDetails( - filters: _extractFirstNestedList(container, 'Filter', MediaFilter.fromJson), - sorts: _extractFirstNestedList(container, 'Sort', MediaSort.fromJson), - ); - } - - List _extractFirstNestedList( - Map container, - String key, - T Function(Map) fromJson, - ) { - final direct = _parseRawList(container[key], fromJson); - if (direct.isNotEmpty) return direct; - - final directories = container['Directory']; - if (directories is! List) return []; - for (final directory in directories) { - if (directory is! Map) continue; - final parsed = _parseRawList(directory[key], fromJson); - if (parsed.isNotEmpty) return parsed; - } - return []; - } - - List _parseRawList(Object? raw, T Function(Map) fromJson) { - if (raw is! List) return []; - return raw.whereType().map((json) => fromJson(Map.from(json))).toList(); + final response = await _getWithFailover('/library/sections/$sectionId/filters'); + return _extractDirectoryList(response, MediaFilter.fromJson); } /// Get first characters (alphabet index) for a library section @@ -1563,7 +1510,8 @@ class PlexClient with MediaServerCacheMixin, _PlexLiveTvClientMethods implements ]; } try { - final sorts = (await _getLibrarySectionDetails(sectionId)).sorts; + final response = await _getWithFailover('/library/sections/$sectionId/sorts'); + final sorts = _extractDirectoryList(response, MediaSort.fromJson); if (sorts.isNotEmpty) { return sorts; diff --git a/test/services/plex_library_details_test.dart b/test/services/plex_library_details_test.dart index c1cdcaaa..78222dbd 100644 --- a/test/services/plex_library_details_test.dart +++ b/test/services/plex_library_details_test.dart @@ -5,8 +5,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:plezy/database/app_database.dart'; -import 'package:plezy/media/media_filter.dart'; -import 'package:plezy/media/media_sort.dart'; import 'package:plezy/models/plex/plex_config.dart'; import 'package:plezy/services/plex_api_cache.dart'; import 'package:plezy/services/plex_client.dart'; @@ -37,58 +35,86 @@ void main() { ); } - test('filters and sorts share the includeDetails section request', () async { - var requestCount = 0; + test('filters and sorts use dedicated Plex endpoints', () async { + final requests = []; final client = makeClient((request) async { - requestCount++; - expect(request.url.path, '/library/sections/1'); - expect(request.url.queryParameters['includeDetails'], '1'); - return http.Response(jsonEncode(_sectionDetailsPayload()), 200, headers: {'content-type': 'application/json'}); + requests.add(request.url); + return switch (request.url.path) { + '/library/sections/1/filters' => http.Response( + jsonEncode(_filtersPayload()), + 200, + headers: {'content-type': 'application/json'}, + ), + '/library/sections/1/sorts' => http.Response( + jsonEncode(_sortsPayload()), + 200, + headers: {'content-type': 'application/json'}, + ), + _ => http.Response('not found', 404), + }; }); addTearDown(client.close); - final results = await Future.wait([ - client.getLibraryFilters('1'), - client.fetchSortOptions('1', libraryType: 'movie'), - ]); + final filters = await client.getLibraryFilters('1'); + final sorts = await client.fetchSortOptions('1', libraryType: 'show'); - final filters = results[0] as List; - final sorts = results[1] as List; - expect(requestCount, 1); - expect(filters.map((f) => f.filter), ['genre', 'year']); - expect(sorts.map((s) => s.key), ['addedAt', 'titleSort']); + expect(requests.map((u) => u.path), ['/library/sections/1/filters', '/library/sections/1/sorts']); + expect(requests.every((u) => u.queryParameters.isEmpty), isTrue); + expect(filters.map((f) => f.filter), ['genre', 'year', 'unwatched']); + expect(sorts.map((s) => s.key), [ + 'titleSort', + 'rating', + 'audienceRating', + 'addedAt', + 'episode.addedAt', + 'lastViewedAt', + 'random', + ]); }); } -Map _sectionDetailsPayload() => { +Map _filtersPayload() => { 'MediaContainer': { 'Directory': [ - {'key': 'all', 'title': 'All Movies'}, { - 'key': '/library/sections/1/all?type=1', - 'title': 'Movies', - 'type': '1', - 'Filter': [ - { - 'filter': 'genre', - 'filterType': 'string', - 'key': '/library/sections/1/genre', - 'title': 'Genre', - 'type': 'filter', - }, - { - 'filter': 'year', - 'filterType': 'integer', - 'key': '/library/sections/1/year', - 'title': 'Year', - 'type': 'filter', - }, - ], - 'Sort': [ - {'defaultDirection': 'desc', 'descKey': 'addedAt:desc', 'key': 'addedAt', 'title': 'Date Added'}, - {'defaultDirection': 'asc', 'descKey': 'titleSort:desc', 'key': 'titleSort', 'title': 'Name'}, - ], + 'filter': 'genre', + 'filterType': 'string', + 'key': '/library/sections/1/genre', + 'title': 'Genre', + 'type': 'filter', + }, + {'filter': 'year', 'filterType': 'integer', 'key': '/library/sections/1/year', 'title': 'Year', 'type': 'filter'}, + { + 'filter': 'unwatched', + 'filterType': 'boolean', + 'key': '/library/sections/1/unwatched', + 'title': 'Unwatched', + 'type': 'filter', }, ], }, }; + +Map _sortsPayload() => { + 'MediaContainer': { + 'Directory': [ + {'defaultDirection': 'asc', 'descKey': 'titleSort:desc', 'key': 'titleSort', 'title': 'Title'}, + {'defaultDirection': 'desc', 'descKey': 'rating:desc', 'key': 'rating', 'title': 'Critic Rating'}, + { + 'defaultDirection': 'desc', + 'descKey': 'audienceRating:desc', + 'key': 'audienceRating', + 'title': 'Audience Rating', + }, + {'defaultDirection': 'desc', 'descKey': 'addedAt:desc', 'key': 'addedAt', 'title': 'Date Added'}, + { + 'defaultDirection': 'desc', + 'descKey': 'episode.addedAt:desc', + 'key': 'episode.addedAt', + 'title': 'Last Episode Date Added', + }, + {'defaultDirection': 'desc', 'descKey': 'lastViewedAt:desc', 'key': 'lastViewedAt', 'title': 'Date Viewed'}, + {'defaultDirection': 'desc', 'descKey': 'random:desc', 'key': 'random', 'title': 'Randomly'}, + ], + }, +};