feat(jellyfin): show media quality labels

This commit is contained in:
edde746
2026-06-04 09:56:10 +02:00
parent 0e7ff609b6
commit e5bbdc06f9
7 changed files with 283 additions and 76 deletions
@@ -172,6 +172,46 @@ void main() {
expect(extras[1].posterThumb(), extras[1].artPath);
});
test('fetchChildren requests media sources for episode-row quality labels', () async {
final requests = <Uri>[];
final scoped = JellyfinClient.forTesting(
connection: _conn(),
httpClient: MockClient((request) async {
requests.add(request.url);
if (request.url.path == '/Shows/season-1/Seasons') {
return http.Response('not found', 404);
}
if (request.url.path == '/Items') {
return http.Response(jsonEncode({'Items': <Object>[], 'TotalRecordCount': 0}), 200);
}
return http.Response('unexpected ${request.url}', 500);
}),
);
addTearDown(scoped.close);
await scoped.fetchChildren('season-1');
final directChildrenRequest = requests.firstWhere((uri) => uri.path == '/Items');
expect(directChildrenRequest.queryParameters['Fields']!.split(','), contains('MediaSources'));
});
test('fetchPlayableDescendantsPage requests media sources for episode-row quality labels', () async {
Uri? capturedUri;
final scoped = JellyfinClient.forTesting(
connection: _conn(),
httpClient: MockClient((request) async {
capturedUri = request.url;
return http.Response(jsonEncode({'Items': <Object>[], 'TotalRecordCount': 0}), 200);
}),
);
addTearDown(scoped.close);
await scoped.fetchPlayableDescendantsPage('show-1');
expect(capturedUri!.path, '/Items');
expect(capturedUri!.queryParameters['Fields']!.split(','), contains('MediaSources'));
});
test('reportPlaybackProgress sends media source and stream indexes', () async {
Uri? capturedUri;
String? capturedBody;
+67
View File
@@ -313,6 +313,73 @@ void main() {
expect(subtitle.isExternal, isTrue);
expect(subtitle.sidecarPath, '/Videos/movie-1/movie-1/Subtitles/2/Stream.srt');
});
test('media streams map Jellyfin Dolby Vision, HDR, and source default audio', () {
final json = {
'Id': 'movie-1',
'Name': 'Movie',
'Type': 'Movie',
'MediaSources': [
{
'Id': 'src-1',
'DefaultAudioStreamIndex': 2,
'MediaStreams': [
{
'Index': 0,
'Type': 'Video',
'Codec': 'hevc',
'Width': 3840,
'Height': 2160,
'VideoRangeType': 'DOVI',
'VideoRange': 'HDR',
'VideoDoViTitle': 'Dolby Vision Profile 8',
'DvProfile': 8,
'DvLevel': 6,
'DvBlSignalCompatibilityId': 1,
},
{'Index': 1, 'Type': 'Audio', 'Codec': 'eac3', 'Channels': 6, 'IsDefault': true},
{'Index': 2, 'Type': 'Audio', 'Codec': 'aac', 'Channels': 2},
],
},
],
};
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
final streams = item.mediaVersions!.single.parts.single.streams;
final video = streams.firstWhere((stream) => stream.kind == MediaStreamKind.video);
final firstAudio = streams.firstWhere((stream) => stream.index == 1);
final selectedAudio = streams.firstWhere((stream) => stream.index == 2);
expect(video.codec, 'hevc');
expect(video.hdr, isTrue);
expect(video.dolbyVision, isTrue);
expect(video.dolbyVisionProfile, 8);
expect(firstAudio.selected, isFalse);
expect(selectedAudio.selected, isTrue);
});
test('media streams map Jellyfin HDR without Dolby Vision', () {
final json = {
'Id': 'movie-1',
'Name': 'Movie',
'Type': 'Movie',
'MediaSources': [
{
'Id': 'src-1',
'MediaStreams': [
{'Index': 0, 'Type': 'Video', 'Codec': 'hevc', 'VideoRangeType': 'HDR10', 'VideoRange': 'HDR'},
],
},
],
};
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
final video = item.mediaVersions!.single.parts.single.streams.single;
expect(video.hdr, isTrue);
expect(video.dolbyVision, isFalse);
expect(video.dolbyVisionProfile, isNull);
});
});
group('JellyfinMappers.library', () {
+36
View File
@@ -6,6 +6,7 @@ import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_part.dart';
import 'package:plezy/media/media_stream.dart';
import 'package:plezy/media/media_version.dart';
import 'package:plezy/services/jellyfin_mappers.dart';
import 'package:plezy/services/plex_mappers.dart';
import 'package:plezy/utils/media_quality_labels.dart';
@@ -151,6 +152,41 @@ void main() {
expect(buildMediaQualityLabels(item), ['4K', 'DV P8', 'EAC3 5.1']);
});
test('formats Jellyfin stream metadata from MediaSources', () {
final item = JellyfinMappers.mediaItem(
{
'Id': 'movie-1',
'Name': 'Movie',
'Type': 'Movie',
'MediaSources': [
{
'Id': 'source-1',
'DefaultAudioStreamIndex': 2,
'MediaStreams': [
{
'Index': 0,
'Type': 'Video',
'Codec': 'hevc',
'Width': 3840,
'Height': 2160,
'VideoRangeType': 'DOVI',
'VideoDoViTitle': 'Dolby Vision Profile 8',
'DvProfile': 8,
'DvBlSignalCompatibilityId': 1,
},
{'Index': 1, 'Type': 'Audio', 'Codec': 'eac3', 'Channels': 6, 'IsDefault': true},
{'Index': 2, 'Type': 'Audio', 'Codec': 'aac', 'Channels': 2},
],
},
],
},
serverId: ServerId('jellyfin'),
absolutizer: null,
)!;
expect(buildMediaQualityLabels(item), ['4K', 'DV P8', 'AAC Stereo']);
});
test('uses selected audio stream and stereo label', () {
final item = _episodeWithVersion(
MediaVersion(