From 863f90f65d081a34975ecd44df82a0b9af38f8ef Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:55:44 +0100 Subject: [PATCH] fix: respect series-level subtitle mode for episodes --- lib/models/plex_metadata.dart | 4 +++ lib/models/plex_metadata.g.dart | 2 ++ lib/screens/metadata_edit_screen.dart | 2 ++ lib/screens/video_player_screen.dart | 34 ++++++++++++++++++++++- lib/services/track_selection_service.dart | 34 ++++++++++++++++++++--- 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index cf9347a1..2392fb50 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -89,6 +89,7 @@ class PlexMetadata with MultiServerFields { final List? 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? 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, diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index d934f63b..4c737960 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -45,6 +45,7 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( role: (json['Role'] as List?)?.map((e) => PlexRole.fromJson(e as Map)).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 _$PlexMetadataToJson(PlexMetadata instance) => { return meta.audioLanguage ?? ''; case 'subtitleLanguage': return meta.subtitleLanguage ?? ''; + case 'subtitleMode': + return meta.subtitleMode?.toString() ?? '-1'; default: return ''; } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index e115e283..a76d1182 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -2229,10 +2229,42 @@ class VideoPlayerScreenState extends State 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, ); diff --git a/lib/services/track_selection_service.dart b/lib/services/track_selection_service.dart index 4812f43a..3492186c 100644 --- a/lib/services/track_selection_service.dart +++ b/lib/services/track_selection_service.dart @@ -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,