freezer/lib/api/player/player_helper.dart
Pato05 87c9733f51
add build script for linux
fix audio service stop on android
getTrack backend improvements
get new track token when expired
move shuffle button into LibraryPlaylists as FAB
move favoriteButton next to track title
move lyrics button on top of album art
search: fix chips, and remove checkbox when selected
2024-02-19 00:49:32 +01:00

344 lines
12 KiB
Dart

import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player/audio_handler.dart';
import 'package:freezer/main.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
class PlayerHelper {
late StreamSubscription _customEventSubscription;
late StreamSubscription _mediaItemSubscription;
late StreamSubscription _playbackStateStreamSubscription;
AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none;
bool equalizerOpen = false;
bool _shuffleEnabled = false;
int _queueIndex = 0;
bool _started = false;
/// Whether this system supports the [Connectivity] plugin or not
bool _isConnectivityPluginAvailable = true;
bool get isConnectivityPluginAvailable => _isConnectivityPluginAvailable;
//Visualizer
// StreamController _visualizerController = StreamController.broadcast();
// Stream get visualizerStream => _visualizerController.stream;
final _queueSourceSubject = BehaviorSubject<QueueSource>();
ValueStream<QueueSource> get queueSource => _queueSourceSubject.stream;
final _streamInfoSubject = BehaviorSubject<StreamQualityInfo>();
ValueStream<StreamQualityInfo> get streamInfo => _streamInfoSubject.stream;
final _bufferPositionSubject = BehaviorSubject<Duration>();
ValueStream<Duration> get bufferPosition => _bufferPositionSubject.stream;
final _playingSubject = BehaviorSubject<bool>();
ValueStream<bool> get playing => _playingSubject.stream;
final _processingStateSubject = BehaviorSubject<AudioProcessingState>();
ValueStream<AudioProcessingState> get processingState =>
_processingStateSubject.stream;
/// Find queue index by id
///
/// The function gets more expensive the longer the queue is and the further the element is from the beginning.
int getQueueIndexFromId() => audioHandler.mediaItem.value == null
? -1
: audioHandler.queue.value
.indexWhere((mi) => mi.id == audioHandler.mediaItem.value!.id);
int getQueueIndex() =>
audioHandler.playbackState.value.queueIndex ?? getQueueIndexFromId();
int get queueIndex => _queueIndex;
Future<void> initAudioHandler() async {
if (failsafe) {
Fluttertoast.showToast(msg: 'what the fuck?');
return;
}
failsafe = true;
final initArgs = AudioPlayerTaskInitArguments.from(
settings: settings, deezerAPI: deezerAPI);
// initialize our audiohandler instance
audioHandler = await AudioService.init<AudioPlayerTask>(
builder: () => AudioPlayerTask(initArgs),
config: AudioServiceConfig(
notificationColor: settings.primaryColor,
androidStopForegroundOnPause: true,
androidNotificationOngoing: true,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
androidNotificationChannelId: 'f.f.freezer.audio',
preloadArtwork: true,
androidBrowsableRootExtras: <String, dynamic>{
"android.media.browse.SEARCH_SUPPORTED":
true, // support showing search button on Android Auto as well as alternative search results on the player screen after voice search
},
),
cacheManager: cacheManager,
);
}
Future<void> start() async {
if (_started) return;
_started = true;
//Subscribe to custom events
_customEventSubscription = audioHandler.customEvent.listen((event) async {
if (event is! Map) return;
Logger('PlayerHelper').fine("event received: ${event['action']}");
switch (event['action']) {
case 'onRestore':
//Load queueSource from isolate
_queueSourceSubject.add(event['queueSource'] as QueueSource);
repeatType = event['repeatMode'] as AudioServiceRepeatMode;
_queueIndex = getQueueIndex();
break;
//Visualizer data
// case 'visualizer':
// _visualizerController.add(event['data']);
// break;
case 'streamInfo':
Logger('PlayerHelper').fine("streamInfo received");
_streamInfoSubject.add(event['data'] as StreamQualityInfo);
break;
case 'bufferPosition':
_bufferPositionSubject.add(event['data'] as Duration);
break;
case 'connectivityPlugin':
_isConnectivityPluginAvailable = event['available'] as bool;
break;
}
});
_mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async {
if (mediaItem == null) return;
_queueIndex = getQueueIndex();
//Load more flow if last song (not using .last since it iterates through previous elements first)
//Save queue
await audioHandler.customAction('saveQueue', {});
//Add to history
if (cache.history.isNotEmpty && cache.history.last.id == mediaItem.id) {
return;
}
cache.history.add(Track.fromMediaItem(mediaItem));
cache.save();
});
_playbackStateStreamSubscription =
audioHandler.playbackState.listen((playbackState) {
if (!_processingStateSubject.hasValue ||
_processingStateSubject.value != playbackState.processingState) {
_processingStateSubject.add(playbackState.processingState);
}
if (!_playingSubject.hasValue ||
_playingSubject.value != playbackState.playing) {
_playingSubject.add(playbackState.playing);
}
});
//Start audio_service
// await startService(); it is already ready, there is no need to start it
}
Future<void> authorizeLastFM() async {
if (settings.lastFMUsername == null || settings.lastFMPassword == null) {
return;
}
await audioHandler.customAction('authorizeLastFM', {
'username': settings.lastFMUsername,
'password': settings.lastFMPassword
});
}
Future<bool> toggleShuffle() async {
_shuffleEnabled = !_shuffleEnabled;
await audioHandler.setShuffleMode(_shuffleEnabled
? AudioServiceShuffleMode.all
: AudioServiceShuffleMode.none);
return _shuffleEnabled;
}
bool get shuffleEnabled => _shuffleEnabled;
//Repeat toggle
Future changeRepeat() async {
//Change to next repeat type
repeatType = repeatType == AudioServiceRepeatMode.all
? AudioServiceRepeatMode.none
: repeatType == AudioServiceRepeatMode.none
? AudioServiceRepeatMode.one
: AudioServiceRepeatMode.all;
//Set repeat type
await audioHandler.setRepeatMode(repeatType);
}
//Executed before exit
Future stop() async {
_customEventSubscription.cancel();
_playbackStateStreamSubscription.cancel();
_mediaItemSubscription.cancel();
_started = false;
}
//Replace queue, play specified track id
Future<void> _loadQueuePlay(List<MediaItem> queue, int? index) async {
if (index != null) {
await audioHandler.customAction('setIndex', {'index': index});
}
await audioHandler.updateQueue(queue);
// if (queue[0].id != trackId)
// await AudioService.skipToQueueItem(trackId);
if (!audioHandler.playbackState.value.playing) audioHandler.play();
}
//Play track from album
Future<void> playFromAlbum(Album album, [String? trackId]) async {
if (album.tracks!.length == 1) {
// play mix based on track
await playSearchMixDeferred(album.tracks![0]);
return;
}
await playFromTrackList(album.tracks!, trackId,
QueueSource(id: album.id, text: album.title, source: 'album'));
}
//Play mix by track
Future<void> playMix(String trackId, String trackTitle) async {
List<Track> tracks = await deezerAPI.playMix(trackId);
await playFromTrackList(
tracks,
tracks[0].id,
QueueSource(
id: trackId,
text: 'Mix based on %s'.i18n.fill([trackTitle]),
source: 'mix'));
}
Future<void> playSearchMixDeferred(Track track) async {
final playFuture = playFromTrackList(
[track],
null,
QueueSource(
id: track.id,
text: 'Mix based on %s'.i18n.fill([track.title!]),
source: 'searchMix'));
List<Track> tracks = await deezerAPI.getSearchTrackMix(track.id, false);
// discard first track (if it is the searched track)
if (tracks[0].id == track.id) tracks.removeAt(0);
await playFuture; // avoid race conditions
// update queue with mix
await audioHandler.addQueueItems(
tracks.map((e) => e.toMediaItem()).toList(growable: false));
}
Future<void> playSearchMix(String trackId, String trackTitle) async {
List<Track> tracks = await deezerAPI.getSearchTrackMix(trackId, true);
await playFromTrackList(
tracks,
null, // we can avoid passing it, as the index is 0
QueueSource(
id: trackId,
text: 'Mix based on %s'.i18n.fill([trackTitle]),
source: 'searchMix'));
}
//Play from artist top tracks
Future<void> playFromTopTracks(
List<Track> tracks, String? trackId, Artist artist) async {
await playFromTrackList(
tracks,
trackId,
QueueSource(
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
}
Future<void> playFromPlaylist(Playlist playlist, [String? trackId]) async {
await playFromTrackList(playlist.tracks!, trackId,
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
}
//Play episode from show, load whole show as queue
Future<void> playShowEpisode(Show show, List<ShowEpisode> episodes,
{int index = 0}) async {
QueueSource queueSource =
QueueSource(id: show.id, text: show.name, source: 'show');
//Generate media items
List<MediaItem> queue =
episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
//Load and play
// await startService(); // audioservice is ready
await setQueueSource(queueSource);
await audioHandler.customAction('setIndex', {'index': index});
await audioHandler.updateQueue(queue);
if (!audioHandler.playbackState.value.playing) audioHandler.play();
}
//Load tracks as queue, play track id, set queue source
Future<void> playFromTrackList(
List<Track> tracks, String? trackId, QueueSource queueSource) async {
final queue =
tracks.map<MediaItem>((track) => track.toMediaItem()).toList();
await setQueueSource(queueSource);
await _loadQueuePlay(
queue, trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId));
}
//Load smart track list as queue, start from beginning
Future<void> playFromSmartTrackList(SmartTrackList stl) async {
//Load from API if no tracks
if (stl.tracks == null || stl.tracks!.isEmpty) {
if (settings.offlineMode) {
Fluttertoast.showToast(
msg: "Offline mode, can't play flow or smart track lists.".i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
return;
}
//Flow songs cannot be accessed by smart track list call
if (stl.id! == 'flow') {
stl.tracks = await deezerAPI.flow(stl.flowConfig);
} else {
stl = await deezerAPI.smartTrackList(stl.id);
}
}
QueueSource queueSource = QueueSource(
id: stl.flowConfig ?? stl.id,
source: (stl.id == 'flow') ? 'flow' : 'smarttracklist',
text: stl.title ??
((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n));
await playFromTrackList(stl.tracks!, stl.tracks![0].id, queueSource);
}
Future setQueueSource(QueueSource queueSource) async {
_queueSourceSubject.add(queueSource);
await audioHandler.customAction('queueSource', queueSource.toJson());
}
//Reorder tracks in queue
Future reorder(int oldIndex, int newIndex) => audioHandler
.customAction('reorder', {'oldIndex': oldIndex, 'newIndex': newIndex});
//Start visualizer
// Future startVisualizer() async {
// await audioHandler.customAction('startVisualizer');
// }
//Stop visualizer
// Future stopVisualizer() async {
// await audioHandler.customAction('stopVisualizer');
// }
}