import 'dart:async'; import 'dart:math'; import 'package:drift/drift.dart'; import 'package:get/get.dart' hide Value; import 'package:media_kit/media_kit.dart' hide Track; import 'package:rhythm_box/providers/database.dart'; import 'package:rhythm_box/services/audio_player/state.dart'; import 'package:rhythm_box/services/database/database.dart'; import 'package:spotify/spotify.dart' hide Playlist; import 'package:rhythm_box/services/audio_player/audio_player.dart'; class AudioPlayerProvider extends GetxController { Rx durationTotal = Rx(Duration.zero); Rx durationCurrent = Rx(Duration.zero); Rx durationBuffered = Rx(Duration.zero); RxBool isPlaying = false.obs; Rx state = Rx(AudioPlayerState( playing: false, shuffled: false, loopMode: PlaylistMode.none, playlist: const Playlist([]), collections: [], )); List>? _subscriptions; @override void onInit() { _subscriptions = [ audioPlayer.playingStream.listen((playing) async { state.value = state.value.copyWith(playing: playing); await _updatePlayerState( AudioPlayerStateTableCompanion( playing: Value(playing), ), ); }), audioPlayer.loopModeStream.listen((loopMode) async { state.value = state.value.copyWith(loopMode: loopMode); await _updatePlayerState( AudioPlayerStateTableCompanion( loopMode: Value(loopMode), ), ); }), audioPlayer.shuffledStream.listen((shuffled) async { state.value = state.value.copyWith(shuffled: shuffled); await _updatePlayerState( AudioPlayerStateTableCompanion( shuffled: Value(shuffled), ), ); }), audioPlayer.playlistStream.listen((playlist) async { state.value = state.value.copyWith(playlist: playlist); await _updatePlaylist(playlist); }), audioPlayer.durationStream.listen((value) => durationTotal.value = value), audioPlayer.positionStream .listen((value) => durationCurrent.value = value), audioPlayer.bufferedPositionStream .listen((value) => durationBuffered.value = value), ]; _readSavedState(); audioPlayer.playingStream.listen((playing) { isPlaying.value = playing; }); super.onInit(); } @override void dispose() { if (_subscriptions != null) { for (final subscription in _subscriptions!) { subscription.cancel(); } } super.dispose(); } Future _readSavedState() async { final database = Get.find().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: [], 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 _updatePlayerState( AudioPlayerStateTableCompanion companion, ) async { final database = Get.find().database; await (database.update(database.audioPlayerStateTable) ..where((tb) => tb.id.equals(0))) .write(companion); } Future _updatePlaylist( Playlist playlist, ) async { final database = Get.find().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), ), ], ); }); } Future addCollections(List collectionIds) async { state.value = state.value.copyWith(collections: [ ...state.value.collections, ...collectionIds, ]); await _updatePlayerState( AudioPlayerStateTableCompanion( collections: Value(state.value.collections), ), ); } Future addCollection(String collectionId) async { await addCollections([collectionId]); } Future removeCollections(List collectionIds) async { state.value = state.value.copyWith( collections: state.value.collections .where((element) => !collectionIds.contains(element)) .toList(), ); await _updatePlayerState( AudioPlayerStateTableCompanion( collections: Value(state.value.collections), ), ); } Future removeCollection(String collectionId) async { await removeCollections([collectionId]); } Future load( List 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 // final intendedActiveTrack = medias.elementAt(initialIndex); // if (intendedActiveTrack.track is! LocalTrack) { // await Get.find() // .fetch(RhythmMedia(intendedActiveTrack.track)); // } if (medias.isEmpty) return; await removeCollections(state.value.collections); await audioPlayer.openPlaylist( medias.map((s) => s as Media).toList(), initialIndex: initialIndex, autoPlay: autoPlay, ); } Future addTracksAtFirst(Iterable 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 addTrack(Track track) async { await audioPlayer.addTrack(RhythmMedia(track)); } Future addTracks(Iterable tracks) async { for (final track in tracks) { await audioPlayer.addTrack(RhythmMedia(track)); } } Future removeTrack(String trackId) async { final index = state.value.tracks.indexWhere((element) => element.id == trackId); if (index == -1) return; await audioPlayer.removeTrack(index); } Future removeTracks(Iterable trackIds) async { for (final trackId in trackIds) { await removeTrack(trackId); } } Future 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 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; 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), ), ); await audioPlayer.moveTrack(oldIndex, newIndex); } Future stop() async { await audioPlayer.stop(); } }