import 'dart:async'; import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/skip_segments/skip_segments.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/user_preferences/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/services/logger/logger.dart'; class AudioPlayerStreamListeners { final Ref ref; late final AudioServices notificationService; AudioPlayerStreamListeners(this.ref) { AudioServices.create(ref, ref.read(audioPlayerProvider.notifier)).then( (value) => notificationService = value, ); final subscriptions = [ subscribeToPlaylist(), subscribeToSkipSponsor(), subscribeToScrobbleChanged(), subscribeToPosition(), subscribeToPlayerError(), ]; ref.onDispose(() { for (final subscription in subscriptions) { subscription.cancel(); } }); } ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); UserPreferences get preferences => ref.read(userPreferencesProvider); DiscordNotifier get discord => ref.read(discordProvider.notifier); AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider); PlaybackHistoryActions get history => ref.read(playbackHistoryActionsProvider); StreamSubscription subscribeToPlaylist() { return audioPlayer.playlistStream.listen((mpvPlaylist) { try { if (audioPlayerState.activeTrack == null) return; notificationService.addTrack(audioPlayerState.activeTrack!); discord.updatePresence(audioPlayerState.activeTrack!); } catch (e, stack) { AppLogger.reportError(e, stack); } }); } StreamSubscription subscribeToSkipSponsor() { return audioPlayer.positionStream.listen((position) async { try { final currentSegments = await ref.read(segmentProvider.future); if (currentSegments?.segments.isNotEmpty != true || position < const Duration(seconds: 3)) { return; } for (final segment in currentSegments!.segments) { final seconds = position.inSeconds; if (seconds < segment.start || seconds >= segment.end) continue; await audioPlayer.seek(Duration(seconds: segment.end + 1)); } } catch (e, stack) { AppLogger.reportError(e, stack); } }); } StreamSubscription subscribeToScrobbleChanged() { String? lastScrobbled; return audioPlayer.positionStream.listen((position) async { try { final uid = audioPlayerState.activeTrack is SpotubeLocalTrackObject ? (audioPlayerState.activeTrack as SpotubeLocalTrackObject).path : audioPlayerState.activeTrack?.id; if (audioPlayerState.activeTrack == null || lastScrobbled == uid || position.inSeconds < 30) { return; } scrobbler.scrobble(audioPlayerState.activeTrack!); lastScrobbled = uid; /// The [Track] from Playlist.getTracks doesn't contain artist images /// so we need to fetch them from the API var activeTrack = audioPlayerState.activeTrack!; if (activeTrack.artists.any((a) => a.images == null)) { final metadataPlugin = await ref.read(metadataPluginProvider.future); final artists = await Future.wait( activeTrack.artists .map((artist) => metadataPlugin!.artist.getArtist(artist.id)), ); activeTrack = activeTrack.copyWith( artists: artists .map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson())) .toList(), ); } await history.addTrack(activeTrack); } catch (e, stack) { AppLogger.reportError(e, stack); } }); } StreamSubscription subscribeToPosition() { String lastTrack = ""; // used to prevent multiple calls to the same track return audioPlayer.positionStream.listen((event) async { final percentProgress = (event.inSeconds / max(audioPlayer.duration.inSeconds, 1)) * 100; try { if (percentProgress < 80 || audioPlayerState.currentIndex == -1 || audioPlayerState.currentIndex == audioPlayerState.tracks.length - 1) { return; } final nextTrack = audioPlayerState.tracks .elementAtOrNull(audioPlayerState.currentIndex + 1); if (nextTrack == null || lastTrack == nextTrack.id || nextTrack is SpotubeLocalTrackObject) { return; } try { await ref.read( trackSourcesProvider( TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject), ).future, ); } finally { lastTrack = nextTrack.id; } } catch (e, stack) { AppLogger.reportError(e, stack); } }); } StreamSubscription subscribeToPlayerError() { return audioPlayer.errorStream.listen((event) {}); } } final audioPlayerStreamListenersProvider = Provider(AudioPlayerStreamListeners.new);