fix(playback): keep Plex playlist queue when entering the player
close #978
This commit is contained in:
@@ -67,6 +67,15 @@ class PlaybackStateProvider with ChangeNotifier, DisposableChangeNotifierMixin {
|
||||
/// Whether any queue-based playback is active
|
||||
bool get isQueueActive => _playQueueId != null && _isQueueMode;
|
||||
|
||||
/// Whether [item] belongs to the currently active queue. True for Plex
|
||||
/// items the server-side queue stamped with a `playQueueItemId`, and for
|
||||
/// items present in a Jellyfin local queue (synthetic id). Gates the
|
||||
/// player's "preserve vs. wipe launcher-set queue" decision in both
|
||||
/// [VideoPlayerScreen.initState] and `_ensurePlayQueue`, so a playlist
|
||||
/// or collection queue survives entry into the player instead of being
|
||||
/// replaced with a show queue.
|
||||
bool isItemInActiveQueue(MediaItem item) => isQueueActive && playQueueItemIdFor(item) != null;
|
||||
|
||||
/// The context key (show/season/playlist ratingKey) for the current session
|
||||
String? get shuffleContextKey => _contextKey;
|
||||
|
||||
|
||||
@@ -34,20 +34,19 @@ extension _VideoPlayerEpisodeQueueMethods on VideoPlayerScreenState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's already an active queue for THIS show.
|
||||
// A leftover queue from a different show or — more importantly —
|
||||
// from a different backend (Jellyfin's local queue is published
|
||||
// here too) would otherwise mask the new show's navigation.
|
||||
final existingContextKey = playbackState.shuffleContextKey;
|
||||
final isQueueActive = playbackState.isQueueActive;
|
||||
|
||||
if (isQueueActive && existingContextKey == showRatingKey) {
|
||||
// Preserve any queue this item belongs to — playlist, collection,
|
||||
// or same-show queue. `isItemInActiveQueue` is the same gate
|
||||
// VideoPlayerScreen.initState uses; a context-key check alone would
|
||||
// wipe a playlist queue (its key is the playlist id, not the show).
|
||||
// Only when the active queue is genuinely stale (item not in it)
|
||||
// do we clobber and create a fresh show queue.
|
||||
if (playbackState.isItemInActiveQueue(_currentMetadata)) {
|
||||
playbackState.setCurrentItem(_currentMetadata);
|
||||
appLogger.d('Using existing play queue (context: $existingContextKey)');
|
||||
appLogger.d('Using existing play queue (context: ${playbackState.shuffleContextKey})');
|
||||
return;
|
||||
}
|
||||
if (isQueueActive) {
|
||||
appLogger.d('Resetting stale play queue (was: $existingContextKey, now: $showRatingKey)');
|
||||
if (playbackState.isQueueActive) {
|
||||
appLogger.d('Resetting stale play queue (was: ${playbackState.shuffleContextKey}, now: $showRatingKey)');
|
||||
playbackState.clearShuffle();
|
||||
}
|
||||
|
||||
|
||||
@@ -449,8 +449,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
// playback (continue-watching, direct episode tap with no queue
|
||||
// launcher) clear any stale queue so prev/next stays consistent.
|
||||
final meta = widget.metadata;
|
||||
final inActiveQueue = playbackState.isQueueActive && playbackState.playQueueItemIdFor(meta) != null;
|
||||
if (inActiveQueue) {
|
||||
if (playbackState.isItemInActiveQueue(meta)) {
|
||||
playbackState.setCurrentItem(meta);
|
||||
} else {
|
||||
playbackState.clearShuffle();
|
||||
|
||||
@@ -227,11 +227,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('playQueueItemIdFor returns synthetic ids for Jellyfin local queue items', () {
|
||||
// Anchor: VideoPlayerScreen.initState gates clearShuffle on
|
||||
// `playQueueItemIdFor(meta) != null` so a Jellyfin playlist queue
|
||||
// survives entry into the player. If this returns null for queue
|
||||
// members, the player wipes the launcher-set queue and prev/next
|
||||
// walks the show instead of the playlist.
|
||||
// Anchor: VideoPlayerScreen.initState and `_ensurePlayQueue` both gate
|
||||
// on `isItemInActiveQueue(meta)` (which delegates to `playQueueItemIdFor`)
|
||||
// so a Jellyfin playlist queue survives entry into the player. If this
|
||||
// returns null for queue members, the player wipes the launcher-set
|
||||
// queue and prev/next walks the show instead of the playlist.
|
||||
final p = PlaybackStateProvider();
|
||||
addTearDown(p.dispose);
|
||||
|
||||
@@ -247,6 +247,47 @@ void main() {
|
||||
expect(p.playQueueItemIdFor(ep1), 0);
|
||||
expect(p.playQueueItemIdFor(ep2), 1);
|
||||
expect(p.playQueueItemIdFor(outsider), isNull);
|
||||
expect(p.isItemInActiveQueue(ep1), isTrue);
|
||||
expect(p.isItemInActiveQueue(outsider), isFalse);
|
||||
});
|
||||
|
||||
test('isItemInActiveQueue keeps Plex playlist/collection queues alive', () async {
|
||||
// Anchor (Plex side): `_ensurePlayQueue` in episode_queue.dart gates
|
||||
// its "preserve vs. clobber" decision on `isItemInActiveQueue`. A
|
||||
// Plex playlist queue's contextKey is the playlist id (not the show),
|
||||
// so a context-key-only check would wipe it. Membership via the
|
||||
// server-stamped `playQueueItemId` is the right signal — see gh #978.
|
||||
final p = PlaybackStateProvider();
|
||||
addTearDown(p.dispose);
|
||||
|
||||
final inQueue = _item('ep-in-playlist', 5001);
|
||||
// A real-world non-queue item (e.g. tapped from media detail) carries
|
||||
// no `playQueueItemId` — that's how the helper distinguishes it from
|
||||
// a launcher-seeded queue member.
|
||||
final outsider = PlexMediaItem(id: 'ep-different-show', kind: MediaKind.episode);
|
||||
|
||||
await p.setPlaybackFromPlayQueue(
|
||||
_queue(
|
||||
playQueueID: 77,
|
||||
selectedItemID: 5001,
|
||||
totalCount: 2,
|
||||
items: [inQueue, _item('ep-other-in-playlist', 5002)],
|
||||
),
|
||||
// contextKey is the playlist id, deliberately != grandparentId of any item
|
||||
'playlist-Z',
|
||||
);
|
||||
|
||||
expect(p.isItemInActiveQueue(inQueue), isTrue);
|
||||
expect(p.isItemInActiveQueue(outsider), isFalse);
|
||||
});
|
||||
|
||||
test('isItemInActiveQueue is false when no queue is active', () {
|
||||
final p = PlaybackStateProvider();
|
||||
addTearDown(p.dispose);
|
||||
|
||||
final ep = _item('ep1', 1);
|
||||
expect(p.isQueueActive, isFalse);
|
||||
expect(p.isItemInActiveQueue(ep), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user