fix(jellyfin): align playback and browse requests
This commit is contained in:
@@ -832,6 +832,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
'Limit': '1',
|
'Limit': '1',
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
});
|
});
|
||||||
final onDeckEpisode = nextUp.isEmpty ? null : _mapItem(nextUp.first);
|
final onDeckEpisode = nextUp.isEmpty ? null : _mapItem(nextUp.first);
|
||||||
return (item: item, onDeckEpisode: onDeckEpisode);
|
return (item: item, onDeckEpisode: onDeckEpisode);
|
||||||
@@ -909,7 +910,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
try {
|
try {
|
||||||
final seasons = await _http.get(
|
final seasons = await _http.get(
|
||||||
'/Shows/${_segment(parentId)}/Seasons',
|
'/Shows/${_segment(parentId)}/Seasons',
|
||||||
queryParameters: {'userId': connection.userId, 'Fields': _browseFields},
|
queryParameters: {'userId': connection.userId, 'Fields': _browseFields, ...jellyfinImageQueryParameters},
|
||||||
);
|
);
|
||||||
if (seasons.statusCode == 200) {
|
if (seasons.statusCode == 200) {
|
||||||
final data = seasons.data;
|
final data = seasons.data;
|
||||||
@@ -926,7 +927,13 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
// collection → items, etc.
|
// collection → items, etc.
|
||||||
final response = await _http.get(
|
final response = await _http.get(
|
||||||
'/Items',
|
'/Items',
|
||||||
queryParameters: {'userId': connection.userId, 'ParentId': parentId, 'Fields': _browseFields, 'Limit': '500'},
|
queryParameters: {
|
||||||
|
'userId': connection.userId,
|
||||||
|
'ParentId': parentId,
|
||||||
|
'Fields': _browseFields,
|
||||||
|
'Limit': '500',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
final data = response.data;
|
final data = response.data;
|
||||||
@@ -955,6 +962,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Recursive': 'true',
|
'Recursive': 'true',
|
||||||
'IncludeItemTypes': 'Movie,Episode',
|
'IncludeItemTypes': 'Movie,Episode',
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
@@ -982,6 +990,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Fields': _queueFields,
|
'Fields': _queueFields,
|
||||||
'StartIndex': '$startIndex',
|
'StartIndex': '$startIndex',
|
||||||
'Limit': '$_episodeQueuePageSize',
|
'Limit': '$_episodeQueuePageSize',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
@@ -1010,6 +1019,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Limit': limit.toString(),
|
'Limit': limit.toString(),
|
||||||
'IncludeItemTypes': 'Movie,Series,Episode',
|
'IncludeItemTypes': 'Movie,Series,Episode',
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
@@ -1021,7 +1031,12 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
// Matches userLibraryApi.getLatestMedia in the Jellyfin SDK.
|
// Matches userLibraryApi.getLatestMedia in the Jellyfin SDK.
|
||||||
final response = await _http.get(
|
final response = await _http.get(
|
||||||
'/Users/${_segment(connection.userId)}/Items/Latest',
|
'/Users/${_segment(connection.userId)}/Items/Latest',
|
||||||
queryParameters: {'Limit': limit.toString(), 'Fields': _browseFields, 'IncludeItemTypes': 'Movie,Series,Episode'},
|
queryParameters: {
|
||||||
|
'Limit': limit.toString(),
|
||||||
|
'Fields': _browseFields,
|
||||||
|
'IncludeItemTypes': 'Movie,Series,Episode',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
final data = response.data;
|
final data = response.data;
|
||||||
@@ -1040,6 +1055,8 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Limit': count.toString(),
|
'Limit': count.toString(),
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
'MediaTypes': 'Video',
|
'MediaTypes': 'Video',
|
||||||
|
'Recursive': 'true',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
_safeFetchItemsArray('/Shows/NextUp', {
|
_safeFetchItemsArray('/Shows/NextUp', {
|
||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
@@ -1047,6 +1064,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
'EnableResumable': 'false',
|
'EnableResumable': 'false',
|
||||||
'EnableTotalRecordCount': 'false',
|
'EnableTotalRecordCount': 'false',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1065,11 +1083,15 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Limit': limit.toString(),
|
'Limit': limit.toString(),
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
'IncludeItemTypes': 'Movie,Series,Episode',
|
'IncludeItemTypes': 'Movie,Series,Episode',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
_safeFetchItemsArray('/UserItems/Resume', {
|
_safeFetchItemsArray('/UserItems/Resume', {
|
||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
'Limit': limit.toString(),
|
'Limit': limit.toString(),
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
'MediaTypes': 'Video',
|
||||||
|
'Recursive': 'true',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
_safeFetchItemsArray('/Shows/NextUp', {
|
_safeFetchItemsArray('/Shows/NextUp', {
|
||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
@@ -1077,6 +1099,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
'EnableResumable': 'false',
|
'EnableResumable': 'false',
|
||||||
'EnableTotalRecordCount': 'false',
|
'EnableTotalRecordCount': 'false',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1125,6 +1148,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Limit': limit.toString(),
|
'Limit': limit.toString(),
|
||||||
'ParentId': libraryId,
|
'ParentId': libraryId,
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
_safeFetchItemsArray('/UserItems/Resume', {
|
_safeFetchItemsArray('/UserItems/Resume', {
|
||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
@@ -1132,6 +1156,8 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Limit': limit.toString(),
|
'Limit': limit.toString(),
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
'MediaTypes': 'Video',
|
'MediaTypes': 'Video',
|
||||||
|
'Recursive': 'true',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
_safeFetchItemsArray('/Shows/NextUp', {
|
_safeFetchItemsArray('/Shows/NextUp', {
|
||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
@@ -1140,6 +1166,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
'EnableResumable': 'false',
|
'EnableResumable': 'false',
|
||||||
'EnableTotalRecordCount': 'false',
|
'EnableTotalRecordCount': 'false',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1196,6 +1223,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'Limit': effectiveLimit,
|
'Limit': effectiveLimit,
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
if (parentId != null) 'ParentId': parentId else 'IncludeItemTypes': 'Movie,Series,Episode',
|
if (parentId != null) 'ParentId': parentId else 'IncludeItemTypes': 'Movie,Series,Episode',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'continue':
|
case 'continue':
|
||||||
@@ -1203,7 +1231,9 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'userId': connection.userId,
|
'userId': connection.userId,
|
||||||
'Limit': effectiveLimit,
|
'Limit': effectiveLimit,
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
'Recursive': 'true',
|
||||||
if (parentId != null) 'ParentId': parentId else 'MediaTypes': 'Video',
|
if (parentId != null) 'ParentId': parentId else 'MediaTypes': 'Video',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'nextup':
|
case 'nextup':
|
||||||
@@ -1214,6 +1244,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'ParentId': ?parentId,
|
'ParentId': ?parentId,
|
||||||
'EnableResumable': 'false',
|
'EnableResumable': 'false',
|
||||||
'EnableTotalRecordCount': 'false',
|
'EnableTotalRecordCount': 'false',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1226,7 +1257,12 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
Future<List<MediaHub>> fetchRelatedHubs(String id, {int count = 10}) async {
|
Future<List<MediaHub>> fetchRelatedHubs(String id, {int count = 10}) async {
|
||||||
final response = await _http.get(
|
final response = await _http.get(
|
||||||
'/Items/${_segment(id)}/Similar',
|
'/Items/${_segment(id)}/Similar',
|
||||||
queryParameters: {'userId': connection.userId, 'Limit': count.toString(), 'Fields': _browseFields},
|
queryParameters: {
|
||||||
|
'userId': connection.userId,
|
||||||
|
'Limit': count.toString(),
|
||||||
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
return [
|
return [
|
||||||
@@ -1299,6 +1335,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'IncludeItemTypes': 'Playlist',
|
'IncludeItemTypes': 'Playlist',
|
||||||
'Recursive': 'true',
|
'Recursive': 'true',
|
||||||
'Fields': 'Overview,DateCreated,DateLastSaved,ChildCount,Tags',
|
'Fields': 'Overview,DateCreated,DateLastSaved,ChildCount,Tags',
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
@@ -1340,6 +1377,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'StartIndex': offset.toString(),
|
'StartIndex': offset.toString(),
|
||||||
'Limit': limit.toString(),
|
'Limit': limit.toString(),
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
@@ -1443,6 +1481,7 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
'IncludeItemTypes': 'BoxSet',
|
'IncludeItemTypes': 'BoxSet',
|
||||||
'Recursive': 'true',
|
'Recursive': 'true',
|
||||||
'Fields': _browseFields,
|
'Fields': _browseFields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throwIfHttpError(response);
|
throwIfHttpError(response);
|
||||||
@@ -1650,35 +1689,9 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HLS master playlist URL for transcoded playback. Use when the file
|
|
||||||
/// container/codecs don't match the player's capabilities. The exact
|
|
||||||
/// negotiation (which streams, what bitrate) is server-driven via the
|
|
||||||
/// `PlaybackInfo` POST that should precede this call when fidelity
|
|
||||||
/// matters; this method assumes the caller already knows what they want.
|
|
||||||
String buildHlsStreamUrl(
|
|
||||||
String itemId, {
|
|
||||||
required String mediaSourceId,
|
|
||||||
int? videoBitrate,
|
|
||||||
int? audioStreamIndex,
|
|
||||||
int? subtitleStreamIndex,
|
|
||||||
String? playSessionId,
|
|
||||||
}) {
|
|
||||||
return buildJellyfinHlsStreamUrl(
|
|
||||||
baseUrl: connection.baseUrl,
|
|
||||||
accessToken: connection.accessToken,
|
|
||||||
deviceId: connection.deviceId,
|
|
||||||
itemId: itemId,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
videoBitrate: videoBitrate,
|
|
||||||
audioStreamIndex: audioStreamIndex,
|
|
||||||
subtitleStreamIndex: subtitleStreamIndex,
|
|
||||||
playSessionId: playSessionId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Negotiate playback: returns the parsed `MediaSources[]` array and the
|
/// Negotiate playback: returns the parsed `MediaSources[]` array and the
|
||||||
/// server's recommended `PlaySessionId`. Caller decides which media source
|
/// server's recommended `PlaySessionId`. Caller decides which media source
|
||||||
/// to use and feeds the result into [buildHlsStreamUrl] / direct play.
|
/// to use and feeds the returned `TranscodingUrl` into the player.
|
||||||
///
|
///
|
||||||
/// [maxStreamingBitrate] is forwarded as both the top-level field and inside
|
/// [maxStreamingBitrate] is forwarded as both the top-level field and inside
|
||||||
/// the `DeviceProfile` so the server caps direct-stream and transcode bitrate
|
/// the `DeviceProfile` so the server caps direct-stream and transcode bitrate
|
||||||
|
|||||||
@@ -31,29 +31,5 @@ String buildJellyfinTrickplayTileUrl({
|
|||||||
return '$baseUrl/Videos/$encodedItem/Trickplay/$width/$sheetIndex.jpg?${_encodeQuery(params)}';
|
return '$baseUrl/Videos/$encodedItem/Trickplay/$width/$sheetIndex.jpg?${_encodeQuery(params)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
String buildJellyfinHlsStreamUrl({
|
|
||||||
required String baseUrl,
|
|
||||||
required String accessToken,
|
|
||||||
required String deviceId,
|
|
||||||
required String itemId,
|
|
||||||
required String mediaSourceId,
|
|
||||||
int? videoBitrate,
|
|
||||||
int? audioStreamIndex,
|
|
||||||
int? subtitleStreamIndex,
|
|
||||||
String? playSessionId,
|
|
||||||
}) {
|
|
||||||
final params = <String, String>{
|
|
||||||
'DeviceId': deviceId,
|
|
||||||
'MediaSourceId': mediaSourceId,
|
|
||||||
'api_key': accessToken,
|
|
||||||
'VideoBitrate': ?videoBitrate?.toString(),
|
|
||||||
'AudioStreamIndex': ?audioStreamIndex?.toString(),
|
|
||||||
'SubtitleStreamIndex': ?subtitleStreamIndex?.toString(),
|
|
||||||
'PlaySessionId': ?playSessionId,
|
|
||||||
};
|
|
||||||
final encodedItem = Uri.encodeComponent(itemId);
|
|
||||||
return '$baseUrl/Videos/$encodedItem/master.m3u8?${_encodeQuery(params)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _encodeQuery(Map<String, String> params) =>
|
String _encodeQuery(Map<String, String> params) =>
|
||||||
params.entries.map((e) => '${e.key}=${Uri.encodeQueryComponent(e.value)}').join('&');
|
params.entries.map((e) => '${e.key}=${Uri.encodeQueryComponent(e.value)}').join('&');
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import '../media/library_query.dart';
|
|||||||
import '../media/media_kind.dart';
|
import '../media/media_kind.dart';
|
||||||
import 'plex_constants.dart';
|
import 'plex_constants.dart';
|
||||||
|
|
||||||
|
/// Limit browse payload image tags to the artwork types the UI maps.
|
||||||
|
const jellyfinImageQueryParameters = <String, String>{
|
||||||
|
'EnableImageTypes': 'Primary,Backdrop,Thumb,Logo',
|
||||||
|
'ImageTypeLimit': '1',
|
||||||
|
};
|
||||||
|
|
||||||
/// Translates a backend-neutral [LibraryQuery] into the per-backend
|
/// Translates a backend-neutral [LibraryQuery] into the per-backend
|
||||||
/// query-parameter map that the corresponding `/library/sections/{id}/all`
|
/// query-parameter map that the corresponding `/library/sections/{id}/all`
|
||||||
/// (Plex) or `/Items` (Jellyfin) endpoint expects.
|
/// (Plex) or `/Items` (Jellyfin) endpoint expects.
|
||||||
@@ -212,6 +218,7 @@ class JellyfinLibraryQueryTranslator implements LibraryQueryTranslator {
|
|||||||
'Limit': query.limit.toString(),
|
'Limit': query.limit.toString(),
|
||||||
'IncludeItemTypes': _includeTypesFor(query.kind),
|
'IncludeItemTypes': _includeTypesFor(query.kind),
|
||||||
'Fields': fields,
|
'Fields': fields,
|
||||||
|
...jellyfinImageQueryParameters,
|
||||||
};
|
};
|
||||||
if (!query.includeWatched) {
|
if (!query.includeWatched) {
|
||||||
params['Filters'] = 'IsUnplayed';
|
params['Filters'] = 'IsUnplayed';
|
||||||
|
|||||||
@@ -77,43 +77,6 @@ void main() {
|
|||||||
expect(Uri.parse(url).path, '/Videos/folder%2Fitem%20%231%3Fx/stream');
|
expect(Uri.parse(url).path, '/Videos/folder%2Fitem%20%231%3Fx/stream');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildHlsStreamUrl wires required + optional params', () {
|
|
||||||
final url = client.buildHlsStreamUrl(
|
|
||||||
'item-42',
|
|
||||||
mediaSourceId: 'src-1',
|
|
||||||
videoBitrate: 5000000,
|
|
||||||
audioStreamIndex: 1,
|
|
||||||
subtitleStreamIndex: 2,
|
|
||||||
playSessionId: 'sess-01',
|
|
||||||
);
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
|
|
||||||
expect(uri.path, '/Videos/item-42/master.m3u8');
|
|
||||||
expect(uri.queryParameters['MediaSourceId'], 'src-1');
|
|
||||||
expect(uri.queryParameters['VideoBitrate'], '5000000');
|
|
||||||
expect(uri.queryParameters['AudioStreamIndex'], '1');
|
|
||||||
expect(uri.queryParameters['SubtitleStreamIndex'], '2');
|
|
||||||
expect(uri.queryParameters['PlaySessionId'], 'sess-01');
|
|
||||||
expect(uri.queryParameters['DeviceId'], 'dev-xyz');
|
|
||||||
expect(uri.queryParameters['api_key'], 'tok-abc');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('buildHlsStreamUrl omits null optional params', () {
|
|
||||||
final url = client.buildHlsStreamUrl('item-42', mediaSourceId: 'src-1');
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
|
|
||||||
expect(uri.queryParameters['MediaSourceId'], 'src-1');
|
|
||||||
expect(uri.queryParameters.containsKey('VideoBitrate'), isFalse);
|
|
||||||
expect(uri.queryParameters.containsKey('AudioStreamIndex'), isFalse);
|
|
||||||
expect(uri.queryParameters.containsKey('SubtitleStreamIndex'), isFalse);
|
|
||||||
expect(uri.queryParameters.containsKey('PlaySessionId'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('buildHlsStreamUrl path-encodes reserved item id characters', () {
|
|
||||||
final url = client.buildHlsStreamUrl('folder/item #1?x', mediaSourceId: 'src-1');
|
|
||||||
expect(Uri.parse(url).path, '/Videos/folder%2Fitem%20%231%3Fx/master.m3u8');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reportPlaybackProgress sends media source and stream indexes', () async {
|
test('reportPlaybackProgress sends media source and stream indexes', () async {
|
||||||
Uri? capturedUri;
|
Uri? capturedUri;
|
||||||
String? capturedBody;
|
String? capturedBody;
|
||||||
@@ -325,6 +288,45 @@ void main() {
|
|||||||
expect(capturedUri.toString(), contains('/Items/folder%2Fitem%20%231%3Fx/PlaybackInfo'));
|
expect(capturedUri.toString(), contains('/Items/folder%2Fitem%20%231%3Fx/PlaybackInfo'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getPlaybackInfo keeps the known-good lean DeviceProfile', () async {
|
||||||
|
Uri? capturedUri;
|
||||||
|
String? capturedBody;
|
||||||
|
final scoped = JellyfinClient.forTesting(
|
||||||
|
connection: _conn(),
|
||||||
|
httpClient: MockClient((request) async {
|
||||||
|
capturedUri = request.url;
|
||||||
|
capturedBody = request.body;
|
||||||
|
return http.Response(jsonEncode({'MediaSources': []}), 200, headers: {'content-type': 'application/json'});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
addTearDown(scoped.close);
|
||||||
|
|
||||||
|
await scoped.getPlaybackInfo(
|
||||||
|
'item-1',
|
||||||
|
maxStreamingBitrate: 5000000,
|
||||||
|
mediaSourceId: 'src-1',
|
||||||
|
audioStreamIndex: 1,
|
||||||
|
subtitleStreamIndex: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(capturedUri!.queryParameters['MaxStreamingBitrate'], '5000000');
|
||||||
|
expect(capturedUri!.queryParameters.containsKey('IsPlayback'), isFalse);
|
||||||
|
expect(capturedUri!.queryParameters.containsKey('AutoOpenLiveStream'), isFalse);
|
||||||
|
expect(capturedUri!.queryParameters['MediaSourceId'], 'src-1');
|
||||||
|
expect(capturedUri!.queryParameters['AudioStreamIndex'], '1');
|
||||||
|
expect(capturedUri!.queryParameters['SubtitleStreamIndex'], '2');
|
||||||
|
|
||||||
|
final body = jsonDecode(capturedBody!) as Map<String, dynamic>;
|
||||||
|
final profile = body['DeviceProfile'] as Map<String, dynamic>;
|
||||||
|
expect(profile['MaxStreamingBitrate'], 5000000);
|
||||||
|
expect(profile.containsKey('MaxStaticBitrate'), isFalse);
|
||||||
|
expect(profile.containsKey('MusicStreamingTranscodingBitrate'), isFalse);
|
||||||
|
expect(profile['DirectPlayProfiles'], isNotEmpty);
|
||||||
|
expect(profile['TranscodingProfiles'], isNotEmpty);
|
||||||
|
expect(profile['CodecProfiles'], isEmpty);
|
||||||
|
expect(profile.containsKey('SubtitleProfiles'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('path-encodes reserved ids for browse and watch-state endpoints', () async {
|
test('path-encodes reserved ids for browse and watch-state endpoints', () async {
|
||||||
final captured = <Uri>[];
|
final captured = <Uri>[];
|
||||||
final scoped = JellyfinClient.forTesting(
|
final scoped = JellyfinClient.forTesting(
|
||||||
@@ -710,6 +712,8 @@ void main() {
|
|||||||
expect(capturedNextUp, isNotNull);
|
expect(capturedNextUp, isNotNull);
|
||||||
expect(capturedNextUp!.queryParameters['seriesId'], 'show-1');
|
expect(capturedNextUp!.queryParameters['seriesId'], 'show-1');
|
||||||
expect(capturedNextUp!.queryParameters['Limit'], '1');
|
expect(capturedNextUp!.queryParameters['Limit'], '1');
|
||||||
|
expect(capturedNextUp!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(capturedNextUp!.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(capturedNextUp!.queryParameters.containsKey('EnableResumable'), isFalse);
|
expect(capturedNextUp!.queryParameters.containsKey('EnableResumable'), isFalse);
|
||||||
expect(capturedNextUp!.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
expect(capturedNextUp!.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||||
});
|
});
|
||||||
@@ -830,11 +834,16 @@ void main() {
|
|||||||
expect(resume.queryParameters['userId'], 'user-1');
|
expect(resume.queryParameters['userId'], 'user-1');
|
||||||
expect(resume.queryParameters['Limit'], '3');
|
expect(resume.queryParameters['Limit'], '3');
|
||||||
expect(resume.queryParameters['MediaTypes'], 'Video');
|
expect(resume.queryParameters['MediaTypes'], 'Video');
|
||||||
|
expect(resume.queryParameters['Recursive'], 'true');
|
||||||
|
expect(resume.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(resume.queryParameters['ImageTypeLimit'], '1');
|
||||||
final nextUp = requests.singleWhere((uri) => uri.path == '/Shows/NextUp');
|
final nextUp = requests.singleWhere((uri) => uri.path == '/Shows/NextUp');
|
||||||
expect(nextUp.queryParameters['userId'], 'user-1');
|
expect(nextUp.queryParameters['userId'], 'user-1');
|
||||||
expect(nextUp.queryParameters['Limit'], '3');
|
expect(nextUp.queryParameters['Limit'], '3');
|
||||||
expect(nextUp.queryParameters['EnableResumable'], 'false');
|
expect(nextUp.queryParameters['EnableResumable'], 'false');
|
||||||
expect(nextUp.queryParameters['EnableTotalRecordCount'], 'false');
|
expect(nextUp.queryParameters['EnableTotalRecordCount'], 'false');
|
||||||
|
expect(nextUp.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(nextUp.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -892,6 +901,8 @@ void main() {
|
|||||||
expect(nextUp.queryParameters['Limit'], '12');
|
expect(nextUp.queryParameters['Limit'], '12');
|
||||||
expect(nextUp.queryParameters['EnableResumable'], 'false');
|
expect(nextUp.queryParameters['EnableResumable'], 'false');
|
||||||
expect(nextUp.queryParameters['EnableTotalRecordCount'], 'false');
|
expect(nextUp.queryParameters['EnableTotalRecordCount'], 'false');
|
||||||
|
expect(nextUp.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(nextUp.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -920,6 +931,8 @@ void main() {
|
|||||||
expect(nextUp.queryParameters['Limit'], '12');
|
expect(nextUp.queryParameters['Limit'], '12');
|
||||||
expect(nextUp.queryParameters['EnableResumable'], 'false');
|
expect(nextUp.queryParameters['EnableResumable'], 'false');
|
||||||
expect(nextUp.queryParameters['EnableTotalRecordCount'], 'false');
|
expect(nextUp.queryParameters['EnableTotalRecordCount'], 'false');
|
||||||
|
expect(nextUp.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(nextUp.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -944,6 +957,8 @@ void main() {
|
|||||||
expect(captured!.path, '/Users/user-1/Items/Latest');
|
expect(captured!.path, '/Users/user-1/Items/Latest');
|
||||||
expect(captured!.queryParameters['Limit'], '80');
|
expect(captured!.queryParameters['Limit'], '80');
|
||||||
expect(captured!.queryParameters['IncludeItemTypes'], 'Movie,Series,Episode');
|
expect(captured!.queryParameters['IncludeItemTypes'], 'Movie,Series,Episode');
|
||||||
|
expect(captured!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(captured!.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(captured!.queryParameters.containsKey('ParentId'), isFalse);
|
expect(captured!.queryParameters.containsKey('ParentId'), isFalse);
|
||||||
client.close();
|
client.close();
|
||||||
});
|
});
|
||||||
@@ -957,6 +972,9 @@ void main() {
|
|||||||
expect(captured!.queryParameters['userId'], 'user-1');
|
expect(captured!.queryParameters['userId'], 'user-1');
|
||||||
expect(captured!.queryParameters['Limit'], '50');
|
expect(captured!.queryParameters['Limit'], '50');
|
||||||
expect(captured!.queryParameters['MediaTypes'], 'Video');
|
expect(captured!.queryParameters['MediaTypes'], 'Video');
|
||||||
|
expect(captured!.queryParameters['Recursive'], 'true');
|
||||||
|
expect(captured!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(captured!.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(captured!.queryParameters.containsKey('ParentId'), isFalse);
|
expect(captured!.queryParameters.containsKey('ParentId'), isFalse);
|
||||||
client.close();
|
client.close();
|
||||||
});
|
});
|
||||||
@@ -972,6 +990,8 @@ void main() {
|
|||||||
expect(captured!.queryParameters.containsKey('ParentId'), isFalse);
|
expect(captured!.queryParameters.containsKey('ParentId'), isFalse);
|
||||||
expect(captured!.queryParameters['EnableResumable'], 'false');
|
expect(captured!.queryParameters['EnableResumable'], 'false');
|
||||||
expect(captured!.queryParameters['EnableTotalRecordCount'], 'false');
|
expect(captured!.queryParameters['EnableTotalRecordCount'], 'false');
|
||||||
|
expect(captured!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(captured!.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(captured!.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
expect(captured!.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||||
client.close();
|
client.close();
|
||||||
});
|
});
|
||||||
@@ -984,6 +1004,8 @@ void main() {
|
|||||||
expect(captured!.path, '/Users/user-1/Items/Latest');
|
expect(captured!.path, '/Users/user-1/Items/Latest');
|
||||||
expect(captured!.queryParameters['ParentId'], 'lib-99');
|
expect(captured!.queryParameters['ParentId'], 'lib-99');
|
||||||
expect(captured!.queryParameters['Limit'], '30');
|
expect(captured!.queryParameters['Limit'], '30');
|
||||||
|
expect(captured!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(captured!.queryParameters['ImageTypeLimit'], '1');
|
||||||
// ParentId-scoped Latest should NOT also pin IncludeItemTypes (the
|
// ParentId-scoped Latest should NOT also pin IncludeItemTypes (the
|
||||||
// library already constrains the kinds returned).
|
// library already constrains the kinds returned).
|
||||||
expect(captured!.queryParameters.containsKey('IncludeItemTypes'), isFalse);
|
expect(captured!.queryParameters.containsKey('IncludeItemTypes'), isFalse);
|
||||||
@@ -998,6 +1020,9 @@ void main() {
|
|||||||
expect(captured!.path, '/UserItems/Resume');
|
expect(captured!.path, '/UserItems/Resume');
|
||||||
expect(captured!.queryParameters['ParentId'], 'lib-99');
|
expect(captured!.queryParameters['ParentId'], 'lib-99');
|
||||||
expect(captured!.queryParameters['userId'], 'user-1');
|
expect(captured!.queryParameters['userId'], 'user-1');
|
||||||
|
expect(captured!.queryParameters['Recursive'], 'true');
|
||||||
|
expect(captured!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(captured!.queryParameters['ImageTypeLimit'], '1');
|
||||||
client.close();
|
client.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1011,6 +1036,8 @@ void main() {
|
|||||||
expect(captured!.queryParameters['userId'], 'user-1');
|
expect(captured!.queryParameters['userId'], 'user-1');
|
||||||
expect(captured!.queryParameters['EnableResumable'], 'false');
|
expect(captured!.queryParameters['EnableResumable'], 'false');
|
||||||
expect(captured!.queryParameters['EnableTotalRecordCount'], 'false');
|
expect(captured!.queryParameters['EnableTotalRecordCount'], 'false');
|
||||||
|
expect(captured!.queryParameters['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(captured!.queryParameters['ImageTypeLimit'], '1');
|
||||||
expect(captured!.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
expect(captured!.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||||
client.close();
|
client.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ void main() {
|
|||||||
expect(params['Recursive'], 'true');
|
expect(params['Recursive'], 'true');
|
||||||
expect(params['Fields'], 'UserData');
|
expect(params['Fields'], 'UserData');
|
||||||
expect(params['IncludeItemTypes'], isNotEmpty);
|
expect(params['IncludeItemTypes'], isNotEmpty);
|
||||||
|
expect(params['EnableImageTypes'], 'Primary,Backdrop,Thumb,Logo');
|
||||||
|
expect(params['ImageTypeLimit'], '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('movie kind maps to IncludeItemTypes=Movie', () {
|
test('movie kind maps to IncludeItemTypes=Movie', () {
|
||||||
|
|||||||
Reference in New Issue
Block a user