PlayQueue was a freezed union whose Plex variant was never constructed (server queues flow through PlayQueueResponse), whose pattern getters had zero call sites, and whose backendId was written but never read — it is now a plain four-field LocalPlayQueue. The MediaItem compatibility factory re-listed the entire ~65-field union surface twice while every caller passes at most 22 fields; it now declares exactly that union. fetchLibraryContent had zero production callers (the UI pages through fetchLibraryPagedContent), so the interface method and both implementations are gone, with Jellyfin's drain folded into its paged entry point.
729 lines
24 KiB
Dart
729 lines
24 KiB
Dart
import 'dart:convert';
|
|
import 'package:plezy/media/ids.dart';
|
|
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:plezy/database/app_database.dart';
|
|
import 'package:plezy/media/library_query.dart';
|
|
import 'package:plezy/services/plex_api_cache.dart';
|
|
import 'package:plezy/services/plex_client.dart';
|
|
|
|
import '../test_helpers/backend_client_fixtures.dart';
|
|
|
|
void main() {
|
|
late AppDatabase db;
|
|
|
|
setUp(() {
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
PlexApiCache.initialize(db);
|
|
});
|
|
|
|
tearDown(() async {
|
|
await db.close();
|
|
});
|
|
|
|
PlexClient makeClient(Future<http.Response> Function(http.Request request) handler) =>
|
|
testPlexClient(serverId: ServerId('server-id'), handler: handler);
|
|
|
|
test('filters and sorts use dedicated Plex endpoints', () async {
|
|
final requests = <Uri>[];
|
|
final client = makeClient((request) async {
|
|
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 filters = await client.getLibraryFilters('1');
|
|
final sorts = await client.fetchSortOptions('1', libraryType: 'show');
|
|
|
|
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',
|
|
// Plex doesn't advertise these in /sorts; we append them for movie/show.
|
|
'viewCount',
|
|
'userRating',
|
|
]);
|
|
});
|
|
|
|
test('appends Date Added, Plays, and User Rating sorts only for movie/show libraries', () async {
|
|
PlexClient clientReturning() => makeClient((request) async {
|
|
if (request.url.path == '/library/sections/1/sorts') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'Directory': [
|
|
{'key': 'titleSort', 'title': 'Title', 'defaultDirection': 'asc'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
|
|
for (final type in ['movie', 'show']) {
|
|
final client = clientReturning();
|
|
addTearDown(client.close);
|
|
final sorts = await client.fetchSortOptions('1', libraryType: type);
|
|
expect(sorts.map((s) => s.key), ['titleSort', 'addedAt', 'viewCount', 'userRating'], reason: type);
|
|
|
|
final dateAdded = sorts.singleWhere((s) => s.key == 'addedAt');
|
|
expect(dateAdded.descKey, 'addedAt:desc', reason: type);
|
|
expect(dateAdded.defaultDirection, 'desc', reason: type);
|
|
}
|
|
|
|
// Other library types (e.g. music) are left as the server returned them.
|
|
final musicClient = clientReturning();
|
|
addTearDown(musicClient.close);
|
|
final musicSorts = await musicClient.fetchSortOptions('1', libraryType: 'artist');
|
|
expect(musicSorts.map((s) => s.key), ['titleSort']);
|
|
});
|
|
|
|
test('does not duplicate Date Added/Plays when the server already advertises them', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/sections/1/sorts') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'Directory': [
|
|
{'key': 'titleSort', 'title': 'Title', 'defaultDirection': 'asc'},
|
|
{'key': 'addedAt', 'title': 'Date Added', 'defaultDirection': 'desc'},
|
|
{'key': 'viewCount', 'title': 'Plays', 'defaultDirection': 'desc'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final sorts = await client.fetchSortOptions('1', libraryType: 'movie');
|
|
// addedAt/viewCount already advertised -> not duplicated; userRating still appended.
|
|
expect(sorts.map((s) => s.key), ['titleSort', 'addedAt', 'viewCount', 'userRating']);
|
|
});
|
|
|
|
test('library content stamps known section when Plex omits librarySectionID on rows', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/sections/7/all') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 1,
|
|
'Metadata': [
|
|
{'ratingKey': '42', 'type': 'movie', 'title': 'Library Movie'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchLibraryPagedContent('7', query: const LibraryQuery(limit: 1));
|
|
|
|
expect(page.items.single.id, '42');
|
|
expect(page.items.single.libraryId, '7');
|
|
});
|
|
|
|
test('child metadata inherits hoisted MediaContainer library section', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/metadata/show-1/children') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'librarySectionID': '9',
|
|
'librarySectionTitle': 'TV Shows',
|
|
'Metadata': [
|
|
{'ratingKey': 'season-1', 'type': 'season', 'title': 'Season 1'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final children = await client.fetchChildren('show-1');
|
|
|
|
expect(children.single.libraryId, '9');
|
|
expect(children.single.libraryTitle, 'TV Shows');
|
|
});
|
|
|
|
test('hub content infers library section from /hubs/sections key', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/hubs/sections/7/recentlyAdded/items') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'Metadata': [
|
|
{'ratingKey': '42', 'type': 'movie', 'title': 'Hub Movie'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final items = await client.fetchHubContent('/hubs/sections/7/recentlyAdded/items');
|
|
|
|
expect(items.single.id, '42');
|
|
expect(items.single.libraryId, '7');
|
|
});
|
|
|
|
test('collection page can inherit source collection library section', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/collections/99/children') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 1,
|
|
'Metadata': [
|
|
{'ratingKey': '42', 'type': 'movie', 'title': 'Collection Movie'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchCollectionPage('99', libraryId: '7', libraryTitle: 'Movies');
|
|
|
|
expect(page.items.single.id, '42');
|
|
expect(page.items.single.libraryId, '7');
|
|
expect(page.items.single.libraryTitle, 'Movies');
|
|
});
|
|
|
|
test('library collections are fetched in pages', () async {
|
|
final requests = <Uri>[];
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/sections/7/collections') {
|
|
requests.add(request.url);
|
|
final start = request.url.queryParameters['X-Plex-Container-Start'];
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 2,
|
|
'Metadata': [
|
|
{'ratingKey': start == '0' ? '99' : '100', 'type': 'collection', 'title': 'Collection'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final collections = await client.fetchCollections('7');
|
|
|
|
expect(collections.map((item) => item.id).toList(), ['99', '100']);
|
|
expect(requests.map((u) => u.queryParameters['X-Plex-Container-Start']).toList(), ['0', '1']);
|
|
expect(requests.every((u) => u.queryParameters['X-Plex-Container-Size'] == '200'), isTrue);
|
|
expect(requests.every((u) => u.queryParameters['includeGuids'] == '1'), isTrue);
|
|
});
|
|
|
|
test('library collection page passes requested pagination params', () async {
|
|
Uri? requestUri;
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/sections/7/collections') {
|
|
requestUri = request.url;
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 50,
|
|
'Metadata': [
|
|
{'ratingKey': '120', 'type': 'collection', 'title': 'Collection'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchCollectionsPage('7', start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, '120');
|
|
expect(page.totalCount, 50);
|
|
expect(page.offset, 20);
|
|
expect(requestUri, isNotNull);
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Start'], '20');
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Size'], '10');
|
|
expect(requestUri!.queryParameters['includeGuids'], '1');
|
|
});
|
|
|
|
test('playlist page passes requested pagination params', () async {
|
|
Uri? requestUri;
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists') {
|
|
requestUri = request.url;
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 50,
|
|
'Metadata': [
|
|
{
|
|
'ratingKey': '120',
|
|
'key': '/playlists/120/items',
|
|
'type': 'playlist',
|
|
'playlistType': 'video',
|
|
'title': 'Playlist',
|
|
'smart': false,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPlaylistsPage(start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, '120');
|
|
expect(page.totalCount, 50);
|
|
expect(page.offset, 20);
|
|
expect(requestUri, isNotNull);
|
|
expect(requestUri!.queryParameters['playlistType'], 'video');
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Start'], '20');
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Size'], '10');
|
|
});
|
|
|
|
test('playlist page fallback total only exposes one possible next item', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 10,
|
|
'Metadata': List.generate(
|
|
10,
|
|
(i) => {
|
|
'ratingKey': '${120 + i}',
|
|
'key': '/playlists/${120 + i}/items',
|
|
'type': 'playlist',
|
|
'playlistType': 'video',
|
|
'title': 'Playlist',
|
|
'smart': false,
|
|
},
|
|
),
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPlaylistsPage(start: 20, size: 10);
|
|
|
|
expect(page.items.length, 10);
|
|
expect(page.totalCount, 31);
|
|
expect(page.offset, 20);
|
|
});
|
|
|
|
test('playlist page uses X-Plex-Container-Total-Size header when body total is absent', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'Metadata': [
|
|
{
|
|
'ratingKey': '120',
|
|
'key': '/playlists/120/items',
|
|
'type': 'playlist',
|
|
'playlistType': 'video',
|
|
'title': 'Playlist',
|
|
'smart': false,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json', 'X-Plex-Container-Total-Size': '50'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPlaylistsPage(start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, '120');
|
|
expect(page.totalCount, 50);
|
|
expect(page.offset, 20);
|
|
});
|
|
|
|
test('fetchPlaylists walks playlist pages', () async {
|
|
final requests = <Uri>[];
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists') {
|
|
requests.add(request.url);
|
|
final start = int.parse(request.url.queryParameters['X-Plex-Container-Start'] ?? '0');
|
|
final metadata = start == 0
|
|
? [
|
|
{'ratingKey': '1', 'type': 'playlist', 'playlistType': 'video', 'title': 'One'},
|
|
{'ratingKey': '2', 'type': 'playlist', 'playlistType': 'video', 'title': 'Two'},
|
|
]
|
|
: [
|
|
{'ratingKey': '3', 'type': 'playlist', 'playlistType': 'video', 'title': 'Three'},
|
|
];
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {'size': metadata.length, 'totalSize': 3, 'Metadata': metadata},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final playlists = await client.fetchPlaylists();
|
|
|
|
expect(playlists.map((p) => p.id), ['1', '2', '3']);
|
|
expect(requests.map((u) => u.queryParameters['X-Plex-Container-Start']), ['0', '2']);
|
|
expect(requests.every((u) => u.queryParameters['X-Plex-Container-Size'] == '200'), isTrue);
|
|
});
|
|
|
|
test('fetchPlaylists returns empty on list failure', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists') {
|
|
return http.Response('server error', 500);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final playlists = await client.fetchPlaylists();
|
|
|
|
expect(playlists, isEmpty);
|
|
});
|
|
|
|
test('playlist item page passes requested pagination params', () async {
|
|
Uri? requestUri;
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists/42/items') {
|
|
requestUri = request.url;
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 30,
|
|
'Metadata': [
|
|
{'ratingKey': '99', 'type': 'movie', 'title': 'Movie'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPlaylistPage('42', start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, '99');
|
|
expect(page.totalCount, 30);
|
|
expect(page.offset, 20);
|
|
expect(requestUri, isNotNull);
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Start'], '20');
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Size'], '10');
|
|
});
|
|
|
|
test('playlist item page uses X-Plex-Container-Total-Size header when body total is absent', () async {
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/playlists/42/items') {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'Metadata': [
|
|
{'ratingKey': '99', 'type': 'movie', 'title': 'Movie'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json', 'X-Plex-Container-Total-Size': '30'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPlaylistPage('42', start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, '99');
|
|
expect(page.totalCount, 30);
|
|
expect(page.offset, 20);
|
|
});
|
|
|
|
test('person media page passes requested pagination params', () async {
|
|
Uri? requestUri;
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/people/person-1/media') {
|
|
requestUri = request.url;
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 30,
|
|
'Metadata': [
|
|
{'ratingKey': '99', 'type': 'movie', 'title': 'Movie'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPersonMediaPage('person-1', start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, '99');
|
|
expect(page.totalCount, 30);
|
|
expect(page.offset, 20);
|
|
expect(requestUri, isNotNull);
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Start'], '20');
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Size'], '10');
|
|
});
|
|
|
|
test('playable descendants page passes requested pagination params', () async {
|
|
Uri? requestUri;
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/metadata/show-1/grandchildren') {
|
|
requestUri = request.url;
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 1,
|
|
'totalSize': 30,
|
|
'Metadata': [
|
|
{'ratingKey': 'ep-1', 'type': 'episode', 'title': 'Episode'},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final page = await client.fetchPlayableDescendantsPage('show-1', start: 20, size: 10);
|
|
|
|
expect(page.items.single.id, 'ep-1');
|
|
expect(page.totalCount, 30);
|
|
expect(page.offset, 20);
|
|
expect(requestUri, isNotNull);
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Start'], '20');
|
|
expect(requestUri!.queryParameters['X-Plex-Container-Size'], '10');
|
|
});
|
|
|
|
test('client-side episode fallback retains watched rows and sorts by watch order', () async {
|
|
Uri? requestUri;
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/library/metadata/show-1/grandchildren') {
|
|
requestUri = request.url;
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {
|
|
'size': 3,
|
|
'totalSize': 3,
|
|
'Metadata': [
|
|
{
|
|
'ratingKey': 'special',
|
|
'type': 'episode',
|
|
'title': 'Special',
|
|
'parentIndex': 0,
|
|
'index': 1,
|
|
'originallyAvailableAt': '2024-01-02',
|
|
'viewCount': 1,
|
|
},
|
|
{
|
|
'ratingKey': 'ep-2',
|
|
'type': 'episode',
|
|
'title': 'Episode 2',
|
|
'parentIndex': 1,
|
|
'index': 2,
|
|
'originallyAvailableAt': '2024-01-03',
|
|
'viewCount': 1,
|
|
},
|
|
{
|
|
'ratingKey': 'ep-1',
|
|
'type': 'episode',
|
|
'title': 'Episode 1',
|
|
'parentIndex': 1,
|
|
'index': 1,
|
|
'originallyAvailableAt': '2024-01-01',
|
|
'viewCount': 1,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final episodes = await client.fetchClientSideEpisodeQueue('show-1');
|
|
|
|
expect(requestUri!.path, '/library/metadata/show-1/grandchildren');
|
|
expect(episodes!.map((episode) => episode.id), ['ep-1', 'special', 'ep-2']);
|
|
expect(episodes.every((episode) => episode.isWatched), isTrue);
|
|
});
|
|
|
|
test('hub content pages by filtered video item offset', () async {
|
|
final requests = <Uri>[];
|
|
final client = makeClient((request) async {
|
|
if (request.url.path == '/hubs/sections/7/recent') {
|
|
requests.add(request.url);
|
|
final start = request.url.queryParameters['X-Plex-Container-Start'] ?? '0';
|
|
final metadata = start == '0'
|
|
? [
|
|
{'ratingKey': 'collection-1', 'type': 'collection', 'title': 'Collection'},
|
|
{'ratingKey': 'movie-1', 'type': 'movie', 'title': 'Movie'},
|
|
]
|
|
: [
|
|
{'ratingKey': 'episode-1', 'type': 'episode', 'title': 'Episode'},
|
|
];
|
|
return http.Response(
|
|
jsonEncode({
|
|
'MediaContainer': {'size': metadata.length, 'totalSize': 3, 'Metadata': metadata},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response('not found', 404);
|
|
});
|
|
addTearDown(client.close);
|
|
|
|
final firstPage = await client.fetchMoreHubItemsPage('/hubs/sections/7/recent', start: 0, size: 1);
|
|
final secondPage = await client.fetchMoreHubItemsPage('/hubs/sections/7/recent', start: 1, size: 1);
|
|
|
|
expect(firstPage.items.single.id, 'movie-1');
|
|
expect(firstPage.totalCount, 2);
|
|
expect(firstPage.offset, 0);
|
|
expect(secondPage.items.single.id, 'episode-1');
|
|
expect(secondPage.totalCount, 2);
|
|
expect(secondPage.offset, 1);
|
|
expect(requests.map((u) => u.queryParameters['X-Plex-Container-Start']).toList(), ['0', '0', '2']);
|
|
expect(requests.every((u) => u.queryParameters['X-Plex-Container-Size'] == '200'), isTrue);
|
|
});
|
|
}
|
|
|
|
Map<String, dynamic> _filtersPayload() => {
|
|
'MediaContainer': {
|
|
'Directory': [
|
|
{
|
|
'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<String, dynamic> _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'},
|
|
],
|
|
},
|
|
};
|