fix(jellyfin): stop overriding a server subtitle mode of None

Jellyfin answers PlaybackInfo with a null DefaultSubtitleStreamIndex when the
user's SubtitleMode is None: the index is the server's whole answer, and null
means it picked no subtitle. The mapper read null as "the server did not say"
and promoted the container's default/forced flags to a server selection
instead, which outranks the profile subtitle mode in the selection ladder. A
viewer who had turned subtitles off for their Jellyfin user got them switched
back on by every item that carried a default or forced row.

Only the row the server names is selected now. A stream the viewer picks, and
an explicit off, still survive per item because Plezy reports the index
through playback progress and the server hands it back as that index or -1.

close #1779
This commit is contained in:
edde746
2026-08-03 17:07:05 +02:00
parent bbaf5f0f9e
commit e7aa1e4782
3 changed files with 93 additions and 6 deletions
@@ -2,7 +2,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_source_info.dart';
import 'package:plezy/models/jellyfin/jellyfin_user_profile.dart';
import 'package:plezy/mpv/mpv.dart';
import 'package:plezy/services/jellyfin_media_info_mapper.dart';
import 'package:plezy/services/playback_initialization_types.dart';
import 'package:plezy/services/playback_subtitle_resolver.dart';
import 'package:plezy/services/subtitle_preference.dart';
@@ -238,6 +240,59 @@ void main() {
expect(result.sidecarsAtOpen, isEmpty);
});
group('Jellyfin server subtitle answer', () {
// Verbatim shape of a direct-played episode from a real Jellyfin server:
// the container flags one row default+forced, and the response carries the
// index the server picked for this user (null when it picked none).
MediaSourceInfo jellyfinInfo({Object? defaultSubtitleStreamIndex}) => jellyfinMediaSourceToMediaSourceInfo({
'Id': 'src-1',
'DefaultSubtitleStreamIndex': defaultSubtitleStreamIndex,
'MediaStreams': [
{'Index': 1, 'Type': 'Audio', 'Language': 'eng', 'IsDefault': true},
{'Index': 3, 'Type': 'Subtitle', 'Language': 'eng', 'Codec': 'ass', 'IsDefault': true, 'IsForced': true},
{'Index': 4, 'Type': 'Subtitle', 'Language': 'eng', 'Codec': 'ass'},
],
});
JellyfinUserProfile profileWithMode(String mode) => JellyfinUserProfile.fromUserDto({
'Configuration': {'SubtitleMode': mode, 'PlayDefaultAudioTrack': true},
});
test('a user who turned subtitles off on the server gets none (#1779)', () {
final result = PlaybackSubtitleResolver.resolve(
metadata: metadata,
mediaInfo: jellyfinInfo(),
sidecars: const [],
profileSettings: profileWithMode('None'),
);
expect(result.isOff, isTrue);
});
test('the stream the server did pick still plays', () {
final result = PlaybackSubtitleResolver.resolve(
metadata: metadata,
mediaInfo: jellyfinInfo(defaultSubtitleStreamIndex: 3),
sidecars: const [],
profileSettings: profileWithMode('Default'),
);
expect(result.isOff, isFalse);
expect(result.primarySourceStreamId, 3);
});
test('an off persisted through progress reports plays nothing', () {
final result = PlaybackSubtitleResolver.resolve(
metadata: metadata,
mediaInfo: jellyfinInfo(defaultSubtitleStreamIndex: -1),
sidecars: const [],
profileSettings: profileWithMode('Default'),
);
expect(result.isOff, isTrue);
});
});
test('retains each source metadata row for a shared preloaded container', () {
final result = PlaybackSubtitleResolver.resolve(
metadata: metadata,