import 'package:catcher/catcher.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/supabase.dart'; mixin NextFetcher on StateNotifier { Future> fetchTracks( UserPreferences preferences, { int count = 3, int offset = 0, }) async { /// get [count] [state.tracks] that are not [SpotubeTrack] and [LocalTrack] final bareTracks = state.tracks .skip(offset) .where((element) => element is! SpotubeTrack && element is! LocalTrack) .take(count); /// fetch [bareTracks] one by one with 100ms delay final fetchedTracks = await Future.wait( bareTracks.mapIndexed((i, track) async { final future = SpotubeTrack.fetchFromTrack(track, preferences); if (i == 0) { return await future; } return await Future.delayed( const Duration(milliseconds: 100), () => future, ); }), ); return fetchedTracks; } /// Merges List of [SpotubeTrack]s with [Track]s and outputs a mixed List Set mergeTracks( Iterable fetchTracks, Iterable tracks, ) { return tracks.map((track) { final fetchedTrack = fetchTracks.firstWhereOrNull( (fetchTrack) => fetchTrack.id == track.id, ); if (fetchedTrack != null) { return fetchedTrack; } return track; }).toSet(); } /// Checks if [Track] is playable bool isUnPlayable(String source) { return source.startsWith('https://youtube.com/unplayable.m4a?id='); } bool isPlayable(String source) => !isUnPlayable(source); /// Returns [Track.id] from [isUnPlayable] source that is not playable String getIdFromUnPlayable(String source) { return source.replaceFirst('https://youtube.com/unplayable.m4a?id=', ''); } /// Returns appropriate Media source for [Track] /// /// * If [Track] is [SpotubeTrack] then return [SpotubeTrack.ytUri] /// * If [Track] is [LocalTrack] then return [LocalTrack.path] /// * If [Track] is [Track] then return [Track.id] with [isUnPlayable] source String makeAppropriateSource(Track track) { if (track is SpotubeTrack) { return track.ytUri; } else if (track is LocalTrack) { return track.path; } else { return "https://youtube.com/unplayable.m4a?id=${track.id}"; } } /// This method must be called after any playback operation as /// it can increase the latency Future storeTrack(Track track, SpotubeTrack spotubeTrack) async { if (track is! SpotubeTrack) { await supabase .insertTrack( MatchedTrack( youtubeId: spotubeTrack.ytTrack.id, spotifyId: spotubeTrack.id!, ), ) .catchError(Catcher.reportCheckedError); } } }