import 'package:audio_service/audio_service.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.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/user_preferences_provider.dart'; import 'package:spotube/services/audio_player.dart'; import 'package:spotube/services/linux_audio_service.dart'; import 'package:spotube/services/mobile_audio_service.dart'; import 'package:spotube/services/windows_audio_service.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.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; MobileAudioService? mobileService; LinuxAudioService? linuxService; WindowsAudioService? windowsService; static final provider = StateNotifierProvider( (ref) => PlaylistQueueNotifier._(ref), ); static final notifier = provider.notifier; PlaylistQueueNotifier._(this.ref) : super(null, "playlist") { configure(); } void configure() async { if (kIsMobile || kIsMacOS) { mobileService = await AudioService.init( builder: () => MobileAudioService( this, ref.read(VolumeProvider.provider.notifier), ), config: const AudioServiceConfig( androidNotificationChannelId: 'com.krtirtho.Spotube', androidNotificationChannelName: 'Spotube', androidNotificationOngoing: true, ), ); } if (kIsLinux) { linuxService = LinuxAudioService(ref, this); } if (kIsWindows) { windowsService = WindowsAudioService(ref, this); } addListener((state) { linuxService?.player.updateProperties(); }); audioPlayer.onPlayerStateChanged.listen((event) { linuxService?.player.updateProperties(); }); audioPlayer.onPlayerComplete.listen((event) async { if (!isLoaded) return; if (state!.isLooping) { await audioPlayer.seek(Duration.zero); await audioPlayer.resume(); } else { await next(); } }); bool isPreSearching = false; audioPlayer.onPositionChanged.listen((pos) async { if (!isLoaded) return; await linuxService?.player.updateProperties(); final currentDuration = await audioPlayer.getDuration() ?? 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.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(); tracks[state!.active + 1] = await SpotubeTrack.fetchFromTrack( state!.tracks.elementAt(state!.active + 1), preferences, ); 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; // redirectors static bool get isPlaying => audioPlayer.state == PlayerState.playing; static bool get isPaused => audioPlayer.state == PlayerState.paused; static bool get isStopped => audioPlayer.state == PlayerState.stopped; static Stream get duration => audioPlayer.onDurationChanged.asBroadcastStream(); static Stream get position => audioPlayer.onPositionChanged.asBroadcastStream(); static Stream get playing => audioPlayer.onPlayerStateChanged .map((event) => event == PlayerState.playing) .asBroadcastStream(); List