fix: trust server-computed subtitle selection flags

The Plex server already applies all preference levels (account,
show/season, per-item) into stream `selected` flags. Remove redundant
client-side subtitleMode/account prefs logic and show/season metadata
fetching (2 fewer API calls per episode).
This commit is contained in:
edde746
2026-03-06 04:25:47 +01:00
parent f3d24866b7
commit 7ae293631a
2 changed files with 14 additions and 211 deletions
+1 -34
View File
@@ -2229,43 +2229,10 @@ 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 showFuture = widget.metadata.grandparentRatingKey != null
? client.getMetadataWithImages(widget.metadata.grandparentRatingKey!)
: Future<PlexMetadata?>.value(null);
final seasonFuture = widget.metadata.parentRatingKey != null
? client.getMetadataWithImages(widget.metadata.parentRatingKey!)
: Future<PlexMetadata?>.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,
);
+13 -177
View File
@@ -458,113 +458,6 @@ class TrackSelectionService {
);
}
SubtitleTrack? findSubtitleTrackByProfile(
List<SubtitleTrack> 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<SubtitleTrack>(
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<SubtitleTrack> filterSubtitlesBySDH(List<SubtitleTrack> 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<SubtitleTrack> filterSubtitlesByForced(List<SubtitleTrack> 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<T>(
@@ -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<SubtitleTrack> selectSubtitleTrack(
List<SubtitleTrack> 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);
}