mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor(playlist_queue): add playlist 3 items load first support
This commit is contained in:
parent
f1080e1675
commit
3ba3df7265
@ -31,7 +31,8 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
|
||||
if (audioPlayer.hasSource && !await audioPlayer.isCompleted) {
|
||||
await playlistNotifier.resume();
|
||||
} else {
|
||||
await playlistNotifier.play();
|
||||
// TODO: Implement play on start
|
||||
// await playlistNotifier.play();
|
||||
}
|
||||
} else {
|
||||
await playlistNotifier.pause();
|
||||
|
@ -11,6 +11,7 @@ import 'package:spotube/hooks/use_progress.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
class PlayerControls extends HookConsumerWidget {
|
||||
@ -186,20 +187,20 @@ class PlayerControls extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: playlist?.isShuffled == true
|
||||
tooltip: playlist?.shuffled == true
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
style: playlist?.isShuffled == true
|
||||
style: playlist?.shuffled == true
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
onPressed: playlist == null || playlist.isLoading
|
||||
? null
|
||||
: () {
|
||||
if (playlist.isShuffled == true) {
|
||||
playlistNotifier.unshuffle();
|
||||
if (playlist.shuffled == true) {
|
||||
playlistNotifier.setShuffle(false);
|
||||
} else {
|
||||
playlistNotifier.shuffle();
|
||||
playlistNotifier.setShuffle(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -240,24 +241,26 @@ class PlayerControls extends HookConsumerWidget {
|
||||
onPressed: playlistNotifier.next,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: playlist?.isLooping != true
|
||||
tooltip: playlist?.loopMode == PlaybackLoopMode.one
|
||||
? context.l10n.loop_track
|
||||
: context.l10n.repeat_playlist,
|
||||
icon: Icon(
|
||||
playlist?.isLooping == true
|
||||
playlist?.loopMode == PlaybackLoopMode.one
|
||||
? SpotubeIcons.repeatOne
|
||||
: SpotubeIcons.repeat,
|
||||
),
|
||||
style: playlist?.isLooping == true
|
||||
style: playlist?.loopMode == PlaybackLoopMode.one
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
onPressed: playlist == null || playlist.isLoading
|
||||
? null
|
||||
: () {
|
||||
if (playlist.isLooping == true) {
|
||||
playlistNotifier.unloop();
|
||||
if (playlist.loopMode == PlaybackLoopMode.one) {
|
||||
playlistNotifier
|
||||
.setLoopMode(PlaybackLoopMode.all);
|
||||
} else {
|
||||
playlistNotifier.loop();
|
||||
playlistNotifier
|
||||
.setLoopMode(PlaybackLoopMode.one);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -28,11 +28,10 @@ class AlbumPage extends HookConsumerWidget {
|
||||
currentTrack ??= sortedTracks.first;
|
||||
final isPlaylistPlaying = playback.isPlayingPlaylist(tracks);
|
||||
if (!isPlaylistPlaying) {
|
||||
playback.load(
|
||||
await playback.loadAndPlay(
|
||||
sortedTracks,
|
||||
active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
);
|
||||
await playback.play();
|
||||
} else if (isPlaylistPlaying &&
|
||||
currentTrack.id != null &&
|
||||
currentTrack.id != playlist?.activeTrack.id) {
|
||||
|
@ -12,6 +12,7 @@ import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/palette_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
@ -20,16 +21,18 @@ import 'package:collection/collection.dart';
|
||||
|
||||
class PlaylistQueue {
|
||||
final Set<Track> tracks;
|
||||
final Set<Track> tempTracks;
|
||||
final bool loop;
|
||||
final int active;
|
||||
|
||||
Track get activeTrack => tracks.elementAt(active);
|
||||
|
||||
final bool shuffled;
|
||||
final PlaybackLoopMode loopMode;
|
||||
|
||||
static Future<PlaylistQueue> fromJson(
|
||||
Map<String, dynamic> json, UserPreferences preferences) async {
|
||||
Map<String, dynamic> json,
|
||||
UserPreferences preferences,
|
||||
) async {
|
||||
final List? tracks = json['tracks'];
|
||||
final List? tempTracks = json['tempTracks'];
|
||||
return PlaylistQueue(
|
||||
Set.from(
|
||||
await Future.wait(
|
||||
@ -54,28 +57,8 @@ class PlaylistQueue {
|
||||
),
|
||||
),
|
||||
active: json['active'],
|
||||
tempTracks: Set.from(
|
||||
await Future.wait(
|
||||
tempTracks?.mapIndexed(
|
||||
(i, e) async {
|
||||
final jsonTrack =
|
||||
Map.castFrom<dynamic, dynamic, String, dynamic>(e);
|
||||
|
||||
if (e["path"] != null) {
|
||||
return LocalTrack.fromJson(jsonTrack);
|
||||
} else if (i == json["active"] && !json.containsKey("path")) {
|
||||
return await SpotubeTrack.fetchFromTrack(
|
||||
Track.fromJson(jsonTrack),
|
||||
preferences,
|
||||
);
|
||||
} else {
|
||||
return Track.fromJson(jsonTrack);
|
||||
}
|
||||
},
|
||||
) ??
|
||||
[],
|
||||
),
|
||||
),
|
||||
shuffled: json['shuffled'],
|
||||
loopMode: PlaybackLoopMode.fromString(json['loopMode'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
@ -93,43 +76,32 @@ class PlaylistQueue {
|
||||
},
|
||||
).toList(),
|
||||
'active': active,
|
||||
'tempTracks': tempTracks.map(
|
||||
(e) {
|
||||
if (e is SpotubeTrack) {
|
||||
return e.toJson();
|
||||
} else if (e is LocalTrack) {
|
||||
return e.toJson();
|
||||
} else {
|
||||
return e.toJson();
|
||||
}
|
||||
},
|
||||
).toList(),
|
||||
'shuffled': shuffled,
|
||||
'loopMode': loopMode.name,
|
||||
};
|
||||
}
|
||||
|
||||
bool get isLoading =>
|
||||
activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack;
|
||||
bool get isShuffled => tempTracks.isNotEmpty;
|
||||
bool get isLooping => loop;
|
||||
|
||||
PlaylistQueue(
|
||||
this.tracks, {
|
||||
required this.tempTracks,
|
||||
this.active = 0,
|
||||
this.loop = false,
|
||||
this.shuffled = false,
|
||||
this.loopMode = PlaybackLoopMode.none,
|
||||
}) : assert(active < tracks.length && active >= 0, "Invalid active index");
|
||||
|
||||
PlaylistQueue copyWith({
|
||||
Set<Track>? tracks,
|
||||
Set<Track>? tempTracks,
|
||||
int? active,
|
||||
bool? loop,
|
||||
bool? shuffled,
|
||||
PlaybackLoopMode? loopMode,
|
||||
}) {
|
||||
return PlaylistQueue(
|
||||
tracks ?? this.tracks,
|
||||
active: active ?? this.active,
|
||||
tempTracks: tempTracks ?? this.tempTracks,
|
||||
loop: loop ?? this.loop,
|
||||
shuffled: shuffled ?? this.shuffled,
|
||||
loopMode: loopMode ?? this.loopMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -153,13 +125,67 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
void configure() async {
|
||||
audioServices = await AudioServices.create(ref, this);
|
||||
|
||||
audioPlayer.completedStream.listen((event) async {
|
||||
audioPlayer.currentIndexChangedStream.listen((index) async {
|
||||
if (!isLoaded) return;
|
||||
if (state!.isLooping) {
|
||||
await audioPlayer.seek(Duration.zero);
|
||||
await audioPlayer.resume();
|
||||
} else {
|
||||
await next();
|
||||
state = state!.copyWith(active: index);
|
||||
await audioServices.addTrack(state!.activeTrack);
|
||||
});
|
||||
|
||||
audioPlayer.almostCompleteStream.listen((_) async {
|
||||
if (!isLoaded) return;
|
||||
final nextTrack = state!.tracks.elementAtOrNull(state!.active + 1);
|
||||
final sources = audioPlayer.sources;
|
||||
|
||||
// we don't have a next track or the next track is already loaded
|
||||
// only when the next track isn't loaded we load next 3 tracks
|
||||
if (nextTrack == null ||
|
||||
nextTrack is SpotubeTrack && sources.contains(nextTrack.ytUri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<SpotubeTrack> fetchedTracks = [];
|
||||
|
||||
// load next 3 tracks
|
||||
final tracks = await Future.wait(state!.tracks
|
||||
.toList()
|
||||
.skip(state!.active + 1)
|
||||
.take(3)
|
||||
.mapIndexed((i, track) async {
|
||||
if (track is LocalTrack) return Future.value(track.path);
|
||||
if (track is SpotubeTrack) return Future.value(track.ytUri);
|
||||
if (i == 0) {
|
||||
final fetchedTrack =
|
||||
await SpotubeTrack.fetchFromTrack(track, preferences);
|
||||
fetchedTracks.add(fetchedTrack);
|
||||
return fetchedTrack.ytUri;
|
||||
}
|
||||
// Adding delay to not spoof the YouTube API for IP Block
|
||||
final fetchedTrack = await Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => SpotubeTrack.fetchFromTrack(track, preferences),
|
||||
);
|
||||
|
||||
fetchedTracks.add(fetchedTrack);
|
||||
return fetchedTrack.ytUri;
|
||||
}));
|
||||
|
||||
// replacing the tracks with the fetched tracks
|
||||
// in proxy playlist
|
||||
state = state!.copyWith(
|
||||
tracks: state!.tracks.map((track) {
|
||||
final fetchedTrack =
|
||||
fetchedTracks.firstWhereOrNull((e) => e.id == track.id);
|
||||
|
||||
if (fetchedTrack != null) {
|
||||
return fetchedTrack;
|
||||
}
|
||||
return track;
|
||||
}).toSet(),
|
||||
);
|
||||
|
||||
for (final track in tracks) {
|
||||
if (sources.contains(track)) continue;
|
||||
await audioPlayer.addTrack(track);
|
||||
}
|
||||
});
|
||||
|
||||
@ -241,6 +267,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Removal of track support
|
||||
void remove(List<Track> tracks) {
|
||||
if (!isLoaded) return;
|
||||
final trackIds = tracks.map((e) => e.id!).toSet();
|
||||
@ -260,50 +287,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
: state!.active
|
||||
: null,
|
||||
);
|
||||
if (state!.isLoading) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
void shuffle() {
|
||||
if (!isLoaded || state!.isShuffled) return;
|
||||
state = state?.copyWith(
|
||||
tempTracks: state!.tracks,
|
||||
tracks: {
|
||||
state!.activeTrack,
|
||||
...state!.tracks.toList()
|
||||
..removeAt(state!.active)
|
||||
..shuffle()
|
||||
},
|
||||
active: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void unshuffle() {
|
||||
if (!isLoaded || !state!.isShuffled) return;
|
||||
state = state?.copyWith(
|
||||
tracks: state!.tempTracks,
|
||||
active: state!.tempTracks
|
||||
.toList()
|
||||
.indexWhere((element) => element.id == state!.activeTrack.id),
|
||||
tempTracks: {},
|
||||
);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!isLoaded || state!.isLooping) return;
|
||||
state = state?.copyWith(
|
||||
loop: true,
|
||||
);
|
||||
}
|
||||
|
||||
void unloop() {
|
||||
if (!isLoaded || !state!.isLooping) return;
|
||||
state = state?.copyWith(
|
||||
loop: false,
|
||||
);
|
||||
// if (state!.isLoading) {
|
||||
// play();
|
||||
// }
|
||||
}
|
||||
|
||||
// TODO: Swap sibling support
|
||||
Future<void> swapSibling(Video video) async {
|
||||
if (!isLoaded || state!.isLoading) return;
|
||||
await pause();
|
||||
@ -314,7 +303,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
tracks[state!.active] = track;
|
||||
|
||||
state = state!.copyWith(tracks: Set.from(tracks));
|
||||
await play();
|
||||
// await play();
|
||||
}
|
||||
|
||||
Future<void> populateSibling() async {
|
||||
@ -325,66 +314,92 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
state = state!.copyWith(tracks: Set.from(tracks));
|
||||
}
|
||||
|
||||
Future<void> play() async {
|
||||
if (!isLoaded) return;
|
||||
await pause();
|
||||
await audioServices.addTrack(state!.activeTrack);
|
||||
if (state!.activeTrack is LocalTrack) {
|
||||
await audioPlayer.play((state!.activeTrack as LocalTrack).path);
|
||||
return;
|
||||
}
|
||||
if (state!.activeTrack is! SpotubeTrack) {
|
||||
final tracks = state!.tracks.toList();
|
||||
tracks[state!.active] = await SpotubeTrack.fetchFromTrack(
|
||||
state!.activeTrack,
|
||||
preferences,
|
||||
);
|
||||
final tempTracks = state!.tempTracks
|
||||
.map((e) =>
|
||||
e.id == tracks[state!.active].id ? tracks[state!.active] : e)
|
||||
.toList();
|
||||
// Future<void> play() async {
|
||||
// if (!isLoaded) return;
|
||||
// await pause();
|
||||
// await audioServices.addTrack(state!.activeTrack);
|
||||
// if (state!.activeTrack is LocalTrack) {
|
||||
// await audioPlayer.play((state!.activeTrack as LocalTrack).path);
|
||||
// return;
|
||||
// }
|
||||
// if (state!.activeTrack is! SpotubeTrack) {
|
||||
// final tracks = state!.tracks.toList();
|
||||
// tracks[state!.active] = await SpotubeTrack.fetchFromTrack(
|
||||
// state!.activeTrack,
|
||||
// preferences,
|
||||
// );
|
||||
|
||||
state = state!.copyWith(
|
||||
tracks: Set.from(tracks),
|
||||
tempTracks: Set.from(tempTracks),
|
||||
);
|
||||
}
|
||||
// state = state!.copyWith(tracks: Set.from(tracks));
|
||||
// }
|
||||
|
||||
await audioServices.addTrack(state!.activeTrack);
|
||||
// await audioServices.addTrack(state!.activeTrack);
|
||||
|
||||
final cached =
|
||||
await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
||||
if (preferences.predownload && cached != null) {
|
||||
await audioPlayer.play(cached.file.path);
|
||||
} else {
|
||||
await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri);
|
||||
}
|
||||
}
|
||||
// final cached =
|
||||
// await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
||||
// if (preferences.predownload && cached != null) {
|
||||
// await audioPlayer.play(cached.file.path);
|
||||
// } else {
|
||||
// await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri);
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: Implement Playtrack
|
||||
Future<void> playTrack(Track track) async {
|
||||
if (!isLoaded) return;
|
||||
final active =
|
||||
state!.tracks.toList().indexWhere((element) => element.id == track.id);
|
||||
if (active == -1) return;
|
||||
state = state!.copyWith(active: active);
|
||||
return play();
|
||||
}
|
||||
|
||||
void load(Iterable<Track> tracks, {int active = 0}) {
|
||||
Future<void> load(Iterable<Track> tracks, {int active = 0}) async {
|
||||
final activeTrack = tracks.elementAt(active);
|
||||
final filtered = Set.from(blacklist.filter(tracks));
|
||||
state = PlaylistQueue(
|
||||
Set.from(blacklist.filter(tracks)),
|
||||
tempTracks: {},
|
||||
active: filtered
|
||||
.toList()
|
||||
.indexWhere((element) => element.id == activeTrack.id),
|
||||
);
|
||||
|
||||
// load 3 items first to avoid huge initial loading time
|
||||
final firstTracks = await Future.wait(
|
||||
filtered
|
||||
.skip(active == 0 ? 0 : active - 1)
|
||||
.take(3)
|
||||
.mapIndexed((i, track) {
|
||||
if (track is LocalTrack) return Future.value(track.path);
|
||||
if (i == 0) {
|
||||
return SpotubeTrack.fetchFromTrack(track, preferences).then(
|
||||
(s) => s.ytUri,
|
||||
);
|
||||
}
|
||||
// Adding delay to not spoof the YouTube API for IP Block
|
||||
return Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => SpotubeTrack.fetchFromTrack(track, preferences).then(
|
||||
(s) => s.ytUri,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
final localTracks = tracks
|
||||
.where(
|
||||
(element) =>
|
||||
element is LocalTrack && !firstTracks.contains(element.path),
|
||||
)
|
||||
.map((e) => (e as LocalTrack).path);
|
||||
|
||||
await audioPlayer.openPlaylist(
|
||||
[...firstTracks, ...localTracks],
|
||||
autoPlay: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadAndPlay(Iterable<Track> tracks, {int active = 0}) async {
|
||||
load(tracks, active: active);
|
||||
await play();
|
||||
await load(tracks, active: active);
|
||||
await resume();
|
||||
}
|
||||
|
||||
Future<void> pause() {
|
||||
@ -403,31 +418,11 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
}
|
||||
|
||||
Future<void> next() async {
|
||||
if (!isLoaded) return;
|
||||
if (state!.active == state!.tracks.length - 1) {
|
||||
state = state!.copyWith(
|
||||
active: 0,
|
||||
);
|
||||
} else {
|
||||
state = state!.copyWith(
|
||||
active: state!.active + 1,
|
||||
);
|
||||
}
|
||||
return play();
|
||||
return audioPlayer.skipToNext();
|
||||
}
|
||||
|
||||
Future<void> previous() async {
|
||||
if (!isLoaded) return;
|
||||
if (state!.active == 0) {
|
||||
state = state!.copyWith(
|
||||
active: state!.tracks.length - 1,
|
||||
);
|
||||
} else {
|
||||
state = state!.copyWith(
|
||||
active: state!.active - 1,
|
||||
);
|
||||
}
|
||||
return play();
|
||||
return audioPlayer.skipToPrevious();
|
||||
}
|
||||
|
||||
Future<void> seek(Duration position) async {
|
||||
@ -436,12 +431,23 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
await resume();
|
||||
}
|
||||
|
||||
Future<void> setShuffle(bool shuffle) async {
|
||||
if (!isLoaded) return;
|
||||
audioPlayer.setShuffle(shuffle);
|
||||
state = state!.copyWith(shuffled: await audioPlayer.isShuffled());
|
||||
}
|
||||
|
||||
Future<void> setLoopMode(PlaybackLoopMode loopMode) async {
|
||||
if (!isLoaded) return;
|
||||
audioPlayer.setLoopMode(loopMode);
|
||||
state = state!.copyWith(loopMode: loopMode);
|
||||
}
|
||||
|
||||
// utility
|
||||
bool isPlayingPlaylist(Iterable<TrackSimple> playlist) {
|
||||
if (!isLoaded || playlist.isEmpty) return false;
|
||||
|
||||
final trackIds = (state!.isShuffled ? state!.tempTracks : state!.tracks)
|
||||
.map((track) => track.id!);
|
||||
final trackIds = state!.tracks.map((track) => track.id!);
|
||||
return blacklist
|
||||
.filter(playlist)
|
||||
.every((track) => trackIds.contains(track.id!));
|
||||
@ -449,10 +455,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
||||
|
||||
bool isTrackOnQueue(TrackSimple track) {
|
||||
if (!isLoaded) return false;
|
||||
if (state!.isShuffled) {
|
||||
final trackIds = state!.tempTracks.map((track) => track.id!);
|
||||
return trackIds.contains(track.id!);
|
||||
}
|
||||
final trackIds = state!.tracks.map((track) => track.id!);
|
||||
return trackIds.contains(track.id!);
|
||||
}
|
||||
|
@ -107,6 +107,22 @@ class SpotubeAudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
Stream<int> get currentIndexChangedStream {
|
||||
if (mkSupportedPlatform) {
|
||||
return _mkPlayer!.streams.playlist
|
||||
.map((event) => event.index)
|
||||
.asBroadcastStream();
|
||||
} else {
|
||||
return _justAudio!.positionDiscontinuityStream
|
||||
.where(
|
||||
(event) =>
|
||||
event.reason == ja.PositionDiscontinuityReason.autoAdvance,
|
||||
)
|
||||
.map((event) => currentIndex)
|
||||
.asBroadcastStream();
|
||||
}
|
||||
}
|
||||
|
||||
// regular info getter
|
||||
|
||||
Future<Duration?> get duration async {
|
||||
@ -301,22 +317,24 @@ class SpotubeAudioPlayer {
|
||||
}
|
||||
|
||||
List<SpotubeTrack> resolveTracksForSource(List<SpotubeTrack> tracks) {
|
||||
if (mkSupportedPlatform) {
|
||||
final urls = _mkPlayer!.state.playlist.medias.map((e) => e.uri).toList();
|
||||
return tracks.where((e) => urls.contains(e.ytUri)).toList();
|
||||
} else {
|
||||
final urls = (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
||||
.children
|
||||
.map((e) => (e as ja.UriAudioSource).uri.toString())
|
||||
.toList();
|
||||
return tracks.where((e) => urls.contains(e.ytUri)).toList();
|
||||
}
|
||||
return tracks.where((e) => sources.contains(e.ytUri)).toList();
|
||||
}
|
||||
|
||||
bool tracksExistsInPlaylist(List<SpotubeTrack> tracks) {
|
||||
return resolveTracksForSource(tracks).length == tracks.length;
|
||||
}
|
||||
|
||||
List<String> get sources {
|
||||
if (mkSupportedPlatform) {
|
||||
return _mkPlayer!.state.playlist.medias.map((e) => e.uri).toList();
|
||||
} else {
|
||||
return (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
||||
.children
|
||||
.map((e) => (e as ja.UriAudioSource).uri.toString())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
int get currentIndex {
|
||||
if (mkSupportedPlatform) {
|
||||
return _mkPlayer!.state.playlist.index;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:mpris_service/mpris_service.dart';
|
||||
|
||||
/// An unified loop mode for both [LoopMode] and [PlaylistMode]
|
||||
enum PlaybackLoopMode {
|
||||
@ -50,4 +52,63 @@ enum PlaybackLoopMode {
|
||||
return PlaylistMode.none;
|
||||
}
|
||||
}
|
||||
|
||||
static PlaybackLoopMode fromMPRISLoopStatus(MPRISLoopStatus status) {
|
||||
switch (status) {
|
||||
case MPRISLoopStatus.none:
|
||||
return PlaybackLoopMode.none;
|
||||
case MPRISLoopStatus.track:
|
||||
return PlaybackLoopMode.one;
|
||||
case MPRISLoopStatus.playlist:
|
||||
return PlaybackLoopMode.all;
|
||||
}
|
||||
}
|
||||
|
||||
MPRISLoopStatus toMPRISLoopStatus() {
|
||||
switch (this) {
|
||||
case PlaybackLoopMode.all:
|
||||
return MPRISLoopStatus.playlist;
|
||||
case PlaybackLoopMode.one:
|
||||
return MPRISLoopStatus.track;
|
||||
case PlaybackLoopMode.none:
|
||||
return MPRISLoopStatus.none;
|
||||
}
|
||||
}
|
||||
|
||||
static PlaybackLoopMode fromAudioServiceRepeatMode(
|
||||
AudioServiceRepeatMode mode) {
|
||||
switch (mode) {
|
||||
case AudioServiceRepeatMode.all:
|
||||
case AudioServiceRepeatMode.group:
|
||||
return PlaybackLoopMode.all;
|
||||
case AudioServiceRepeatMode.one:
|
||||
return PlaybackLoopMode.one;
|
||||
case AudioServiceRepeatMode.none:
|
||||
return PlaybackLoopMode.none;
|
||||
}
|
||||
}
|
||||
|
||||
AudioServiceRepeatMode toAudioServiceRepeatMode() {
|
||||
switch (this) {
|
||||
case PlaybackLoopMode.all:
|
||||
return AudioServiceRepeatMode.all;
|
||||
case PlaybackLoopMode.one:
|
||||
return AudioServiceRepeatMode.one;
|
||||
case PlaybackLoopMode.none:
|
||||
return AudioServiceRepeatMode.none;
|
||||
}
|
||||
}
|
||||
|
||||
static PlaybackLoopMode fromString(String? value) {
|
||||
switch (value) {
|
||||
case 'all':
|
||||
return PlaybackLoopMode.all;
|
||||
case 'one':
|
||||
return PlaybackLoopMode.one;
|
||||
case 'none':
|
||||
return PlaybackLoopMode.none;
|
||||
default:
|
||||
return PlaybackLoopMode.none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
@ -29,11 +30,9 @@ class LinuxAudioService {
|
||||
mpris.playbackStatus = MPRISPlaybackStatus.stopped;
|
||||
mpris.setEventHandler(MPRISEventHandler(
|
||||
loopStatus: (value) async {
|
||||
if (value == MPRISLoopStatus.none) {
|
||||
playlistNotifier.unloop();
|
||||
} else if (value == MPRISLoopStatus.track) {
|
||||
playlistNotifier.loop();
|
||||
}
|
||||
playlistNotifier.setLoopMode(
|
||||
PlaybackLoopMode.fromMPRISLoopStatus(value),
|
||||
);
|
||||
},
|
||||
next: playlistNotifier.next,
|
||||
pause: playlistNotifier.pause,
|
||||
@ -46,13 +45,7 @@ class LinuxAudioService {
|
||||
}
|
||||
},
|
||||
seek: playlistNotifier.seek,
|
||||
shuffle: (value) async {
|
||||
if (value) {
|
||||
playlistNotifier.shuffle();
|
||||
} else {
|
||||
playlistNotifier.unshuffle();
|
||||
}
|
||||
},
|
||||
shuffle: playlistNotifier.setShuffle,
|
||||
stop: playlistNotifier.stop,
|
||||
volume: (value) async {
|
||||
await ref.read(VolumeProvider.provider.notifier).setVolume(value);
|
||||
|
@ -4,6 +4,7 @@ import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
|
||||
class MobileAudioService extends BaseAudioHandler {
|
||||
AudioSession? session;
|
||||
@ -58,21 +59,15 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||
await super.setShuffleMode(shuffleMode);
|
||||
|
||||
if (shuffleMode == AudioServiceShuffleMode.all) {
|
||||
playlistNotifier.shuffle();
|
||||
} else {
|
||||
playlistNotifier.unshuffle();
|
||||
}
|
||||
playlistNotifier.setShuffle(shuffleMode == AudioServiceShuffleMode.all);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||
super.setRepeatMode(repeatMode);
|
||||
if (repeatMode == AudioServiceRepeatMode.all) {
|
||||
playlistNotifier.loop();
|
||||
} else {
|
||||
playlistNotifier.unloop();
|
||||
}
|
||||
playlistNotifier.setLoopMode(
|
||||
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -114,12 +109,11 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: position,
|
||||
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
||||
shuffleMode: playlist?.isShuffled == true
|
||||
shuffleMode: playlist?.shuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: playlist?.isLooping == true
|
||||
? AudioServiceRepeatMode.one
|
||||
: AudioServiceRepeatMode.all,
|
||||
repeatMode: playlist?.loopMode.toAudioServiceRepeatMode() ??
|
||||
AudioServiceRepeatMode.none,
|
||||
processingState: playlist?.isLoading == true
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
|
Loading…
Reference in New Issue
Block a user