diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index daede47f..947b5c07 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -64,7 +64,10 @@ mixin NextFetcher on StateNotifier { /// Returns [Track.id] from [isUnPlayable] source that is not playable String getIdFromUnPlayable(String source) { - return source.replaceFirst('https://youtube.com/unplayable.m4a?id=', ''); + return source + .split('&') + .first + .replaceFirst('https://youtube.com/unplayable.m4a?id=', ''); } /// Returns appropriate Media source for [Track] @@ -78,19 +81,25 @@ mixin NextFetcher on StateNotifier { } else if (track is LocalTrack) { return track.path; } else { - return "https://youtube.com/unplayable.m4a?id=${track.id}"; + return "https://youtube.com/unplayable.m4a?id=${track.id}&title=${track.name?.replaceAll( + RegExp(r'\s+', caseSensitive: false), + '-', + )}"; } } List mapSourcesToTracks(List sources) { final tracks = state.tracks; - return sources.map((source) { - final track = tracks.firstWhereOrNull( - (track) => makeAppropriateSource(track) == source, - ); - return track!; - }).toList(); + return sources + .map((source) { + final track = tracks.firstWhereOrNull( + (track) => makeAppropriateSource(track) == source, + ); + return track; + }) + .whereNotNull() + .toList(); } /// This method must be called after any playback operation as diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index f3d63a45..f620b9a9 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -56,50 +56,70 @@ class ProxyPlaylistNotifier extends StateNotifier () async { notificationService = await AudioServices.create(ref, this); - audioPlayer.currentIndexChangedStream.listen((index) async { - if (index == -1 || index == state.active) return; + audioPlayer.activeSourceChangedStream.listen((newActiveSource) { + final newActiveTrack = + mapSourcesToTracks([newActiveSource]).firstOrNull; - final newIndexedTrack = - mapSourcesToTracks([audioPlayer.sources[index]]).firstOrNull; + if (newActiveTrack == null || + newActiveTrack.id == state.activeTrack?.id) { + return; + } - if (newIndexedTrack == null) return; - notificationService.addTrack(newIndexedTrack); + print('=============== Active Track Changed ==============='); + + print('Current tracks: ${state.tracks.map((e) => e.name).toList()}'); + print('newIndexedTrack: ${newActiveTrack.name}'); + + notificationService.addTrack(newActiveTrack); state = state.copyWith( active: state.tracks .toList() - .indexWhere((element) => element.id == newIndexedTrack.id), + .indexWhere((element) => element.id == newActiveTrack.id), ); + print('New active: ${state.active}'); + print('=============== ----- ==============='); if (preferences.albumColorSync) { updatePalette(); } }); audioPlayer.shuffledStream.listen((event) { + print('=============== Shuffled ==============='); + + print('oldTracks: ${state.tracks.map((e) => e.name).toList()}'); + final newlyOrderedTracks = mapSourcesToTracks(audioPlayer.sources); - final newIndex = newlyOrderedTracks.indexWhere( + + print( + 'newlyOrderedTracks: ${newlyOrderedTracks.map((e) => e.name).toList()}'); + + final newActiveIndex = newlyOrderedTracks.indexWhere( (element) => element.id == state.activeTrack?.id, ); - if (newIndex == -1) return; + print('newActiveIndex $newActiveIndex'); + + print('=============== ----- ==============='); + + if (newActiveIndex == -1) return; state = state.copyWith( tracks: newlyOrderedTracks.toSet(), - active: newIndex, + active: newActiveIndex, ); }); bool isPreSearching = false; audioPlayer.percentCompletedStream(60).listen((percent) async { - if (isPreSearching) return; + if (isPreSearching || audioPlayer.currentSource == null) return; try { isPreSearching = true; // TODO: Make repeat mode sensitive changes later final oldTrack = - state.tracks.elementAtOrNull(audioPlayer.currentIndex); - final track = - await ensureNthSourcePlayable(audioPlayer.currentIndex + 1); + mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; + final track = await ensureSourcePlayable(audioPlayer.nextSource!); if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); @@ -129,36 +149,35 @@ class ProxyPlaylistNotifier extends StateNotifier // player stops at 99% if nextSource is still not playable audioPlayer.percentCompletedStream(99).listen((_) async { - final nextSource = - audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex + 1); - if (nextSource == null || isPlayable(nextSource)) return; + if (audioPlayer.nextSource == null || + isPlayable(audioPlayer.nextSource!)) return; await audioPlayer.pause(); }); }(); } - Future ensureNthSourcePlayable(int n) async { - final sources = audioPlayer.sources; - if (n < 0 || n > sources.length - 1) return null; - final nthSource = sources.elementAtOrNull(n); - if (nthSource == null || !isUnPlayable(nthSource)) return null; + Future ensureSourcePlayable(String source) async { + print("======== Ensure Source Playable ========="); + print("source: $source"); - final nthTrack = state.tracks.firstWhereOrNull( - (element) => element.id == getIdFromUnPlayable(nthSource), - ); - if (nthTrack == null || nthTrack is LocalTrack) { + if (isPlayable(source)) return null; + + final track = mapSourcesToTracks([source]).firstOrNull; + + print("nthTrack: ${track?.name}"); + if (track == null || track is LocalTrack) { return null; } - final nthFetchedTrack = switch (nthTrack.runtimeType) { - SpotubeTrack => nthTrack as SpotubeTrack, - _ => await SpotubeTrack.fetchFromTrack(nthTrack, preferences), + final nthFetchedTrack = switch (track.runtimeType) { + SpotubeTrack => track as SpotubeTrack, + _ => await SpotubeTrack.fetchFromTrack(track, preferences), }; - if (nthSource == nthFetchedTrack.ytUri) return null; + print("======== ----- ========="); await audioPlayer.replaceSource( - nthSource, + source, nthFetchedTrack.ytUri, ); @@ -235,8 +254,9 @@ class ProxyPlaylistNotifier extends StateNotifier } Future jumpTo(int index) async { - final oldTrack = state.tracks.elementAtOrNull(audioPlayer.currentIndex); - final track = await ensureNthSourcePlayable(index); + final oldTrack = + mapSourcesToTracks([audioPlayer.currentSource!]).firstOrNull; + final track = await ensureSourcePlayable(audioPlayer.sources[index]); if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); } @@ -278,8 +298,9 @@ class ProxyPlaylistNotifier extends StateNotifier Future swapSibling(PipedSearchItem video) async {} Future next() async { - final oldTrack = state.tracks.elementAtOrNull(audioPlayer.currentIndex + 1); - final track = await ensureNthSourcePlayable(audioPlayer.currentIndex + 1); + if (audioPlayer.nextSource == null) return; + final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; + final track = await ensureSourcePlayable(audioPlayer.nextSource!); if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); } @@ -294,8 +315,10 @@ class ProxyPlaylistNotifier extends StateNotifier } Future previous() async { - final oldTrack = state.tracks.elementAtOrNull(audioPlayer.currentIndex - 1); - final track = await ensureNthSourcePlayable(audioPlayer.currentIndex - 1); + if (audioPlayer.previousSource == null) return; + final oldTrack = + mapSourcesToTracks([audioPlayer.previousSource!]).firstOrNull; + final track = await ensureSourcePlayable(audioPlayer.previousSource!); if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); } @@ -317,9 +340,7 @@ class ProxyPlaylistNotifier extends StateNotifier return Future.microtask(() async { final activeTrack = state.tracks.firstWhereOrNull( (track) => - track is SpotubeTrack && - track.ytUri == - audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex), + track is SpotubeTrack && track.ytUri == audioPlayer.currentSource, ); if (activeTrack == null) return; diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 455dde3b..40f4e09b 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -62,6 +62,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface Future stop() async { await _mkPlayer?.stop(); await _justAudio?.stop(); + await _justAudio?.setShuffleModeEnabled(false); + await _justAudio?.setLoopMode(ja.LoopMode.off); } Future seek(Duration position) async { @@ -138,11 +140,45 @@ class SpotubeAudioPlayer extends AudioPlayerInterface } } - int get currentIndex { + String? get currentSource { if (mkSupportedPlatform) { - return _mkPlayer!.playlist.index; + return _mkPlayer!.playlist.medias + .elementAtOrNull(_mkPlayer!.playlist.index) + ?.uri; } else { - return _justAudio!.sequenceState?.currentIndex ?? -1; + return (_justAudio?.sequenceState?.effectiveSequence + .elementAtOrNull(_justAudio!.sequenceState!.currentIndex) + as ja.UriAudioSource?) + ?.uri + .toString(); + } + } + + String? get nextSource { + if (mkSupportedPlatform) { + return _mkPlayer!.playlist.medias + .elementAtOrNull(_mkPlayer!.playlist.index + 1) + ?.uri; + } else { + return (_justAudio?.sequenceState?.effectiveSequence + .elementAtOrNull(_justAudio!.sequenceState!.currentIndex + 1) + as ja.UriAudioSource?) + ?.uri + .toString(); + } + } + + String? get previousSource { + if (mkSupportedPlatform) { + return _mkPlayer!.playlist.medias + .elementAtOrNull(_mkPlayer!.playlist.index - 1) + ?.uri; + } else { + return (_justAudio?.sequenceState?.effectiveSequence + .elementAtOrNull(_justAudio!.sequenceState!.currentIndex - 1) + as ja.UriAudioSource?) + ?.uri + .toString(); } } @@ -209,21 +245,27 @@ class SpotubeAudioPlayer extends AudioPlayerInterface if (mkSupportedPlatform) { _mkPlayer!.replace(oldSource, newSource); } else { - await addTrack(newSource); - await removeTrack(oldSourceIndex); + final playlist = _justAudio!.audioSource as ja.ConcatenatingAudioSource; - int newSourceIndex = sources.indexOf(newSource); - while (newSourceIndex == -1) { - await Future.delayed(const Duration(milliseconds: 100)); - newSourceIndex = sources.indexOf(newSource); - } - await moveTrack(newSourceIndex, oldSourceIndex); - newSourceIndex = sources.indexOf(newSource); - while (newSourceIndex != oldSourceIndex) { - await Future.delayed(const Duration(milliseconds: 100)); - await moveTrack(newSourceIndex, oldSourceIndex); - newSourceIndex = sources.indexOf(newSource); + print('oldSource: $oldSource'); + print('newSource: $newSource'); + final oldSourceIndexInPlaylist = + _justAudio?.sequenceState?.effectiveSequence.indexWhere( + (e) => (e as ja.UriAudioSource).uri.toString() == oldSource, + ); + + print('oldSourceIndexInPlaylist: $oldSourceIndexInPlaylist'); + + // ignores non existing source + if (oldSourceIndexInPlaylist == null || oldSourceIndexInPlaylist == -1) { + return; } + + await playlist.removeAt(oldSourceIndexInPlaylist); + await playlist.insert( + oldSourceIndexInPlaylist, + ja.AudioSource.uri(Uri.parse(newSource)), + ); } } diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index dc5654ed..90644d33 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -127,4 +127,22 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { .asBroadcastStream(); } } + + Stream get activeSourceChangedStream { + if (mkSupportedPlatform) { + return _mkPlayer!.indexChangeStream + .map((event) { + return _mkPlayer!.playlist.medias.elementAtOrNull(event)?.uri; + }) + .where((event) => event != null) + .cast(); + } else { + return _justAudio!.sequenceStateStream + .map((event) { + return (event?.currentSource as ja.UriAudioSource?)?.uri.toString(); + }) + .where((event) => event != null) + .cast(); + } + } }