mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
fix: use id based source getters instead of index
This commit is contained in:
parent
75b46e1a17
commit
a0744630ba
@ -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
|
||||||
final track = tracks.firstWhereOrNull(
|
.map((source) {
|
||||||
(track) => makeAppropriateSource(track) == source,
|
final track = tracks.firstWhereOrNull(
|
||||||
);
|
(track) => makeAppropriateSource(track) == source,
|
||||||
return track!;
|
);
|
||||||
}).toList();
|
return track;
|
||||||
|
})
|
||||||
|
.whereNotNull()
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method must be called after any playback operation as
|
/// This method must be called after any playback operation as
|
||||||
|
@ -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;
|
||||||
|
@ -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)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user