mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05: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) {
|
if (audioPlayer.hasSource && !await audioPlayer.isCompleted) {
|
||||||
await playlistNotifier.resume();
|
await playlistNotifier.resume();
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.play();
|
// TODO: Implement play on start
|
||||||
|
// await playlistNotifier.play();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.pause();
|
await playlistNotifier.pause();
|
||||||
|
@ -11,6 +11,7 @@ import 'package:spotube/hooks/use_progress.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.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';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|
||||||
class PlayerControls extends HookConsumerWidget {
|
class PlayerControls extends HookConsumerWidget {
|
||||||
@ -186,20 +187,20 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: playlist?.isShuffled == true
|
tooltip: playlist?.shuffled == true
|
||||||
? context.l10n.unshuffle_playlist
|
? context.l10n.unshuffle_playlist
|
||||||
: context.l10n.shuffle_playlist,
|
: context.l10n.shuffle_playlist,
|
||||||
icon: const Icon(SpotubeIcons.shuffle),
|
icon: const Icon(SpotubeIcons.shuffle),
|
||||||
style: playlist?.isShuffled == true
|
style: playlist?.shuffled == true
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist == null || playlist.isLoading
|
onPressed: playlist == null || playlist.isLoading
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (playlist.isShuffled == true) {
|
if (playlist.shuffled == true) {
|
||||||
playlistNotifier.unshuffle();
|
playlistNotifier.setShuffle(false);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.shuffle();
|
playlistNotifier.setShuffle(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -240,24 +241,26 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
onPressed: playlistNotifier.next,
|
onPressed: playlistNotifier.next,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: playlist?.isLooping != true
|
tooltip: playlist?.loopMode == PlaybackLoopMode.one
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: context.l10n.repeat_playlist,
|
: context.l10n.repeat_playlist,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
playlist?.isLooping == true
|
playlist?.loopMode == PlaybackLoopMode.one
|
||||||
? SpotubeIcons.repeatOne
|
? SpotubeIcons.repeatOne
|
||||||
: SpotubeIcons.repeat,
|
: SpotubeIcons.repeat,
|
||||||
),
|
),
|
||||||
style: playlist?.isLooping == true
|
style: playlist?.loopMode == PlaybackLoopMode.one
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist == null || playlist.isLoading
|
onPressed: playlist == null || playlist.isLoading
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (playlist.isLooping == true) {
|
if (playlist.loopMode == PlaybackLoopMode.one) {
|
||||||
playlistNotifier.unloop();
|
playlistNotifier
|
||||||
|
.setLoopMode(PlaybackLoopMode.all);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.loop();
|
playlistNotifier
|
||||||
|
.setLoopMode(PlaybackLoopMode.one);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -28,11 +28,10 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
currentTrack ??= sortedTracks.first;
|
currentTrack ??= sortedTracks.first;
|
||||||
final isPlaylistPlaying = playback.isPlayingPlaylist(tracks);
|
final isPlaylistPlaying = playback.isPlayingPlaylist(tracks);
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.load(
|
await playback.loadAndPlay(
|
||||||
sortedTracks,
|
sortedTracks,
|
||||||
active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
await playback.play();
|
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlist?.activeTrack.id) {
|
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/palette_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_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/audio_player.dart';
|
||||||
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -20,16 +21,18 @@ import 'package:collection/collection.dart';
|
|||||||
|
|
||||||
class PlaylistQueue {
|
class PlaylistQueue {
|
||||||
final Set<Track> tracks;
|
final Set<Track> tracks;
|
||||||
final Set<Track> tempTracks;
|
|
||||||
final bool loop;
|
|
||||||
final int active;
|
final int active;
|
||||||
|
|
||||||
Track get activeTrack => tracks.elementAt(active);
|
Track get activeTrack => tracks.elementAt(active);
|
||||||
|
|
||||||
|
final bool shuffled;
|
||||||
|
final PlaybackLoopMode loopMode;
|
||||||
|
|
||||||
static Future<PlaylistQueue> fromJson(
|
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? tracks = json['tracks'];
|
||||||
final List? tempTracks = json['tempTracks'];
|
|
||||||
return PlaylistQueue(
|
return PlaylistQueue(
|
||||||
Set.from(
|
Set.from(
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
@ -54,28 +57,8 @@ class PlaylistQueue {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
active: json['active'],
|
active: json['active'],
|
||||||
tempTracks: Set.from(
|
shuffled: json['shuffled'],
|
||||||
await Future.wait(
|
loopMode: PlaybackLoopMode.fromString(json['loopMode'] ?? ''),
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) ??
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,43 +76,32 @@ class PlaylistQueue {
|
|||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
'active': active,
|
'active': active,
|
||||||
'tempTracks': tempTracks.map(
|
'shuffled': shuffled,
|
||||||
(e) {
|
'loopMode': loopMode.name,
|
||||||
if (e is SpotubeTrack) {
|
|
||||||
return e.toJson();
|
|
||||||
} else if (e is LocalTrack) {
|
|
||||||
return e.toJson();
|
|
||||||
} else {
|
|
||||||
return e.toJson();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isLoading =>
|
bool get isLoading =>
|
||||||
activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack;
|
activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack;
|
||||||
bool get isShuffled => tempTracks.isNotEmpty;
|
|
||||||
bool get isLooping => loop;
|
|
||||||
|
|
||||||
PlaylistQueue(
|
PlaylistQueue(
|
||||||
this.tracks, {
|
this.tracks, {
|
||||||
required this.tempTracks,
|
|
||||||
this.active = 0,
|
this.active = 0,
|
||||||
this.loop = false,
|
this.shuffled = false,
|
||||||
|
this.loopMode = PlaybackLoopMode.none,
|
||||||
}) : assert(active < tracks.length && active >= 0, "Invalid active index");
|
}) : assert(active < tracks.length && active >= 0, "Invalid active index");
|
||||||
|
|
||||||
PlaylistQueue copyWith({
|
PlaylistQueue copyWith({
|
||||||
Set<Track>? tracks,
|
Set<Track>? tracks,
|
||||||
Set<Track>? tempTracks,
|
|
||||||
int? active,
|
int? active,
|
||||||
bool? loop,
|
bool? shuffled,
|
||||||
|
PlaybackLoopMode? loopMode,
|
||||||
}) {
|
}) {
|
||||||
return PlaylistQueue(
|
return PlaylistQueue(
|
||||||
tracks ?? this.tracks,
|
tracks ?? this.tracks,
|
||||||
active: active ?? this.active,
|
active: active ?? this.active,
|
||||||
tempTracks: tempTracks ?? this.tempTracks,
|
shuffled: shuffled ?? this.shuffled,
|
||||||
loop: loop ?? this.loop,
|
loopMode: loopMode ?? this.loopMode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,13 +125,67 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
void configure() async {
|
void configure() async {
|
||||||
audioServices = await AudioServices.create(ref, this);
|
audioServices = await AudioServices.create(ref, this);
|
||||||
|
|
||||||
audioPlayer.completedStream.listen((event) async {
|
audioPlayer.currentIndexChangedStream.listen((index) async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
if (state!.isLooping) {
|
state = state!.copyWith(active: index);
|
||||||
await audioPlayer.seek(Duration.zero);
|
await audioServices.addTrack(state!.activeTrack);
|
||||||
await audioPlayer.resume();
|
});
|
||||||
} else {
|
|
||||||
await next();
|
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) {
|
void remove(List<Track> tracks) {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
final trackIds = tracks.map((e) => e.id!).toSet();
|
final trackIds = tracks.map((e) => e.id!).toSet();
|
||||||
@ -260,50 +287,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
: state!.active
|
: state!.active
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
if (state!.isLoading) {
|
// if (state!.isLoading) {
|
||||||
play();
|
// 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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap sibling support
|
||||||
Future<void> swapSibling(Video video) async {
|
Future<void> swapSibling(Video video) async {
|
||||||
if (!isLoaded || state!.isLoading) return;
|
if (!isLoaded || state!.isLoading) return;
|
||||||
await pause();
|
await pause();
|
||||||
@ -314,7 +303,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
tracks[state!.active] = track;
|
tracks[state!.active] = track;
|
||||||
|
|
||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
state = state!.copyWith(tracks: Set.from(tracks));
|
||||||
await play();
|
// await play();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> populateSibling() async {
|
Future<void> populateSibling() async {
|
||||||
@ -325,66 +314,92 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
state = state!.copyWith(tracks: Set.from(tracks));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> play() async {
|
// Future<void> play() async {
|
||||||
if (!isLoaded) return;
|
// if (!isLoaded) return;
|
||||||
await pause();
|
// await pause();
|
||||||
await audioServices.addTrack(state!.activeTrack);
|
// await audioServices.addTrack(state!.activeTrack);
|
||||||
if (state!.activeTrack is LocalTrack) {
|
// if (state!.activeTrack is LocalTrack) {
|
||||||
await audioPlayer.play((state!.activeTrack as LocalTrack).path);
|
// await audioPlayer.play((state!.activeTrack as LocalTrack).path);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
if (state!.activeTrack is! SpotubeTrack) {
|
// if (state!.activeTrack is! SpotubeTrack) {
|
||||||
final tracks = state!.tracks.toList();
|
// final tracks = state!.tracks.toList();
|
||||||
tracks[state!.active] = await SpotubeTrack.fetchFromTrack(
|
// tracks[state!.active] = await SpotubeTrack.fetchFromTrack(
|
||||||
state!.activeTrack,
|
// state!.activeTrack,
|
||||||
preferences,
|
// preferences,
|
||||||
);
|
// );
|
||||||
final tempTracks = state!.tempTracks
|
|
||||||
.map((e) =>
|
|
||||||
e.id == tracks[state!.active].id ? tracks[state!.active] : e)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
state = state!.copyWith(
|
// state = state!.copyWith(tracks: Set.from(tracks));
|
||||||
tracks: Set.from(tracks),
|
// }
|
||||||
tempTracks: Set.from(tempTracks),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await audioServices.addTrack(state!.activeTrack);
|
// await audioServices.addTrack(state!.activeTrack);
|
||||||
|
|
||||||
final cached =
|
// final cached =
|
||||||
await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
// await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
||||||
if (preferences.predownload && cached != null) {
|
// if (preferences.predownload && cached != null) {
|
||||||
await audioPlayer.play(cached.file.path);
|
// await audioPlayer.play(cached.file.path);
|
||||||
} else {
|
// } else {
|
||||||
await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri);
|
// await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// TODO: Implement Playtrack
|
||||||
Future<void> playTrack(Track track) async {
|
Future<void> playTrack(Track track) async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
final active =
|
final active =
|
||||||
state!.tracks.toList().indexWhere((element) => element.id == track.id);
|
state!.tracks.toList().indexWhere((element) => element.id == track.id);
|
||||||
if (active == -1) return;
|
if (active == -1) return;
|
||||||
state = state!.copyWith(active: active);
|
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 activeTrack = tracks.elementAt(active);
|
||||||
final filtered = Set.from(blacklist.filter(tracks));
|
final filtered = Set.from(blacklist.filter(tracks));
|
||||||
state = PlaylistQueue(
|
state = PlaylistQueue(
|
||||||
Set.from(blacklist.filter(tracks)),
|
Set.from(blacklist.filter(tracks)),
|
||||||
tempTracks: {},
|
|
||||||
active: filtered
|
active: filtered
|
||||||
.toList()
|
.toList()
|
||||||
.indexWhere((element) => element.id == activeTrack.id),
|
.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 {
|
Future<void> loadAndPlay(Iterable<Track> tracks, {int active = 0}) async {
|
||||||
load(tracks, active: active);
|
await load(tracks, active: active);
|
||||||
await play();
|
await resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pause() {
|
Future<void> pause() {
|
||||||
@ -403,31 +418,11 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (!isLoaded) return;
|
return audioPlayer.skipToNext();
|
||||||
if (state!.active == state!.tracks.length - 1) {
|
|
||||||
state = state!.copyWith(
|
|
||||||
active: 0,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
state = state!.copyWith(
|
|
||||||
active: state!.active + 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> previous() async {
|
Future<void> previous() async {
|
||||||
if (!isLoaded) return;
|
return audioPlayer.skipToPrevious();
|
||||||
if (state!.active == 0) {
|
|
||||||
state = state!.copyWith(
|
|
||||||
active: state!.tracks.length - 1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
state = state!.copyWith(
|
|
||||||
active: state!.active - 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> seek(Duration position) async {
|
Future<void> seek(Duration position) async {
|
||||||
@ -436,12 +431,23 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
await resume();
|
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
|
// utility
|
||||||
bool isPlayingPlaylist(Iterable<TrackSimple> playlist) {
|
bool isPlayingPlaylist(Iterable<TrackSimple> playlist) {
|
||||||
if (!isLoaded || playlist.isEmpty) return false;
|
if (!isLoaded || playlist.isEmpty) return false;
|
||||||
|
|
||||||
final trackIds = (state!.isShuffled ? state!.tempTracks : state!.tracks)
|
final trackIds = state!.tracks.map((track) => track.id!);
|
||||||
.map((track) => track.id!);
|
|
||||||
return blacklist
|
return blacklist
|
||||||
.filter(playlist)
|
.filter(playlist)
|
||||||
.every((track) => trackIds.contains(track.id!));
|
.every((track) => trackIds.contains(track.id!));
|
||||||
@ -449,10 +455,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
bool isTrackOnQueue(TrackSimple track) {
|
bool isTrackOnQueue(TrackSimple track) {
|
||||||
if (!isLoaded) return false;
|
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!);
|
final trackIds = state!.tracks.map((track) => track.id!);
|
||||||
return trackIds.contains(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
|
// regular info getter
|
||||||
|
|
||||||
Future<Duration?> get duration async {
|
Future<Duration?> get duration async {
|
||||||
@ -301,22 +317,24 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<SpotubeTrack> resolveTracksForSource(List<SpotubeTrack> tracks) {
|
List<SpotubeTrack> resolveTracksForSource(List<SpotubeTrack> tracks) {
|
||||||
if (mkSupportedPlatform) {
|
return tracks.where((e) => sources.contains(e.ytUri)).toList();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tracksExistsInPlaylist(List<SpotubeTrack> tracks) {
|
bool tracksExistsInPlaylist(List<SpotubeTrack> tracks) {
|
||||||
return resolveTracksForSource(tracks).length == tracks.length;
|
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 {
|
int get currentIndex {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
return _mkPlayer!.state.playlist.index;
|
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:media_kit/media_kit.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
import 'package:mpris_service/mpris_service.dart';
|
||||||
|
|
||||||
/// An unified loop mode for both [LoopMode] and [PlaylistMode]
|
/// An unified loop mode for both [LoopMode] and [PlaylistMode]
|
||||||
enum PlaybackLoopMode {
|
enum PlaybackLoopMode {
|
||||||
@ -50,4 +52,63 @@ enum PlaybackLoopMode {
|
|||||||
return PlaylistMode.none;
|
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/models/spotube_track.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.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/services/audio_player/playback_state.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
@ -29,11 +30,9 @@ class LinuxAudioService {
|
|||||||
mpris.playbackStatus = MPRISPlaybackStatus.stopped;
|
mpris.playbackStatus = MPRISPlaybackStatus.stopped;
|
||||||
mpris.setEventHandler(MPRISEventHandler(
|
mpris.setEventHandler(MPRISEventHandler(
|
||||||
loopStatus: (value) async {
|
loopStatus: (value) async {
|
||||||
if (value == MPRISLoopStatus.none) {
|
playlistNotifier.setLoopMode(
|
||||||
playlistNotifier.unloop();
|
PlaybackLoopMode.fromMPRISLoopStatus(value),
|
||||||
} else if (value == MPRISLoopStatus.track) {
|
);
|
||||||
playlistNotifier.loop();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
next: playlistNotifier.next,
|
next: playlistNotifier.next,
|
||||||
pause: playlistNotifier.pause,
|
pause: playlistNotifier.pause,
|
||||||
@ -46,13 +45,7 @@ class LinuxAudioService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
seek: playlistNotifier.seek,
|
seek: playlistNotifier.seek,
|
||||||
shuffle: (value) async {
|
shuffle: playlistNotifier.setShuffle,
|
||||||
if (value) {
|
|
||||||
playlistNotifier.shuffle();
|
|
||||||
} else {
|
|
||||||
playlistNotifier.unshuffle();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stop: playlistNotifier.stop,
|
stop: playlistNotifier.stop,
|
||||||
volume: (value) async {
|
volume: (value) async {
|
||||||
await ref.read(VolumeProvider.provider.notifier).setVolume(value);
|
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:audio_session/audio_session.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
|
|
||||||
class MobileAudioService extends BaseAudioHandler {
|
class MobileAudioService extends BaseAudioHandler {
|
||||||
AudioSession? session;
|
AudioSession? session;
|
||||||
@ -58,21 +59,15 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||||
await super.setShuffleMode(shuffleMode);
|
await super.setShuffleMode(shuffleMode);
|
||||||
|
|
||||||
if (shuffleMode == AudioServiceShuffleMode.all) {
|
playlistNotifier.setShuffle(shuffleMode == AudioServiceShuffleMode.all);
|
||||||
playlistNotifier.shuffle();
|
|
||||||
} else {
|
|
||||||
playlistNotifier.unshuffle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||||
super.setRepeatMode(repeatMode);
|
super.setRepeatMode(repeatMode);
|
||||||
if (repeatMode == AudioServiceRepeatMode.all) {
|
playlistNotifier.setLoopMode(
|
||||||
playlistNotifier.loop();
|
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
|
||||||
} else {
|
);
|
||||||
playlistNotifier.unloop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -114,12 +109,11 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
playing: audioPlayer.isPlaying,
|
playing: audioPlayer.isPlaying,
|
||||||
updatePosition: position,
|
updatePosition: position,
|
||||||
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
||||||
shuffleMode: playlist?.isShuffled == true
|
shuffleMode: playlist?.shuffled == true
|
||||||
? AudioServiceShuffleMode.all
|
? AudioServiceShuffleMode.all
|
||||||
: AudioServiceShuffleMode.none,
|
: AudioServiceShuffleMode.none,
|
||||||
repeatMode: playlist?.isLooping == true
|
repeatMode: playlist?.loopMode.toAudioServiceRepeatMode() ??
|
||||||
? AudioServiceRepeatMode.one
|
AudioServiceRepeatMode.none,
|
||||||
: AudioServiceRepeatMode.all,
|
|
||||||
processingState: playlist?.isLoading == true
|
processingState: playlist?.isLoading == true
|
||||||
? AudioProcessingState.loading
|
? AudioProcessingState.loading
|
||||||
: AudioProcessingState.ready,
|
: AudioProcessingState.ready,
|
||||||
|
Loading…
Reference in New Issue
Block a user