diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 4a23cc48..81ccffdb 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -75,12 +76,12 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { final mounted = useIsMounted(); + final scrobblerNotifier = ref.read(scrobblerProvider.notifier); + final toggleTrackLike = useMutations.track.toggleFavorite( ref, track.id!, onMutate: (isLiked) { - print("Toggle Like onMutate: $isLiked"); - if (isLiked) { savedTracks.setData( savedTracks.data @@ -98,12 +99,15 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { } return isLiked; }, - onData: (data, recoveryData) async { - print("Toggle Like onData: $data"); + onData: (isLiked, recoveryData) async { await savedTracks.refresh(); + if (isLiked) { + await scrobblerNotifier.love(track); + } else { + await scrobblerNotifier.unlove(track); + } }, onError: (payload, isLiked) { - print("Toggle Like onError: $payload"); if (!mounted()) return; if (isLiked != true) { diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index be01978e..ed9552db 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -19,6 +19,7 @@ import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; +import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -52,6 +53,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier final Ref ref; late final AudioServices notificationService; + ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); UserPreferences get preferences => ref.read(userPreferencesProvider); YoutubeEndpoints get youtube => ref.read(youtubeProvider); ProxyPlaylist get playlist => state; @@ -196,12 +198,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } - final (source: _, :segments) = currentSegments.value!; - // skipping in first 2 second breaks stream - if (segments.isEmpty || position < const Duration(seconds: 3)) return; + if (currentSegments.value == null || + currentSegments.value!.segments.isEmpty || + position < const Duration(seconds: 3)) return; - for (final segment in segments) { + for (final segment in currentSegments.value!.segments) { if (position.inSeconds >= segment.start && position.inSeconds < segment.end) { await audioPlayer.seek(Duration(seconds: segment.end)); @@ -607,12 +609,30 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier @override set state(state) { + final hasActiveTrackChanged = super.state.activeTrack is SpotubeTrack + ? state.activeTrack?.id != super.state.activeTrack?.id + : super.state.activeTrack is LocalTrack && + state.activeTrack is LocalTrack + ? (super.state.activeTrack as LocalTrack).path != + (state.activeTrack as LocalTrack).path + : super.state.activeTrack?.id != state.activeTrack?.id; + + final oldTrack = super.state.activeTrack; + super.state = state; if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { ref.read(paletteProvider.notifier).state = null; } else { updatePalette(); } + audioPlayer.position.then((position) { + final isMoreThan30secs = position != null && + (position == Duration.zero || position.inSeconds > 30); + + if (hasActiveTrackChanged && oldTrack != null && isMoreThan30secs) { + scrobbler.scrobble(oldTrack); + } + }); } @override diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart index 1393e21e..a41f722f 100644 --- a/lib/provider/scrobbler_provider.dart +++ b/lib/provider/scrobbler_provider.dart @@ -1,9 +1,13 @@ import 'dart:async'; +import 'package:catcher/catcher.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/collections/env.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; class ScrobblerState { final String username; @@ -28,9 +32,30 @@ class ScrobblerState { class ScrobblerNotifier extends PersistedStateNotifier { final Scrobblenaut? scrobblenaut; + /// Directly scrobbling in set state of [ProxyPlaylistNotifier] + /// brings extra latency in playback + final StreamController _scrobbleController = + StreamController.broadcast(); + ScrobblerNotifier() : scrobblenaut = null, - super(null, "scrobbler", encrypted: true); + super(null, "scrobbler", encrypted: true) { + _scrobbleController.stream.listen((track) async { + try { + await state?.scrobblenaut.track.scrobble( + artist: TypeConversionUtils.artists_X_String(track.artists!), + track: track.name!, + album: track.album!.name!, + chosenByUser: true, + duration: track.duration, + timestamp: DateTime.now().toUtc(), + trackNumber: track.trackNumber, + ); + } catch (e, stackTrace) { + Catcher.reportCheckedError(e, stackTrace); + } + }); + } Future login( String username, @@ -54,6 +79,24 @@ class ScrobblerNotifier extends PersistedStateNotifier { state = null; } + void scrobble(Track track) { + _scrobbleController.add(track); + } + + Future love(Track track) async { + await state?.scrobblenaut.track.love( + artist: TypeConversionUtils.artists_X_String(track.artists!), + track: track.name!, + ); + } + + Future unlove(Track track) async { + await state?.scrobblenaut.track.unLove( + artist: TypeConversionUtils.artists_X_String(track.artists!), + track: track.name!, + ); + } + @override FutureOr fromJson(Map json) async { if (json.isEmpty) {