import 'dart:async'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist; import 'package:collection/collection.dart'; class PlaylistQueue { final Set tracks; final Set tempTracks; final bool loop; final int active; Track get activeTrack => tracks.elementAt(active); static Future fromJson( Map json, UserPreferences preferences) async { final List? tracks = json['tracks']; final List? tempTracks = json['tempTracks']; return PlaylistQueue( Set.from( await Future.wait( tracks?.mapIndexed( (i, e) async { final jsonTrack = Map.castFrom(e); if (e["path"] != null) { return LocalTrack.fromJson(jsonTrack); } else if (i == json["active"] && !json.containsKey("path")) { return await SpotubeTrack.fetchFromTrack( Track.fromJson(jsonTrack), preferences, ); } else { return Track.fromJson(jsonTrack); } }, ) ?? [], ), ), active: json['active'], tempTracks: Set.from( await Future.wait( tempTracks?.mapIndexed( (i, e) async { final jsonTrack = Map.castFrom(e); if (e["path"] != null) { return LocalTrack.fromJson(jsonTrack); } else if (i == json["active"] && !json.containsKey("path")) { return await SpotubeTrack.fetchFromTrack( Track.fromJson(jsonTrack), preferences, ); } else { return Track.fromJson(jsonTrack); } }, ) ?? [], ), ), ); } Map toJson() { return { 'tracks': tracks.map( (e) { if (e is SpotubeTrack) { return e.toJson(); } else if (e is LocalTrack) { return e.toJson(); } else { return e.toJson(); } }, ).toList(), 'active': active, 'tempTracks': tempTracks.map( (e) { if (e is SpotubeTrack) { return e.toJson(); } else if (e is LocalTrack) { return e.toJson(); } else { return e.toJson(); } }, ).toList(), }; } bool get isLoading => activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack; bool get isShuffled => tempTracks.isNotEmpty; bool get isLooping => loop; PlaylistQueue( this.tracks, { required this.tempTracks, this.active = 0, this.loop = false, }) : assert(active < tracks.length && active >= 0, "Invalid active index"); PlaylistQueue copyWith({ Set? tracks, Set? tempTracks, int? active, bool? loop, }) { return PlaylistQueue( tracks ?? this.tracks, active: active ?? this.active, tempTracks: tempTracks ?? this.tempTracks, loop: loop ?? this.loop, ); } } class PlaylistQueueNotifier extends PersistedStateNotifier { final Ref ref; late AudioServices audioServices; static final provider = StateNotifierProvider( (ref) => PlaylistQueueNotifier._(ref), ); static final notifier = provider.notifier; PlaylistQueueNotifier._(this.ref) : super(null, "playlist") { configure(); } void configure() async { audioServices = await AudioServices.create(ref, this); audioPlayer.completedStream.listen((event) async { if (!isLoaded) return; if (state!.isLooping) { await audioPlayer.seek(Duration.zero); await audioPlayer.resume(); } else { await next(); } }); bool isPreSearching = false; audioPlayer.positionStream.listen((pos) async { if (!isLoaded) return; final currentDuration = await audioPlayer.duration ?? Duration.zero; // skip all the activeTrack.skipSegments if (state?.isLoading != true && state?.activeTrack is SpotubeTrack && (state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty == true && preferences.skipSponsorSegments) { for (final segment in (state!.activeTrack as SpotubeTrack).skipSegments) { if ((pos.inSeconds >= segment["start"]! && pos.inSeconds < segment["end"]!)) { await audioPlayer.pause(); await seek(Duration(seconds: segment["end"]!)); } } } // when the track progress is above 80%, track isn't the last // and is not already fetched and nothing is fetching currently if (pos.inSeconds > currentDuration.inSeconds * .8 && state!.active != state!.tracks.length - 1 && state!.tracks.elementAt(state!.active + 1) is! SpotubeTrack && !isPreSearching) { isPreSearching = true; final tracks = state!.tracks.toList(); final newTrack = await SpotubeTrack.fetchFromTrack( state!.tracks.elementAt(state!.active + 1), preferences, ); tracks[state!.active + 1] = newTrack; await audioPlayer.preload(newTrack.ytUri); state = state!.copyWith(tracks: Set.from(tracks)); isPreSearching = false; } }); } // properties // getters UserPreferences get preferences => ref.read(userPreferencesProvider); BlackListNotifier get blacklist => ref.read(BlackListNotifier.provider.notifier); bool get isLoaded => state != null; List