2024-08-26 18:22:47 +00:00
|
|
|
import 'dart:async';
|
2024-08-26 17:49:05 +00:00
|
|
|
import 'dart:math';
|
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
import 'package:drift/drift.dart';
|
|
|
|
import 'package:get/get.dart' hide Value;
|
2024-08-26 17:49:05 +00:00
|
|
|
import 'package:media_kit/media_kit.dart' hide Track;
|
2024-08-27 15:09:16 +00:00
|
|
|
import 'package:rhythm_box/providers/database.dart';
|
2024-08-26 17:49:05 +00:00
|
|
|
import 'package:rhythm_box/services/audio_player/state.dart';
|
2024-08-27 15:09:16 +00:00
|
|
|
import 'package:rhythm_box/services/database/database.dart';
|
2024-08-26 17:49:05 +00:00
|
|
|
import 'package:spotify/spotify.dart' hide Playlist;
|
|
|
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
|
|
|
|
|
|
|
class AudioPlayerProvider extends GetxController {
|
2024-08-28 16:41:40 +00:00
|
|
|
Rx<Duration> durationTotal = Rx(Duration.zero);
|
|
|
|
Rx<Duration> durationCurrent = Rx(Duration.zero);
|
|
|
|
Rx<Duration> durationBuffered = Rx(Duration.zero);
|
|
|
|
|
2024-08-27 06:35:16 +00:00
|
|
|
RxBool isPlaying = false.obs;
|
|
|
|
|
2024-08-26 17:49:05 +00:00
|
|
|
Rx<AudioPlayerState> state = Rx(AudioPlayerState(
|
|
|
|
playing: false,
|
|
|
|
shuffled: false,
|
|
|
|
loopMode: PlaylistMode.none,
|
|
|
|
playlist: const Playlist([]),
|
|
|
|
collections: [],
|
|
|
|
));
|
|
|
|
|
2024-08-26 18:22:47 +00:00
|
|
|
List<StreamSubscription<Object>>? _subscriptions;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void onInit() {
|
|
|
|
_subscriptions = [
|
|
|
|
audioPlayer.playingStream.listen((playing) async {
|
|
|
|
state.value = state.value.copyWith(playing: playing);
|
2024-08-27 15:09:16 +00:00
|
|
|
await _updatePlayerState(
|
|
|
|
AudioPlayerStateTableCompanion(
|
|
|
|
playing: Value(playing),
|
|
|
|
),
|
|
|
|
);
|
2024-08-26 18:22:47 +00:00
|
|
|
}),
|
|
|
|
audioPlayer.loopModeStream.listen((loopMode) async {
|
|
|
|
state.value = state.value.copyWith(loopMode: loopMode);
|
2024-08-27 15:09:16 +00:00
|
|
|
await _updatePlayerState(
|
|
|
|
AudioPlayerStateTableCompanion(
|
|
|
|
loopMode: Value(loopMode),
|
|
|
|
),
|
|
|
|
);
|
2024-08-26 18:22:47 +00:00
|
|
|
}),
|
|
|
|
audioPlayer.shuffledStream.listen((shuffled) async {
|
|
|
|
state.value = state.value.copyWith(shuffled: shuffled);
|
2024-08-27 15:09:16 +00:00
|
|
|
await _updatePlayerState(
|
|
|
|
AudioPlayerStateTableCompanion(
|
|
|
|
shuffled: Value(shuffled),
|
|
|
|
),
|
|
|
|
);
|
2024-08-26 18:22:47 +00:00
|
|
|
}),
|
|
|
|
audioPlayer.playlistStream.listen((playlist) async {
|
|
|
|
state.value = state.value.copyWith(playlist: playlist);
|
2024-08-27 15:09:16 +00:00
|
|
|
await _updatePlaylist(playlist);
|
2024-08-26 18:22:47 +00:00
|
|
|
}),
|
2024-08-28 16:41:40 +00:00
|
|
|
audioPlayer.durationStream.listen((value) => durationTotal.value = value),
|
|
|
|
audioPlayer.positionStream
|
|
|
|
.listen((value) => durationCurrent.value = value),
|
|
|
|
audioPlayer.bufferedPositionStream
|
|
|
|
.listen((value) => durationBuffered.value = value),
|
2024-08-26 18:22:47 +00:00
|
|
|
];
|
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
_readSavedState();
|
|
|
|
|
2024-08-27 06:35:16 +00:00
|
|
|
audioPlayer.playingStream.listen((playing) {
|
|
|
|
isPlaying.value = playing;
|
|
|
|
});
|
2024-08-26 18:22:47 +00:00
|
|
|
|
|
|
|
super.onInit();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
if (_subscriptions != null) {
|
|
|
|
for (final subscription in _subscriptions!) {
|
|
|
|
subscription.cancel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
super.dispose();
|
2024-08-26 17:49:05 +00:00
|
|
|
}
|
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
Future<void> _readSavedState() async {
|
|
|
|
final database = Get.find<DatabaseProvider>().database;
|
|
|
|
|
|
|
|
var playerState =
|
|
|
|
await database.select(database.audioPlayerStateTable).getSingleOrNull();
|
|
|
|
|
|
|
|
if (playerState == null) {
|
|
|
|
await database.into(database.audioPlayerStateTable).insert(
|
|
|
|
AudioPlayerStateTableCompanion.insert(
|
|
|
|
playing: audioPlayer.isPlaying,
|
|
|
|
loopMode: audioPlayer.loopMode,
|
|
|
|
shuffled: audioPlayer.isShuffled,
|
|
|
|
collections: <String>[],
|
|
|
|
id: const Value(0),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
playerState =
|
|
|
|
await database.select(database.audioPlayerStateTable).getSingle();
|
|
|
|
} else {
|
|
|
|
await audioPlayer.setLoopMode(playerState.loopMode);
|
|
|
|
await audioPlayer.setShuffle(playerState.shuffled);
|
|
|
|
}
|
|
|
|
|
|
|
|
var playlist =
|
|
|
|
await database.select(database.playlistTable).getSingleOrNull();
|
|
|
|
var medias = await database.select(database.playlistMediaTable).get();
|
|
|
|
|
|
|
|
if (playlist == null) {
|
|
|
|
await database.into(database.playlistTable).insert(
|
|
|
|
PlaylistTableCompanion.insert(
|
|
|
|
audioPlayerStateId: 0,
|
|
|
|
index: audioPlayer.playlist.index,
|
|
|
|
id: const Value(0),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
playlist = await database.select(database.playlistTable).getSingle();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (medias.isEmpty && audioPlayer.playlist.medias.isNotEmpty) {
|
|
|
|
await database.batch((batch) {
|
|
|
|
batch.insertAll(
|
|
|
|
database.playlistMediaTable,
|
|
|
|
[
|
|
|
|
for (final media in audioPlayer.playlist.medias)
|
|
|
|
PlaylistMediaTableCompanion.insert(
|
|
|
|
playlistId: playlist!.id,
|
|
|
|
uri: media.uri,
|
|
|
|
extras: Value(media.extras),
|
|
|
|
httpHeaders: Value(media.httpHeaders),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
} else if (medias.isNotEmpty) {
|
|
|
|
await audioPlayer.openPlaylist(
|
|
|
|
medias
|
|
|
|
.map(
|
|
|
|
(media) => RhythmMedia.fromMedia(
|
|
|
|
Media(
|
|
|
|
media.uri,
|
|
|
|
extras: media.extras,
|
|
|
|
httpHeaders: media.httpHeaders,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toList(),
|
|
|
|
initialIndex: playlist.index,
|
|
|
|
autoPlay: false,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (playerState.collections.isNotEmpty) {
|
|
|
|
state.value = state.value.copyWith(
|
|
|
|
collections: playerState.collections,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _updatePlayerState(
|
|
|
|
AudioPlayerStateTableCompanion companion,
|
|
|
|
) async {
|
|
|
|
final database = Get.find<DatabaseProvider>().database;
|
2024-08-26 17:49:05 +00:00
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
await (database.update(database.audioPlayerStateTable)
|
|
|
|
..where((tb) => tb.id.equals(0)))
|
|
|
|
.write(companion);
|
2024-08-27 06:35:16 +00:00
|
|
|
}
|
2024-08-26 17:49:05 +00:00
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
Future<void> _updatePlaylist(
|
|
|
|
Playlist playlist,
|
|
|
|
) async {
|
|
|
|
final database = Get.find<DatabaseProvider>().database;
|
|
|
|
|
|
|
|
await database.batch((batch) {
|
|
|
|
batch.update(
|
|
|
|
database.playlistTable,
|
|
|
|
PlaylistTableCompanion(index: Value(playlist.index)),
|
|
|
|
where: (tb) => tb.id.equals(0),
|
|
|
|
);
|
|
|
|
|
|
|
|
batch.deleteAll(database.playlistMediaTable);
|
|
|
|
|
|
|
|
if (playlist.medias.isEmpty) return;
|
|
|
|
batch.insertAll(
|
|
|
|
database.playlistMediaTable,
|
|
|
|
[
|
|
|
|
for (final media in playlist.medias)
|
|
|
|
PlaylistMediaTableCompanion.insert(
|
|
|
|
playlistId: 0,
|
|
|
|
uri: media.uri,
|
|
|
|
extras: Value(media.extras),
|
|
|
|
httpHeaders: Value(media.httpHeaders),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
2024-08-26 17:49:05 +00:00
|
|
|
}
|
|
|
|
|
2024-08-26 18:22:47 +00:00
|
|
|
Future<void> addCollections(List<String> collectionIds) async {
|
|
|
|
state.value = state.value.copyWith(collections: [
|
|
|
|
...state.value.collections,
|
|
|
|
...collectionIds,
|
|
|
|
]);
|
2024-08-27 06:35:16 +00:00
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
await _updatePlayerState(
|
|
|
|
AudioPlayerStateTableCompanion(
|
|
|
|
collections: Value(state.value.collections),
|
|
|
|
),
|
|
|
|
);
|
2024-08-26 18:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> addCollection(String collectionId) async {
|
|
|
|
await addCollections([collectionId]);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> removeCollections(List<String> collectionIds) async {
|
|
|
|
state.value = state.value.copyWith(
|
|
|
|
collections: state.value.collections
|
|
|
|
.where((element) => !collectionIds.contains(element))
|
|
|
|
.toList(),
|
|
|
|
);
|
2024-08-27 06:35:16 +00:00
|
|
|
|
2024-08-27 15:09:16 +00:00
|
|
|
await _updatePlayerState(
|
|
|
|
AudioPlayerStateTableCompanion(
|
|
|
|
collections: Value(state.value.collections),
|
|
|
|
),
|
|
|
|
);
|
2024-08-26 18:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> removeCollection(String collectionId) async {
|
|
|
|
await removeCollections([collectionId]);
|
|
|
|
}
|
|
|
|
|
2024-08-26 17:49:05 +00:00
|
|
|
Future<void> load(
|
|
|
|
List<Track> tracks, {
|
|
|
|
int initialIndex = 0,
|
|
|
|
bool autoPlay = false,
|
|
|
|
}) async {
|
|
|
|
final medias = tracks.map((x) => RhythmMedia(x)).toList();
|
|
|
|
|
|
|
|
// Giving the initial track a boost so MediaKit won't skip
|
|
|
|
// because of timeout
|
2024-08-27 12:49:48 +00:00
|
|
|
// final intendedActiveTrack = medias.elementAt(initialIndex);
|
|
|
|
// if (intendedActiveTrack.track is! LocalTrack) {
|
|
|
|
// await Get.find<SourcedTrackProvider>()
|
|
|
|
// .fetch(RhythmMedia(intendedActiveTrack.track));
|
|
|
|
// }
|
2024-08-26 17:49:05 +00:00
|
|
|
|
|
|
|
if (medias.isEmpty) return;
|
|
|
|
|
2024-08-26 18:22:47 +00:00
|
|
|
await removeCollections(state.value.collections);
|
|
|
|
|
2024-08-26 17:49:05 +00:00
|
|
|
await audioPlayer.openPlaylist(
|
|
|
|
medias.map((s) => s as Media).toList(),
|
|
|
|
initialIndex: initialIndex,
|
|
|
|
autoPlay: autoPlay,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> addTracksAtFirst(Iterable<Track> tracks) async {
|
|
|
|
if (state.value.tracks.length == 1) {
|
|
|
|
return addTracks(tracks);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < tracks.length; i++) {
|
|
|
|
final track = tracks.elementAt(i);
|
|
|
|
|
|
|
|
await audioPlayer.addTrackAt(
|
|
|
|
RhythmMedia(track),
|
|
|
|
max(state.value.playlist.index, 0) + i + 1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> addTrack(Track track) async {
|
|
|
|
await audioPlayer.addTrack(RhythmMedia(track));
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> addTracks(Iterable<Track> tracks) async {
|
|
|
|
for (final track in tracks) {
|
|
|
|
await audioPlayer.addTrack(RhythmMedia(track));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> removeTrack(String trackId) async {
|
|
|
|
final index =
|
|
|
|
state.value.tracks.indexWhere((element) => element.id == trackId);
|
|
|
|
|
|
|
|
if (index == -1) return;
|
|
|
|
|
|
|
|
await audioPlayer.removeTrack(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> removeTracks(Iterable<String> trackIds) async {
|
|
|
|
for (final trackId in trackIds) {
|
|
|
|
await removeTrack(trackId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> jumpToTrack(Track track) async {
|
|
|
|
final index = state.value.tracks
|
|
|
|
.toList()
|
|
|
|
.indexWhere((element) => element.id == track.id);
|
|
|
|
if (index == -1) return;
|
|
|
|
await audioPlayer.jumpTo(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> moveTrack(int oldIndex, int newIndex) async {
|
|
|
|
if (oldIndex == newIndex ||
|
|
|
|
newIndex < 0 ||
|
|
|
|
oldIndex < 0 ||
|
|
|
|
newIndex > state.value.tracks.length - 1 ||
|
|
|
|
oldIndex > state.value.tracks.length - 1) return;
|
|
|
|
|
2024-08-27 17:23:37 +00:00
|
|
|
final item = state.value.playlist.medias.removeAt(oldIndex);
|
|
|
|
|
|
|
|
state.value = state.value.copyWith(
|
|
|
|
playlist: state.value.playlist.copyWith(
|
|
|
|
medias: state.value.playlist.medias
|
|
|
|
..insert(oldIndex < newIndex ? newIndex - 1 : 0, item),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2024-08-26 17:49:05 +00:00
|
|
|
await audioPlayer.moveTrack(oldIndex, newIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> stop() async {
|
|
|
|
await audioPlayer.stop();
|
|
|
|
}
|
|
|
|
}
|