diff --git a/lib/media/library_query.dart b/lib/media/library_query.dart index f2e48844..73be12bb 100644 --- a/lib/media/library_query.dart +++ b/lib/media/library_query.dart @@ -76,3 +76,11 @@ sealed class LibraryPage with _$LibraryPage { const factory LibraryPage({required List items, required int totalCount, @Default(0) int offset}) = _LibraryPage; } + +/// Conservative total for a page whose backend omitted an exact count. A full +/// page adds one sentinel item so callers keep pagination enabled without +/// claiming to know the real total. +int fallbackPageTotal({required int offset, required int itemCount, int? requestedSize}) { + final fullPage = requestedSize != null && requestedSize > 0 && itemCount >= requestedSize; + return offset + itemCount + (fullPage ? 1 : 0); +} diff --git a/lib/services/jellyfin_client/parts/browse.dart b/lib/services/jellyfin_client/parts/browse.dart index c889c5c0..d5e6a3d3 100644 --- a/lib/services/jellyfin_client/parts/browse.dart +++ b/lib/services/jellyfin_client/parts/browse.dart @@ -107,13 +107,6 @@ String _jellyfinFolderSortName(Map item) { return raw.toLowerCase(); } -int _fallbackPageTotal({required int offset, required int itemCount, int? requestedSize}) { - if (requestedSize == null || requestedSize <= 0 || itemCount < requestedSize) { - return offset + itemCount; - } - return offset + itemCount + 1; -} - /// `/Items/Filters` is a legacy unpaged endpoint; keep failures isolated from /// the paged Browse tab so very large libraries can still open. const _filtersTimeout = Duration(seconds: 8); @@ -209,7 +202,7 @@ mixin _JellyfinBrowseMethods on MediaServerCacheMixin { final totalUnreliable = isArtistQuery && rawTotal == 0 && items.isNotEmpty; final total = rawTotal is int && !totalUnreliable ? rawTotal - : _fallbackPageTotal(offset: query.offset, itemCount: items.length, requestedSize: query.limit); + : fallbackPageTotal(offset: query.offset, itemCount: items.length, requestedSize: query.limit); return LibraryPage(items: _mapItems(items), totalCount: total, offset: query.offset); } @@ -1588,7 +1581,7 @@ mixin _JellyfinBrowseMethods on MediaServerCacheMixin { final rawTotal = data is Map ? data['TotalRecordCount'] : null; final fallbackTotal = singlePage ? offset + rawItems.length - : _fallbackPageTotal(offset: offset, itemCount: rawItems.length, requestedSize: requestedSize); + : fallbackPageTotal(offset: offset, itemCount: rawItems.length, requestedSize: requestedSize); return LibraryPage( items: _mapItems(rawItems), totalCount: rawTotal is int ? rawTotal : fallbackTotal, @@ -1603,7 +1596,7 @@ mixin _JellyfinBrowseMethods on MediaServerCacheMixin { LibraryPage _pagedMediaItems(Object? data, {required int offset, required int requestedSize}) { final rawItems = _itemsArray(data); final rawTotal = data is Map ? data['TotalRecordCount'] : null; - final fallbackTotal = _fallbackPageTotal(offset: offset, itemCount: rawItems.length, requestedSize: requestedSize); + final fallbackTotal = fallbackPageTotal(offset: offset, itemCount: rawItems.length, requestedSize: requestedSize); return LibraryPage( items: _mapItems(rawItems), totalCount: rawTotal is int ? rawTotal : fallbackTotal, diff --git a/lib/services/jellyfin_client/parts/collections.dart b/lib/services/jellyfin_client/parts/collections.dart index 9270e464..bdfa44c2 100644 --- a/lib/services/jellyfin_client/parts/collections.dart +++ b/lib/services/jellyfin_client/parts/collections.dart @@ -76,7 +76,7 @@ mixin _JellyfinCollectionMethods on MediaServerCacheMixin { LibraryPage _itemsPage(Object? data, {required int offset, int? requestedSize}) { final rawItems = _itemsArray(data); final rawTotal = data is Map ? data['TotalRecordCount'] : null; - final fallbackTotal = _fallbackPageTotal(offset: offset, itemCount: rawItems.length, requestedSize: requestedSize); + final fallbackTotal = fallbackPageTotal(offset: offset, itemCount: rawItems.length, requestedSize: requestedSize); final total = rawTotal is int ? rawTotal : fallbackTotal; return LibraryPage(items: _mapItems(rawItems), totalCount: total, offset: offset); } diff --git a/lib/services/jellyfin_client/parts/playlists.dart b/lib/services/jellyfin_client/parts/playlists.dart index 599a10a8..05c2030a 100644 --- a/lib/services/jellyfin_client/parts/playlists.dart +++ b/lib/services/jellyfin_client/parts/playlists.dart @@ -83,7 +83,7 @@ mixin _JellyfinPlaylistMethods on MediaServerCacheMixin { final fallbackTotal = rawFinished ? filteredSeen - : _fallbackPageTotal(offset: offset, itemCount: items.length, requestedSize: pageSize); + : fallbackPageTotal(offset: offset, itemCount: items.length, requestedSize: pageSize); return LibraryPage(items: items, totalCount: fallbackTotal, offset: offset); } @@ -134,7 +134,7 @@ mixin _JellyfinPlaylistMethods on MediaServerCacheMixin { final rawTotal = response.data is Map ? (response.data as Map)['TotalRecordCount'] : null; - final fallbackTotal = _fallbackPageTotal(offset: offset, itemCount: items.length, requestedSize: pageSize); + final fallbackTotal = fallbackPageTotal(offset: offset, itemCount: items.length, requestedSize: pageSize); return LibraryPage( items: _mapItems(items), totalCount: rawTotal is int ? rawTotal : fallbackTotal, diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 197c92da..11ee69f5 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -758,11 +758,6 @@ class PlexClient return null; } - int _fallbackPageTotal({required int offset, required int itemCount, int? requestedSize}) { - final fullPage = requestedSize != null && requestedSize > 0 && itemCount >= requestedSize; - return offset + itemCount + (fullPage ? 1 : 0); - } - int _responseTotalSize(MediaServerResponse response, {required int itemCount, int? start, int? requestedSize}) { final headerTotal = _responseHeaderInt(response, 'X-Plex-Container-Total-Size'); if (headerTotal != null) return headerTotal; @@ -776,7 +771,7 @@ class PlexClient return flexibleInt(container?['size']) ?? itemCount; } - return _fallbackPageTotal(offset: offset, itemCount: itemCount, requestedSize: requestedSize); + return fallbackPageTotal(offset: offset, itemCount: itemCount, requestedSize: requestedSize); } ({List items, int totalSize}) _extractPlaylistListResult( diff --git a/test/media/library_query_test.dart b/test/media/library_query_test.dart new file mode 100644 index 00000000..2b06ad06 --- /dev/null +++ b/test/media/library_query_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/media/library_query.dart'; + +void main() { + test('fallbackPageTotal adds a has-more sentinel only for full pages', () { + expect(fallbackPageTotal(offset: 20, itemCount: 10, requestedSize: 10), 31); + expect(fallbackPageTotal(offset: 20, itemCount: 11, requestedSize: 10), 32); + expect(fallbackPageTotal(offset: 20, itemCount: 9, requestedSize: 10), 29); + expect(fallbackPageTotal(offset: 20, itemCount: 10), 30); + expect(fallbackPageTotal(offset: 20, itemCount: 10, requestedSize: 0), 30); + expect(fallbackPageTotal(offset: 20, itemCount: 10, requestedSize: -1), 30); + }); +}