RhythmBox/lib/providers/audio_player.dart

343 lines
9.7 KiB
Dart
Raw Normal View History

2024-08-26 18:22:47 +00:00
import 'dart:async';
2024-08-26 17:49:05 +00:00
import 'dart:math';
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;
import 'package:rhythm_box/providers/database.dart';
2024-08-26 17:49:05 +00:00
import 'package:rhythm_box/services/audio_player/state.dart';
import 'package:rhythm_box/services/database/database.dart';
2024-08-30 14:16:10 +00:00
import 'package:rhythm_box/services/local_track.dart';
import 'package:rhythm_box/services/server/sourced_track.dart';
import 'package:rhythm_box/widgets/tracks/querying_track_info.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);
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);
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);
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);
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
];
_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
}
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
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
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
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
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-30 14:16:10 +00:00
Get.find<QueryingTrackInfoProvider>().isQueryingTrackInfo.value = true;
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();
}
}