diff --git a/lib/services/plex_playback_mapper.dart b/lib/services/plex_playback_mapper.dart index c1e878f3..342b4387 100644 --- a/lib/services/plex_playback_mapper.dart +++ b/lib/services/plex_playback_mapper.dart @@ -10,6 +10,10 @@ import 'plex_mappers.dart'; const _streamReader = PlexFileInfoStreamReader(); +void _logMalformedStream(Object error, StackTrace stackTrace, Map _) { + appLogger.w('Skipping malformed Plex stream metadata', error: error, stackTrace: stackTrace); +} + List _mapList(Object? raw) { final values = flexibleList(raw); if (values == null || values.isEmpty) return const []; @@ -122,7 +126,7 @@ PlexVideoPlaybackData parsePlexVideoPlaybackDataFromJson( if (partKey != null) { videoUrl = '$baseUrl$partKey'.withPlexToken(token); - final streams = walkStreams(flexibleList(part['Stream']), _streamReader); + final streams = walkStreams(flexibleList(part['Stream']), _streamReader, onMalformed: _logMalformedStream); final chapters = plexChaptersFromCacheJson(metadataJson); mediaInfo = MediaSourceInfo( @@ -161,7 +165,7 @@ MediaFileInfo? parsePlexFileInfoFromJson(Map? metadataJson) { // One pass over the streams array, capturing both the raw video / audio // map pointers (for fields the parsed track classes don't carry — // colorSpace, bitDepth, …) and the parsed track lists. - final parsedTracks = walkStreams(flexibleList(part?['Stream']), _streamReader); + final parsedTracks = walkStreams(flexibleList(part?['Stream']), _streamReader, onMalformed: _logMalformedStream); final videoStream = parsedTracks.videoStream; final audioStream = parsedTracks.audioStream; diff --git a/test/services/plex_playback_mapper_test.dart b/test/services/plex_playback_mapper_test.dart index 65415b6f..bb6897ef 100644 --- a/test/services/plex_playback_mapper_test.dart +++ b/test/services/plex_playback_mapper_test.dart @@ -333,6 +333,41 @@ void main() { expect(criteria.primaries, 'bt2020'); expect(criteria.matrix, 'bt2020nc'); }); + + test('skips unidentifiable subtitle streams without blocking playback', () { + final result = parsePlexVideoPlaybackDataFromJson( + { + 'Media': [ + { + 'id': 1, + 'Part': [ + { + 'id': 10, + 'key': '/library/parts/10/file.mp4', + 'accessible': 1, + 'exists': 1, + 'Stream': [ + {'streamType': 1, 'id': 100}, + {'streamType': 2, 'id': 301, 'selected': true}, + {'streamType': 3, 'languageCode': 'eng'}, + {'streamType': 3, 'id': 'cc1', 'languageCode': 'eng'}, + {'streamType': 3, 'id': '401', 'languageCode': 'spa', 'selected': true}, + ], + }, + ], + }, + ], + }, + baseUrl: 'http://plex:32400', + token: 'token', + ); + + expect(result.videoUrl, 'http://plex:32400/library/parts/10/file.mp4?X-Plex-Token=token'); + expect(result.mediaInfo, isNotNull); + expect(result.mediaInfo!.audioTracks.map((track) => track.id), [301]); + expect(result.mediaInfo!.subtitleTracks.map((track) => track.id), [401]); + expect(result.mediaInfo!.subtitleTracks.single.selected, isTrue); + }); }); group('parsePlexFileInfoFromJson', () { @@ -395,5 +430,30 @@ void main() { expect(info?.audioTracks.single.selected, isTrue); expect(info?.subtitleTracks.single.key, '/subtitles/401'); }); + + test('skips unidentifiable subtitle streams in file info', () { + final info = parsePlexFileInfoFromJson({ + 'Media': [ + { + 'container': 'mp4', + 'Part': [ + { + 'file': '/media/movie.mp4', + 'Stream': [ + {'streamType': 1, 'id': 100}, + {'streamType': 3, 'id': null, 'languageCode': 'eng'}, + {'streamType': 3, 'id': 'cea-608', 'languageCode': 'eng'}, + {'streamType': 3, 'id': 402, 'languageCode': 'spa'}, + ], + }, + ], + }, + ], + }); + + expect(info, isNotNull); + expect(info!.filePath, '/media/movie.mp4'); + expect(info.subtitleTracks.map((track) => track.id), [402]); + }); }); }