From 5a1d378b429ae7d1488661729674a4ebb7db841d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:21:18 +0200 Subject: [PATCH] fix(player): stop writing show-wide track defaults --- lib/screens/video_player_screen.dart | 43 ++++++++++----------------- lib/services/plex_client.dart | 23 -------------- lib/services/track_manager.dart | 41 ++++++------------------- test/services/track_manager_test.dart | 8 +---- 4 files changed, 25 insertions(+), 90 deletions(-) diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 675bb94a..bcc15269 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -168,37 +168,24 @@ _PlaybackOpenTiming _playbackOpenTiming({ ); } -/// Builds a [TrackPreferencePersister] that fans the language-preference + -/// stream-selection writes out to a [PlexClient] resolved lazily on each -/// call. Returns a no-op-on-null persister so the [TrackManager] doesn't -/// have to import [PlexClient] itself; the resolver returning null (e.g. -/// when the active server is Jellyfin) makes the call short-circuit. +/// Builds a [TrackPreferencePersister] that writes the per-episode stream +/// selection out to a [PlexClient] resolved lazily on each call. Returns a +/// no-op-on-null persister so the [TrackManager] doesn't have to import +/// [PlexClient] itself; the resolver returning null (e.g. when the active +/// server is Jellyfin) makes the call short-circuit. +/// +/// Only the current episode's part is touched — we deliberately do NOT write +/// the show-wide audio/subtitle language default (#1393): an in-player track +/// change should not silently rewrite the whole series' Plex prefs. The +/// explicit path for that lives in the metadata-edit UI. TrackPreferencePersister _plexTrackPersister(PlexClient? Function() resolve) { - return ({ - required String id, - required int partId, - required String trackType, - String? languageCode, - int? streamID, - }) async { + return ({required int partId, required String trackType, int? streamID}) async { + if (streamID == null) return; final client = resolve(); if (client == null) return; - final futures = []; - if (languageCode != null && (trackType == 'subtitle' || languageCode.isNotEmpty)) { - futures.add( - trackType == 'audio' - ? client.setMetadataPreferences(id, audioLanguage: languageCode) - : client.setMetadataPreferences(id, subtitleLanguage: languageCode), - ); - } - if (streamID != null) { - futures.add( - trackType == 'audio' - ? client.selectStreams(partId, audioStreamID: streamID, allParts: true) - : client.selectStreams(partId, subtitleStreamID: streamID, allParts: true), - ); - } - await Future.wait(futures); + await (trackType == 'audio' + ? client.selectStreams(partId, audioStreamID: streamID, allParts: true) + : client.selectStreams(partId, subtitleStreamID: streamID, allParts: true)); }; } diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 609b6282..75f76ff9 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -1198,29 +1198,6 @@ class PlexClient ); } - /// Set per-media language preferences (audio and subtitle) - /// For TV shows, use grandparentRatingKey to set preference for the entire series - /// For movies, use the movie's ratingKey - Future setMetadataPreferences(String ratingKey, {String? audioLanguage, String? subtitleLanguage}) async { - final queryParams = {}; - if (audioLanguage != null) { - queryParams['audioLanguage'] = audioLanguage; - } - if (subtitleLanguage != null) { - queryParams['subtitleLanguage'] = subtitleLanguage; - } - - // If no preferences to set, return early - if (queryParams.isEmpty) { - return true; - } - - return _wrapBoolApiCall( - () => _http.put('/library/metadata/$ratingKey/prefs', queryParameters: queryParams), - 'Failed to set metadata preferences', - ); - } - /// Select specific audio and subtitle streams for playback /// This updates which streams are "selected" in the media metadata /// Uses the part ID from media info for accurate stream selection diff --git a/lib/services/track_manager.dart b/lib/services/track_manager.dart index e2a444a4..794594ef 100644 --- a/lib/services/track_manager.dart +++ b/lib/services/track_manager.dart @@ -3,7 +3,6 @@ import 'dart:async'; import '../mpv/mpv.dart'; import '../media/media_item.dart'; -import '../media/media_item_types.dart'; import '../media/media_server_user_profile.dart'; import '../media/media_source_info.dart'; import '../services/settings_service.dart'; @@ -12,16 +11,14 @@ import '../utils/app_logger.dart'; import '../utils/language_codes.dart'; import '../utils/track_label_builder.dart'; -/// Persists a track choice through Plex's immediate preference endpoints. +/// Persists a track choice for the current part to the server. /// Backends that persist through another path (Jellyfin uses playback progress -/// stream indexes) or lack server-side track preferences leave this null. +/// stream indexes) or lack server-side stream selection leave this null. /// [trackType] is `'audio'` or `'subtitle'`. typedef TrackPreferencePersister = Future Function({ - required String id, required int partId, required String trackType, - String? languageCode, int? streamID, }); @@ -342,7 +339,7 @@ class TrackManager { } } - await _saveTrackPreferences(partId: partId, trackType: 'audio', languageCode: track.language, streamID: streamID); + await _saveTrackPreferences(partId: partId, trackType: 'audio', streamID: streamID); } /// Handle subtitle track changes — save stream selection and language preference. @@ -351,16 +348,12 @@ class TrackManager { final partId = await _guardTrackChange(info); if (partId == null) return; - String? languageCode; int? streamID; if (track.id == 'no') { - languageCode = 'none'; streamID = 0; appLogger.i('User turned subtitles off, saving preference'); } else if (info != null) { - languageCode = track.language; - streamID = _matchTrackByAttributes( mpvLanguage: track.language, mpvTitle: track.title, @@ -388,7 +381,7 @@ class TrackManager { } } - await _saveTrackPreferences(partId: partId, trackType: 'subtitle', languageCode: languageCode, streamID: streamID); + await _saveTrackPreferences(partId: partId, trackType: 'subtitle', streamID: streamID); } /// Handle secondary subtitle track changes — no server save needed. @@ -399,11 +392,6 @@ class TrackManager { // ── Private helpers ──────────────────────────────────────────────── - /// Series/movie-level identifier used for language preferences. - String get _preferenceId { - return metadata.isEpisode ? (metadata.grandparentId ?? metadata.id) : metadata.id; - } - /// Common guard checks for track change handlers. Future _guardTrackChange(MediaSourceInfo? info) async { final settings = await SettingsService.getInstance(); @@ -423,29 +411,18 @@ class TrackManager { return partId; } - /// Save language preference and stream selection to the server. - Future _saveTrackPreferences({ - required int partId, - required String trackType, - String? languageCode, - int? streamID, - }) async { + /// Save the stream selection for the current part to the server. + Future _saveTrackPreferences({required int partId, required String trackType, int? streamID}) async { try { if (!isActive()) return; final persist = persistTrackPreference; if (persist == null) { return; } - await persist( - id: _preferenceId, - partId: partId, - trackType: trackType, - languageCode: languageCode, - streamID: streamID, - ); - appLogger.d('Successfully saved $trackType preferences (language + stream)'); + await persist(partId: partId, trackType: trackType, streamID: streamID); + appLogger.d('Successfully saved $trackType stream selection'); } catch (e) { - appLogger.e('Failed to save $trackType preferences', error: e); + appLogger.e('Failed to save $trackType stream selection', error: e); } } diff --git a/test/services/track_manager_test.dart b/test/services/track_manager_test.dart index e2669ae6..7b3a2250 100644 --- a/test/services/track_manager_test.dart +++ b/test/services/track_manager_test.dart @@ -144,13 +144,7 @@ Future _drainAsync() async { } } -Future _noopPersister({ - required String id, - required int partId, - required String trackType, - String? languageCode, - int? streamID, -}) async {} +Future _noopPersister({required int partId, required String trackType, int? streamID}) async {} void main() { // The constructor doesn't touch prefs, but [dispose] / [applyTrackSelection]