fix: paginate collection/playlist/person endpoints

close #903
This commit is contained in:
edde746
2026-04-21 21:51:20 +02:00
parent e2be6a0093
commit eb49ead83b
10 changed files with 603 additions and 385 deletions
+79 -39
View File
@@ -599,26 +599,32 @@ class PlexClient {
Map<String, String>? filters,
AbortController? abort,
}) async {
final queryParams = <String, dynamic>{};
if (start != null) queryParams['X-Plex-Container-Start'] = start;
if (size != null) queryParams['X-Plex-Container-Size'] = size;
// Add filter parameters
if (filters != null) {
queryParams.addAll(filters);
}
final queryParams = _buildPaginationParams(start, size);
if (filters != null) queryParams.addAll(filters);
final endpoint = sectionId == 'shared' ? '/library/shared/all' : '/library/sections/$sectionId/all';
final response = await _getWithFailover(endpoint, queryParameters: queryParams, abort: abort);
return _extractLibraryContentResult(response);
}
Map<String, dynamic> _buildPaginationParams(int? start, int? size) {
final params = <String, dynamic>{};
if (start != null) params['X-Plex-Container-Start'] = start;
if (size != null) params['X-Plex-Container-Size'] = size;
return params;
}
LibraryContentResult _extractLibraryContentResult(PlexResponse response) {
final items = _extractMetadataList(response);
final container = _getMediaContainer(response);
final totalSize = container?['totalSize'] as int? ?? container?['size'] as int? ?? items.length;
return LibraryContentResult(items: items, totalSize: totalSize);
}
Future<LibraryContentResult> _fetchPaginatedList(String path, {int? start, int? size, AbortController? abort}) async {
final response = await _getWithFailover(path, queryParameters: _buildPaginationParams(start, size), abort: abort);
return _extractLibraryContentResult(response);
}
/// Parse list of PlexMetadata from a cached response
List<PlexMetadata> _parseMetadataListFromCachedResponse(Map<String, dynamic> cached) {
final metadataList = PlexCacheParser.extractMetadataList(cached);
@@ -837,6 +843,33 @@ class PlexClient {
}
}
/// Page size for iterating all items via [_fetchAllPages]. Also the cap
/// for endpoints that send `X-Plex-Container-Size` but aren't truly paginated
/// (collections listing, playlists listing, search).
static const int _defaultListContainerSize = 1000;
/// Page size used when walking all pages of a paginated endpoint.
static const int _fetchAllPageSize = 200;
/// Iterate every page of a paginated endpoint and concatenate the results.
/// Stops as soon as [LibraryContentResult.totalSize] is reached or a page
/// returns no items. Errors propagate.
Future<List<PlexMetadata>> _fetchAllPages(
Future<LibraryContentResult> Function(int start, int size, AbortController? abort) fetchPage, {
AbortController? abort,
}) async {
final all = <PlexMetadata>[];
var start = 0;
while (true) {
final page = await fetchPage(start, _fetchAllPageSize, abort);
all.addAll(page.items);
start += page.items.length;
if (page.items.isEmpty) break;
if (start >= page.totalSize) break;
}
return all;
}
/// Parse audio/subtitle tracks and the video stream's frame rate from a
/// raw Part.Stream list in a single pass.
({List<PlexAudioTrack> audio, List<PlexSubtitleTrack> subtitles, double? frameRate}) _parseStreams(
@@ -1042,6 +1075,7 @@ class PlexClient {
'searchTypes': 'movies,tv',
'includeCollections': 1,
'includeExternalMedia': 1,
'X-Plex-Container-Size': limit,
},
);
@@ -1712,21 +1746,23 @@ class PlexClient {
}, 'Failed to get hub content');
}
/// Get playlist content by playlist ID
/// Returns the list of metadata items in the playlist
Future<List<PlexMetadata>> getPlaylist(String playlistId) {
return _wrapListApiCall<PlexMetadata>(
() => _http.get('/playlists/$playlistId/items'),
_extractMetadataList,
'Failed to get playlist',
);
}
/// Get playlist content by playlist ID, paginated.
Future<LibraryContentResult> getPlaylist(String playlistId, {int? start, int? size, AbortController? abort}) =>
_fetchPaginatedList('/playlists/$playlistId/items', start: start, size: size, abort: abort);
/// Fetch every page of a playlist's items. For callers that need the full list
/// (downloads, sync rules, context-menu shuffle).
Future<List<PlexMetadata>> fetchAllPlaylistItems(String playlistId) =>
_fetchAllPages((start, size, abort) => getPlaylist(playlistId, start: start, size: size, abort: abort));
/// Get all playlists
/// Filters by playlistType=video by default
/// Set smart to true/false to filter smart playlists, or null for all
Future<List<PlexPlaylist>> getPlaylists({String playlistType = 'video', bool? smart}) {
final queryParams = <String, dynamic>{'playlistType': playlistType};
final queryParams = <String, dynamic>{
'playlistType': playlistType,
'X-Plex-Container-Size': _defaultListContainerSize,
};
if (smart != null) {
queryParams['smart'] = smart ? '1' : '0';
}
@@ -1965,7 +2001,10 @@ class PlexClient {
/// Returns collections as PlexMetadata objects with type="collection"
Future<List<PlexMetadata>> getLibraryCollections(String sectionId) async {
return _wrapListApiCall<PlexMetadata>(
() => _http.get('/library/sections/$sectionId/collections', queryParameters: {'includeGuids': 1}),
() => _http.get(
'/library/sections/$sectionId/collections',
queryParameters: {'includeGuids': 1, 'X-Plex-Container-Size': _defaultListContainerSize},
),
(response) {
final allItems = _extractMetadataList(response);
// Collections should have type="collection"
@@ -1977,24 +2016,25 @@ class PlexClient {
);
}
/// Get items in a collection
/// Returns the list of metadata items in the collection
Future<List<PlexMetadata>> getCollectionItems(String collectionId) {
return _wrapListApiCall<PlexMetadata>(
() => _http.get('/library/collections/$collectionId/children'),
_extractMetadataList,
'Failed to get collection items',
);
}
/// Get items in a collection, paginated.
Future<LibraryContentResult> getCollectionItems(
String collectionId, {
int? start,
int? size,
AbortController? abort,
}) => _fetchPaginatedList('/library/collections/$collectionId/children', start: start, size: size, abort: abort);
/// Get media featuring a specific person (actor/director)
Future<List<PlexMetadata>> getPersonMedia(String personId) {
return _wrapListApiCall<PlexMetadata>(
() => _http.get('/library/people/$personId/media'),
_extractMetadataList,
'Failed to get person media',
);
}
/// Fetch every item in a collection (downloads, sync rules, context-menu shuffle).
Future<List<PlexMetadata>> fetchAllCollectionItems(String collectionId) =>
_fetchAllPages((start, size, abort) => getCollectionItems(collectionId, start: start, size: size, abort: abort));
/// Get media featuring a specific person (actor/director), paginated.
Future<LibraryContentResult> getPersonMedia(String personId, {int? start, int? size, AbortController? abort}) =>
_fetchPaginatedList('/library/people/$personId/media', start: start, size: size, abort: abort);
/// Fetch every media item featuring a given person.
Future<List<PlexMetadata>> fetchAllPersonMedia(String personId) =>
_fetchAllPages((start, size, abort) => getPersonMedia(personId, start: start, size: size, abort: abort));
/// Delete a collection
/// Deletes a library collection from the server