fix: respect series-level subtitle mode for episodes

This commit is contained in:
edde746
2026-03-05 18:55:44 +01:00
parent 22a733280a
commit 863f90f65d
5 changed files with 71 additions and 5 deletions
+4
View File
@@ -89,6 +89,7 @@ class PlexMetadata with MultiServerFields {
final List<PlexRole>? role; // Cast members
final String? audioLanguage; // Per-media preferred audio language
final String? subtitleLanguage; // Per-media preferred subtitle language
final int? subtitleMode; // Per-media subtitle mode (0=manual, 1=foreign audio, 2=always, -1=account default)
final int? playlistItemID; // Playlist item ID (for dumb playlists only)
final int? playQueueItemID; // Play queue item ID (unique even for duplicates)
final int? librarySectionID; // Library section ID this item belongs to
@@ -175,6 +176,7 @@ class PlexMetadata with MultiServerFields {
this.role,
this.audioLanguage,
this.subtitleLanguage,
this.subtitleMode,
this.playlistItemID,
this.playQueueItemID,
this.librarySectionID,
@@ -232,6 +234,7 @@ class PlexMetadata with MultiServerFields {
List<PlexRole>? role,
String? audioLanguage,
String? subtitleLanguage,
int? subtitleMode,
int? playlistItemID,
int? playQueueItemID,
int? librarySectionID,
@@ -287,6 +290,7 @@ class PlexMetadata with MultiServerFields {
role: role ?? this.role,
audioLanguage: audioLanguage ?? this.audioLanguage,
subtitleLanguage: subtitleLanguage ?? this.subtitleLanguage,
subtitleMode: subtitleMode ?? this.subtitleMode,
playlistItemID: playlistItemID ?? this.playlistItemID,
playQueueItemID: playQueueItemID ?? this.playQueueItemID,
librarySectionID: librarySectionID ?? this.librarySectionID,
+2
View File
@@ -45,6 +45,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
role: (json['Role'] as List<dynamic>?)?.map((e) => PlexRole.fromJson(e as Map<String, dynamic>)).toList(),
audioLanguage: json['audioLanguage'] as String?,
subtitleLanguage: json['subtitleLanguage'] as String?,
subtitleMode: (json['subtitleMode'] as num?)?.toInt(),
playlistItemID: (json['playlistItemID'] as num?)?.toInt(),
playQueueItemID: (json['playQueueItemID'] as num?)?.toInt(),
librarySectionID: (json['librarySectionID'] as num?)?.toInt(),
@@ -99,6 +100,7 @@ Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dyn
'Role': instance.role,
'audioLanguage': instance.audioLanguage,
'subtitleLanguage': instance.subtitleLanguage,
'subtitleMode': instance.subtitleMode,
'playlistItemID': instance.playlistItemID,
'playQueueItemID': instance.playQueueItemID,
'librarySectionID': instance.librarySectionID,
+2
View File
@@ -290,6 +290,8 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
return meta.audioLanguage ?? '';
case 'subtitleLanguage':
return meta.subtitleLanguage ?? '';
case 'subtitleMode':
return meta.subtitleMode?.toString() ?? '-1';
default:
return '';
}
+33 -1
View File
@@ -2229,10 +2229,42 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final settingsService = await SettingsService.getInstance();
if (!mounted || player == null) return;
// For episodes without per-media prefs, fetch show/season metadata
// to get series-level language/subtitle mode preferences
PlexMetadata effectiveMetadata = widget.metadata;
if (!widget.isOffline &&
widget.metadata.isEpisode &&
widget.metadata.subtitleMode == null &&
widget.metadata.audioLanguage == null) {
try {
final client = _getClientForMetadata(context);
final showMeta = widget.metadata.grandparentRatingKey != null
? await client.getMetadataWithImages(widget.metadata.grandparentRatingKey!)
: null;
final seasonMeta = widget.metadata.parentRatingKey != null
? await client.getMetadataWithImages(widget.metadata.parentRatingKey!)
: null;
// Season overrides show (Plex preference hierarchy)
final inheritedAudio = seasonMeta?.audioLanguage ?? showMeta?.audioLanguage;
final inheritedSubLang = seasonMeta?.subtitleLanguage ?? showMeta?.subtitleLanguage;
final inheritedSubMode = seasonMeta?.subtitleMode ?? showMeta?.subtitleMode;
if (inheritedAudio != null || inheritedSubLang != null || inheritedSubMode != null) {
effectiveMetadata = widget.metadata.copyWith(
audioLanguage: inheritedAudio,
subtitleLanguage: inheritedSubLang,
subtitleMode: inheritedSubMode,
);
}
} catch (e) {
appLogger.w('Failed to fetch show/season prefs for track selection', error: e);
}
if (!mounted || player == null) return;
}
final trackService = TrackSelectionService(
player: player!,
profileSettings: profileSettings,
metadata: widget.metadata,
metadata: effectiveMetadata,
plexMediaInfo: _currentMediaInfo,
);
+30 -4
View File
@@ -710,11 +710,37 @@ class TrackSelectionService {
}
}
// Priority 3: Try per-media language preference
if (metadata.subtitleLanguage != null) {
if (metadata.subtitleLanguage == 'none' || metadata.subtitleLanguage!.isEmpty) {
// Priority 3: Try per-media language/subtitle mode preference
final hasSubtitleMode = metadata.subtitleMode != null && metadata.subtitleMode != -1;
if (metadata.subtitleLanguage != null || hasSubtitleMode) {
if (metadata.subtitleLanguage == 'none' || (metadata.subtitleLanguage?.isEmpty ?? false)) {
return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.perMedia);
} else if (availableTracks.isNotEmpty) {
}
// Determine effective subtitle mode: per-media overrides account setting
final effectiveMode = hasSubtitleMode
? metadata.subtitleMode!
: profileSettings?.autoSelectSubtitle;
// Mode 0: Manually selected / off
if (effectiveMode == 0) {
return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.perMedia);
}
// Mode 1: Shown with foreign audio only
if (effectiveMode == 1 && selectedAudioTrack != null && metadata.subtitleLanguage != null) {
final audioLang = selectedAudioTrack.language?.toLowerCase();
final prefLang = metadata.subtitleLanguage!.toLowerCase();
final languageVariations = LanguageCodes.getVariations(prefLang);
// If audio matches preferred subtitle language, no subtitles needed
if (audioLang != null && languageVariations.contains(audioLang)) {
return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.perMedia);
}
}
// Mode 2 / null / continuing from mode 1 with foreign audio: find matching subtitle
if (metadata.subtitleLanguage != null && availableTracks.isNotEmpty) {
final matchedTrack = availableTracks.firstWhere(
(track) => languageMatches(track.language, metadata.subtitleLanguage),
orElse: () => availableTracks.first,