mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
fix(player): keep shuffle order stable and restore the original queue
This commit is contained in:
@@ -269,6 +269,128 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'shuffle plays the published queue and off restores the original order',
|
||||
() async {
|
||||
final tracks = [
|
||||
for (var i = 0; i < 8; i++)
|
||||
PlayableMedia(
|
||||
id: '$i',
|
||||
source: '/$i.flac',
|
||||
title: '$i',
|
||||
artist: 'Artist',
|
||||
),
|
||||
];
|
||||
await handler.setQueueAndPlay(tracks, initialIndex: 3);
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
final planned = handler.queue.value.map((item) => item.id).toList();
|
||||
expect(planned.first, '3');
|
||||
expect(planned.toSet(), tracks.map((item) => item.id).toSet());
|
||||
for (var i = 1; i < planned.length; i++) {
|
||||
await handler.skipToNext();
|
||||
expect(handler.mediaItem.value?.id, planned[i]);
|
||||
expect(native.sources['music-player'], '/${planned[i]}.flac');
|
||||
expect(handler.queue.value.map((item) => item.id), planned);
|
||||
}
|
||||
final last = handler.mediaItem.value;
|
||||
final resumes = native.resumedSources.length;
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.none);
|
||||
expect(
|
||||
handler.queue.value.map((item) => item.id),
|
||||
tracks.map((item) => item.id),
|
||||
);
|
||||
expect(handler.mediaItem.value, last);
|
||||
expect(handler.playbackState.value.queueIndex, int.parse(planned.last));
|
||||
expect(native.resumedSources.length, resumes);
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
expect(handler.queue.value.first.id, planned.last);
|
||||
expect(handler.mediaItem.value, last);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'automatic completion follows shuffle order and respects repeat off',
|
||||
() async {
|
||||
await handler.setQueueAndPlay(_tracks);
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
final planned = handler.queue.value.map((item) => item.id).toList();
|
||||
for (var i = 1; i < planned.length; i++) {
|
||||
await native.event('music-player', 'audio.onComplete');
|
||||
await _until(
|
||||
() =>
|
||||
handler.mediaItem.value?.id == planned[i] &&
|
||||
handler.playbackState.value.processingState ==
|
||||
AudioProcessingState.ready,
|
||||
);
|
||||
}
|
||||
await native.event('music-player', 'audio.onComplete');
|
||||
await _until(
|
||||
() =>
|
||||
handler.playbackState.value.processingState ==
|
||||
AudioProcessingState.completed,
|
||||
);
|
||||
expect(handler.mediaItem.value?.id, planned.last);
|
||||
expect(handler.queue.value.map((item) => item.id), planned);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'shuffle restoration retains duplicate entries and explicit queue edits',
|
||||
() async {
|
||||
await handler.setQueueAndPlay([
|
||||
_tracks[0],
|
||||
_tracks[0],
|
||||
_tracks[1],
|
||||
_tracks[2],
|
||||
]);
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
await handler.enqueue(
|
||||
const PlayableMedia(
|
||||
id: 'next',
|
||||
source: '/next.flac',
|
||||
title: 'Next',
|
||||
artist: '',
|
||||
),
|
||||
playNext: true,
|
||||
);
|
||||
await handler.enqueueAll([
|
||||
const PlayableMedia(
|
||||
id: 'last',
|
||||
source: '/last.flac',
|
||||
title: 'Last',
|
||||
artist: '',
|
||||
),
|
||||
]);
|
||||
expect(handler.queue.value[1].id, 'next');
|
||||
await handler.onSourceDeleted('/two.flac');
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.none);
|
||||
expect(handler.queue.value.map((item) => item.id), [
|
||||
'one',
|
||||
'next',
|
||||
'one',
|
||||
'three',
|
||||
'last',
|
||||
]);
|
||||
expect(handler.playbackState.value.queueIndex, 0);
|
||||
},
|
||||
);
|
||||
|
||||
test('restored shuffle can return to the saved original order', () async {
|
||||
await handler.restoreSession(
|
||||
items: [_tracks[1], _tracks[2], _tracks[0]],
|
||||
index: 1,
|
||||
position: const Duration(seconds: 12),
|
||||
shuffle: true,
|
||||
originalOrder: [1, 2, 0],
|
||||
);
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.none);
|
||||
expect(handler.queue.value.map((item) => item.id), ['one', 'two', 'three']);
|
||||
expect(handler.mediaItem.value?.id, 'three');
|
||||
expect(handler.playbackState.value.queueIndex, 2);
|
||||
expect(handler.playbackState.value.position.inSeconds, 12);
|
||||
expect(native.resumedSources, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'notification favorite retains clicked track and ignores double taps',
|
||||
() async {
|
||||
@@ -311,6 +433,21 @@ void main() {
|
||||
return incoming;
|
||||
}
|
||||
|
||||
test(
|
||||
'AutoMix prepares and plays the next entry in the shuffled queue',
|
||||
() async {
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
await prepare();
|
||||
final planned = handler.queue.value.map((item) => item.id).toList();
|
||||
final incoming = native.prepared;
|
||||
expect(native.sources[incoming], '/${planned[1]}.flac');
|
||||
native.positions['music-player'] = 55000;
|
||||
await _until(() => handler.mediaItem.value?.id == planned[1]);
|
||||
expect(handler.queue.value.map((item) => item.id), planned);
|
||||
expect(handler.playbackState.value.queueIndex, 1);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'disabled AutoMix uses only the ordinary player and no analysis',
|
||||
() async {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/services/music_player_service.dart';
|
||||
|
||||
@@ -164,25 +166,48 @@ void main() {
|
||||
expect(metadata['title'], 'Instrumental');
|
||||
});
|
||||
|
||||
test('shuffle candidate selection excludes recent tracks', () {
|
||||
test('shuffle plans the whole queue once with the current song first', () {
|
||||
final order = buildShuffledQueueOrder(
|
||||
mediaCount: 12,
|
||||
currentIndex: 4,
|
||||
random: Random(42),
|
||||
);
|
||||
expect(order.first, 4);
|
||||
expect(order.toSet(), {for (var i = 0; i < 12; i++) i});
|
||||
expect(order.length, 12);
|
||||
expect(
|
||||
buildShuffleCandidatePool(
|
||||
mediaCount: 6,
|
||||
currentIndex: 2,
|
||||
recentIndices: const [0, 1, 3],
|
||||
),
|
||||
[4, 5],
|
||||
order.skip(1),
|
||||
isNot(orderedEquals([0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11])),
|
||||
);
|
||||
});
|
||||
|
||||
test('shuffle candidate selection resets after exhausting the pool', () {
|
||||
expect(
|
||||
buildShuffleCandidatePool(
|
||||
mediaCount: 4,
|
||||
currentIndex: 2,
|
||||
recentIndices: const [0, 1, 3],
|
||||
),
|
||||
[0, 1, 3],
|
||||
);
|
||||
});
|
||||
test(
|
||||
'new shuffle draws a new permutation and handles empty/single queues',
|
||||
() {
|
||||
final random = Random(42);
|
||||
final first = buildShuffledQueueOrder(
|
||||
mediaCount: 12,
|
||||
currentIndex: 4,
|
||||
random: random,
|
||||
);
|
||||
final second = buildShuffledQueueOrder(
|
||||
mediaCount: 12,
|
||||
currentIndex: 4,
|
||||
random: random,
|
||||
);
|
||||
expect(first, isNot(orderedEquals(second)));
|
||||
expect(
|
||||
buildShuffledQueueOrder(
|
||||
mediaCount: 0,
|
||||
currentIndex: -1,
|
||||
random: random,
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
expect(
|
||||
buildShuffledQueueOrder(mediaCount: 1, currentIndex: 0, random: random),
|
||||
[0],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user