fix: use id based source getters instead of index

This commit is contained in:
Kingkor Roy Tirtho 2023-05-27 12:01:59 +06:00
parent 75b46e1a17
commit a0744630ba
4 changed files with 154 additions and 64 deletions

View File

@ -64,7 +64,10 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
/// Returns [Track.id] from [isUnPlayable] source that is not playable /// Returns [Track.id] from [isUnPlayable] source that is not playable
String getIdFromUnPlayable(String source) { 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] /// Returns appropriate Media source for [Track]
@ -78,19 +81,25 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
} else if (track is LocalTrack) { } else if (track is LocalTrack) {
return track.path; return track.path;
} else { } 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<Track> mapSourcesToTracks(List<String> sources) { List<Track> mapSourcesToTracks(List<String> sources) {
final tracks = state.tracks; final tracks = state.tracks;
return sources.map((source) { return sources
.map((source) {
final track = tracks.firstWhereOrNull( final track = tracks.firstWhereOrNull(
(track) => makeAppropriateSource(track) == source, (track) => makeAppropriateSource(track) == source,
); );
return track!; return track;
}).toList(); })
.whereNotNull()
.toList();
} }
/// This method must be called after any playback operation as /// This method must be called after any playback operation as

View File

@ -56,50 +56,70 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
() async { () async {
notificationService = await AudioServices.create(ref, this); notificationService = await AudioServices.create(ref, this);
audioPlayer.currentIndexChangedStream.listen((index) async { audioPlayer.activeSourceChangedStream.listen((newActiveSource) {
if (index == -1 || index == state.active) return; final newActiveTrack =
mapSourcesToTracks([newActiveSource]).firstOrNull;
final newIndexedTrack = if (newActiveTrack == null ||
mapSourcesToTracks([audioPlayer.sources[index]]).firstOrNull; newActiveTrack.id == state.activeTrack?.id) {
return;
}
if (newIndexedTrack == null) return; print('=============== Active Track Changed ===============');
notificationService.addTrack(newIndexedTrack);
print('Current tracks: ${state.tracks.map((e) => e.name).toList()}');
print('newIndexedTrack: ${newActiveTrack.name}');
notificationService.addTrack(newActiveTrack);
state = state.copyWith( state = state.copyWith(
active: state.tracks active: state.tracks
.toList() .toList()
.indexWhere((element) => element.id == newIndexedTrack.id), .indexWhere((element) => element.id == newActiveTrack.id),
); );
print('New active: ${state.active}');
print('=============== ----- ===============');
if (preferences.albumColorSync) { if (preferences.albumColorSync) {
updatePalette(); updatePalette();
} }
}); });
audioPlayer.shuffledStream.listen((event) { audioPlayer.shuffledStream.listen((event) {
print('=============== Shuffled ===============');
print('oldTracks: ${state.tracks.map((e) => e.name).toList()}');
final newlyOrderedTracks = mapSourcesToTracks(audioPlayer.sources); 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, (element) => element.id == state.activeTrack?.id,
); );
if (newIndex == -1) return; print('newActiveIndex $newActiveIndex');
print('=============== ----- ===============');
if (newActiveIndex == -1) return;
state = state.copyWith( state = state.copyWith(
tracks: newlyOrderedTracks.toSet(), tracks: newlyOrderedTracks.toSet(),
active: newIndex, active: newActiveIndex,
); );
}); });
bool isPreSearching = false; bool isPreSearching = false;
audioPlayer.percentCompletedStream(60).listen((percent) async { audioPlayer.percentCompletedStream(60).listen((percent) async {
if (isPreSearching) return; if (isPreSearching || audioPlayer.currentSource == null) return;
try { try {
isPreSearching = true; isPreSearching = true;
// TODO: Make repeat mode sensitive changes later // TODO: Make repeat mode sensitive changes later
final oldTrack = final oldTrack =
state.tracks.elementAtOrNull(audioPlayer.currentIndex); mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
final track = final track = await ensureSourcePlayable(audioPlayer.nextSource!);
await ensureNthSourcePlayable(audioPlayer.currentIndex + 1);
if (track != null) { if (track != null) {
state = state.copyWith(tracks: mergeTracks([track], state.tracks)); state = state.copyWith(tracks: mergeTracks([track], state.tracks));
@ -129,36 +149,35 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
// player stops at 99% if nextSource is still not playable // player stops at 99% if nextSource is still not playable
audioPlayer.percentCompletedStream(99).listen((_) async { audioPlayer.percentCompletedStream(99).listen((_) async {
final nextSource = if (audioPlayer.nextSource == null ||
audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex + 1); isPlayable(audioPlayer.nextSource!)) return;
if (nextSource == null || isPlayable(nextSource)) return;
await audioPlayer.pause(); await audioPlayer.pause();
}); });
}(); }();
} }
Future<SpotubeTrack?> ensureNthSourcePlayable(int n) async { Future<SpotubeTrack?> ensureSourcePlayable(String source) async {
final sources = audioPlayer.sources; print("======== Ensure Source Playable =========");
if (n < 0 || n > sources.length - 1) return null; print("source: $source");
final nthSource = sources.elementAtOrNull(n);
if (nthSource == null || !isUnPlayable(nthSource)) return null;
final nthTrack = state.tracks.firstWhereOrNull( if (isPlayable(source)) return null;
(element) => element.id == getIdFromUnPlayable(nthSource),
); final track = mapSourcesToTracks([source]).firstOrNull;
if (nthTrack == null || nthTrack is LocalTrack) {
print("nthTrack: ${track?.name}");
if (track == null || track is LocalTrack) {
return null; return null;
} }
final nthFetchedTrack = switch (nthTrack.runtimeType) { final nthFetchedTrack = switch (track.runtimeType) {
SpotubeTrack => nthTrack as SpotubeTrack, SpotubeTrack => track as SpotubeTrack,
_ => await SpotubeTrack.fetchFromTrack(nthTrack, preferences), _ => await SpotubeTrack.fetchFromTrack(track, preferences),
}; };
if (nthSource == nthFetchedTrack.ytUri) return null; print("======== ----- =========");
await audioPlayer.replaceSource( await audioPlayer.replaceSource(
nthSource, source,
nthFetchedTrack.ytUri, nthFetchedTrack.ytUri,
); );
@ -235,8 +254,9 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
} }
Future<void> jumpTo(int index) async { Future<void> jumpTo(int index) async {
final oldTrack = state.tracks.elementAtOrNull(audioPlayer.currentIndex); final oldTrack =
final track = await ensureNthSourcePlayable(index); mapSourcesToTracks([audioPlayer.currentSource!]).firstOrNull;
final track = await ensureSourcePlayable(audioPlayer.sources[index]);
if (track != null) { if (track != null) {
state = state.copyWith(tracks: mergeTracks([track], state.tracks)); state = state.copyWith(tracks: mergeTracks([track], state.tracks));
} }
@ -278,8 +298,9 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
Future<void> swapSibling(PipedSearchItem video) async {} Future<void> swapSibling(PipedSearchItem video) async {}
Future<void> next() async { Future<void> next() async {
final oldTrack = state.tracks.elementAtOrNull(audioPlayer.currentIndex + 1); if (audioPlayer.nextSource == null) return;
final track = await ensureNthSourcePlayable(audioPlayer.currentIndex + 1); final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
if (track != null) { if (track != null) {
state = state.copyWith(tracks: mergeTracks([track], state.tracks)); state = state.copyWith(tracks: mergeTracks([track], state.tracks));
} }
@ -294,8 +315,10 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
} }
Future<void> previous() async { Future<void> previous() async {
final oldTrack = state.tracks.elementAtOrNull(audioPlayer.currentIndex - 1); if (audioPlayer.previousSource == null) return;
final track = await ensureNthSourcePlayable(audioPlayer.currentIndex - 1); final oldTrack =
mapSourcesToTracks([audioPlayer.previousSource!]).firstOrNull;
final track = await ensureSourcePlayable(audioPlayer.previousSource!);
if (track != null) { if (track != null) {
state = state.copyWith(tracks: mergeTracks([track], state.tracks)); state = state.copyWith(tracks: mergeTracks([track], state.tracks));
} }
@ -317,9 +340,7 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
return Future.microtask(() async { return Future.microtask(() async {
final activeTrack = state.tracks.firstWhereOrNull( final activeTrack = state.tracks.firstWhereOrNull(
(track) => (track) =>
track is SpotubeTrack && track is SpotubeTrack && track.ytUri == audioPlayer.currentSource,
track.ytUri ==
audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex),
); );
if (activeTrack == null) return; if (activeTrack == null) return;

View File

@ -62,6 +62,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
Future<void> stop() async { Future<void> stop() async {
await _mkPlayer?.stop(); await _mkPlayer?.stop();
await _justAudio?.stop(); await _justAudio?.stop();
await _justAudio?.setShuffleModeEnabled(false);
await _justAudio?.setLoopMode(ja.LoopMode.off);
} }
Future<void> seek(Duration position) async { Future<void> seek(Duration position) async {
@ -138,11 +140,45 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
} }
} }
int get currentIndex { String? get currentSource {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
return _mkPlayer!.playlist.index; return _mkPlayer!.playlist.medias
.elementAtOrNull(_mkPlayer!.playlist.index)
?.uri;
} else { } 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) { if (mkSupportedPlatform) {
_mkPlayer!.replace(oldSource, newSource); _mkPlayer!.replace(oldSource, newSource);
} else { } else {
await addTrack(newSource); final playlist = _justAudio!.audioSource as ja.ConcatenatingAudioSource;
await removeTrack(oldSourceIndex);
int newSourceIndex = sources.indexOf(newSource); print('oldSource: $oldSource');
while (newSourceIndex == -1) { print('newSource: $newSource');
await Future.delayed(const Duration(milliseconds: 100)); final oldSourceIndexInPlaylist =
newSourceIndex = sources.indexOf(newSource); _justAudio?.sequenceState?.effectiveSequence.indexWhere(
} (e) => (e as ja.UriAudioSource).uri.toString() == oldSource,
await moveTrack(newSourceIndex, oldSourceIndex); );
newSourceIndex = sources.indexOf(newSource);
while (newSourceIndex != oldSourceIndex) { print('oldSourceIndexInPlaylist: $oldSourceIndexInPlaylist');
await Future.delayed(const Duration(milliseconds: 100));
await moveTrack(newSourceIndex, oldSourceIndex); // ignores non existing source
newSourceIndex = sources.indexOf(newSource); if (oldSourceIndexInPlaylist == null || oldSourceIndexInPlaylist == -1) {
return;
} }
await playlist.removeAt(oldSourceIndexInPlaylist);
await playlist.insert(
oldSourceIndexInPlaylist,
ja.AudioSource.uri(Uri.parse(newSource)),
);
} }
} }

View File

@ -127,4 +127,22 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
.asBroadcastStream(); .asBroadcastStream();
} }
} }
Stream<String> get activeSourceChangedStream {
if (mkSupportedPlatform) {
return _mkPlayer!.indexChangeStream
.map((event) {
return _mkPlayer!.playlist.medias.elementAtOrNull(event)?.uri;
})
.where((event) => event != null)
.cast<String>();
} else {
return _justAudio!.sequenceStateStream
.map((event) {
return (event?.currentSource as ja.UriAudioSource?)?.uri.toString();
})
.where((event) => event != null)
.cast<String>();
}
}
} }