diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 51f33abb..e115e283 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -2229,43 +2229,10 @@ 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 showFuture = widget.metadata.grandparentRatingKey != null - ? client.getMetadataWithImages(widget.metadata.grandparentRatingKey!) - : Future.value(null); - final seasonFuture = widget.metadata.parentRatingKey != null - ? client.getMetadataWithImages(widget.metadata.parentRatingKey!) - : Future.value(null); - final (showMeta, seasonMeta) = await (showFuture, seasonFuture).wait; - // 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: effectiveMetadata, + metadata: widget.metadata, plexMediaInfo: _currentMediaInfo, ); diff --git a/lib/services/track_selection_service.dart b/lib/services/track_selection_service.dart index 1c8c01cf..c7a94af9 100644 --- a/lib/services/track_selection_service.dart +++ b/lib/services/track_selection_service.dart @@ -458,113 +458,6 @@ class TrackSelectionService { ); } - SubtitleTrack? findSubtitleTrackByProfile( - List availableTracks, - PlexUserProfile profile, { - AudioTrack? selectedAudioTrack, - }) { - if (availableTracks.isEmpty) return null; - - // Mode 0: Manually selected - return OFF - if (profile.autoSelectSubtitle == 0) return SubtitleTrack.off; - - // Mode 1: Shown with foreign audio - if (profile.autoSelectSubtitle == 1) { - if (selectedAudioTrack != null && profile.defaultSubtitleLanguage != null) { - final audioLang = selectedAudioTrack.language?.toLowerCase(); - final prefLang = profile.defaultSubtitleLanguage!.toLowerCase(); - final languageVariations = LanguageCodes.getVariations(prefLang); - - // If audio matches preferred language, no subtitles needed - if (audioLang != null && languageVariations.contains(audioLang)) { - return SubtitleTrack.off; - } - } - } - - // Mode 2: Always enabled (or continuing from mode 1 with foreign audio) - final preferredLanguages = _buildPreferredLanguages(profile, isAudio: false); - if (preferredLanguages.isEmpty) return null; - - // Apply filtering with fallback to original tracks if filter produces empty result - var candidateTracks = availableTracks; - candidateTracks = filterSubtitlesBySDH(candidateTracks, profile.defaultSubtitleAccessibility); - candidateTracks = filterSubtitlesByForced(candidateTracks, profile.defaultSubtitleForced); - candidateTracks = _applyFilterWithFallback(availableTracks, (_) => candidateTracks, 'strict filters'); - - for (final preferredLanguage in preferredLanguages) { - final match = _findTrackByPreferredLanguage( - candidateTracks, - preferredLanguage, - (t) => t.language, - (t) => t.title ?? 'Track ${t.id}', - 'subtitle', - ); - if (match != null) return match; - } - - return null; - } - - /// Filters subtitle tracks based on SDH (Subtitles for Deaf or Hard-of-Hearing) preference - /// - /// Values: - /// - 0: Prefer non-SDH subtitles - /// - 1: Prefer SDH subtitles - /// - 2: Only show SDH subtitles - /// - 3: Only show non-SDH subtitles - List filterSubtitlesBySDH(List tracks, int preference) { - if (preference == 0 || preference == 1) { - final preferSDH = preference == 1; - final preferred = tracks.where((t) => isSDH(t) == preferSDH).toList(); - return preferred.isNotEmpty ? preferred : tracks; - } else if (preference == 2) { - return tracks.where((t) => isSDH(t)).toList(); - } else if (preference == 3) { - return tracks.where((t) => !isSDH(t)).toList(); - } - return tracks; - } - - /// Filters subtitle tracks based on forced subtitle preference - /// - /// Values: - /// - 0: Prefer non-forced subtitles - /// - 1: Prefer forced subtitles - /// - 2: Only show forced subtitles - /// - 3: Only show non-forced subtitles - List filterSubtitlesByForced(List tracks, int preference) { - if (preference == 0 || preference == 1) { - final preferForced = preference == 1; - final preferred = tracks.where((t) => isForced(t) == preferForced).toList(); - return preferred.isNotEmpty ? preferred : tracks; - } else if (preference == 2) { - return tracks.where((t) => isForced(t)).toList(); - } else if (preference == 3) { - return tracks.where((t) => !isForced(t)).toList(); - } - return tracks; - } - - /// Checks if a subtitle track is SDH (Subtitles for Deaf or Hard-of-Hearing) - /// - /// Since mpv may not expose this directly, we infer from the title - bool isSDH(SubtitleTrack track) { - final title = track.title?.toLowerCase() ?? ''; - - // Look for common SDH indicators - return title.contains('sdh') || - title.contains('cc') || - title.contains('hearing impaired') || - title.contains('deaf'); - } - - /// Checks if a subtitle track is forced - bool isForced(SubtitleTrack track) { - final title = track.title?.toLowerCase() ?? ''; - return title.contains('forced'); - } - /// Find a track matching a preferred language from a list of tracks /// Returns the first track whose language matches any variation of the preferred language T? _findTrackByLanguageVariations( @@ -673,32 +566,30 @@ class TrackSelectionService { /// Select the best subtitle track based on priority: /// Priority 1: Preferred track from navigation - /// Priority 2: Plex-selected track from media info - /// Priority 3: Per-media language preference - /// Priority 4: User profile preferences - /// Priority 5: Default track - /// Priority 6: Off + /// Priority 2: Plex server's selected track (the server computes this from + /// account prefs, show/season prefs, and per-item stream selections) + /// Priority 3: Default track + /// Priority 4: Off TrackSelectionResult selectSubtitleTrack( List availableTracks, SubtitleTrack? preferredSubtitleTrack, AudioTrack? selectedAudioTrack, ) { - SubtitleTrack? subtitleToSelect; - // Priority 1: Try preferred track from navigation if (preferredSubtitleTrack != null) { if (preferredSubtitleTrack.id == 'no') { return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.navigation); } else if (availableTracks.isNotEmpty) { - subtitleToSelect = findBestSubtitleMatch(availableTracks, preferredSubtitleTrack); + final subtitleToSelect = findBestSubtitleMatch(availableTracks, preferredSubtitleTrack); if (subtitleToSelect != null) { return TrackSelectionResult(subtitleToSelect, TrackSelectionPriority.navigation); } } } - // Priority 2: Check Plex-selected track from media info - // If a subtitle IS actively selected, the user manually chose it — respect that. + // Priority 2: Trust Plex server's selected track + // The server applies all preference levels (account, show/season, per-item) + // and exposes the result via the `selected` flag on streams. if (plexMediaInfo != null && availableTracks.isNotEmpty) { final plexSelectedTrack = plexMediaInfo!.subtitleTracks.where((t) => t.selected).firstOrNull; @@ -708,68 +599,13 @@ class TrackSelectionService { if (matchedMpvTrack != null) { return TrackSelectionResult(matchedMpvTrack, TrackSelectionPriority.plexSelected); } + } else if (plexMediaInfo!.subtitleTracks.isNotEmpty) { + // Server has subtitle tracks but none selected — trust that decision + return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.plexSelected); } } - // Priority 3: Try per-media language/subtitle mode preference (show/season/item-level) - 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); - } - - // 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, - ); - if (languageMatches(matchedTrack.language, metadata.subtitleLanguage)) { - return TrackSelectionResult(matchedTrack, TrackSelectionPriority.perMedia); - } - } - } - - // Priority 4: No subtitle selected by server — trust that decision - if (plexMediaInfo != null && plexMediaInfo!.subtitleTracks.isNotEmpty && availableTracks.isNotEmpty) { - return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.plexSelected); - } - - // Priority 5: Apply user profile preferences - if (profileSettings != null && availableTracks.isNotEmpty) { - subtitleToSelect = findSubtitleTrackByProfile( - availableTracks, - profileSettings!, - selectedAudioTrack: selectedAudioTrack, - ); - if (subtitleToSelect != null) { - return TrackSelectionResult(subtitleToSelect, TrackSelectionPriority.profile); - } - } - - // Priority 6: Check for default subtitle + // Priority 3: Check for default subtitle if (availableTracks.isNotEmpty) { final defaultTrack = availableTracks.firstWhere((t) => t.isDefault, orElse: () => availableTracks.first); if (defaultTrack.isDefault) { @@ -777,7 +613,7 @@ class TrackSelectionService { } } - // Priority 7: Turn off subtitles + // Priority 4: Turn off subtitles return TrackSelectionResult(SubtitleTrack.off, TrackSelectionPriority.off); }