fix(player): stop writing show-wide track defaults

This commit is contained in:
edde746
2026-06-21 16:23:54 +02:00
parent 19abd7831f
commit 5a1d378b42
4 changed files with 25 additions and 90 deletions
+15 -28
View File
@@ -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 = <Future>[];
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));
};
}
-23
View File
@@ -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<bool> setMetadataPreferences(String ratingKey, {String? audioLanguage, String? subtitleLanguage}) async {
final queryParams = <String, dynamic>{};
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
+9 -32
View File
@@ -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<void> 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<int?> _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<void> _saveTrackPreferences({
required int partId,
required String trackType,
String? languageCode,
int? streamID,
}) async {
/// Save the stream selection for the current part to the server.
Future<void> _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);
}
}
+1 -7
View File
@@ -144,13 +144,7 @@ Future<void> _drainAsync() async {
}
}
Future<void> _noopPersister({
required String id,
required int partId,
required String trackType,
String? languageCode,
int? streamID,
}) async {}
Future<void> _noopPersister({required int partId, required String trackType, int? streamID}) async {}
void main() {
// The constructor doesn't touch prefs, but [dispose] / [applyTrackSelection]