feat: add lastfm scrobbling support

This commit is contained in:
Kingkor Roy Tirtho 2023-09-29 14:58:35 +06:00
parent 20bb28beb3
commit 6d05379d03
3 changed files with 77 additions and 10 deletions

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.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/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -75,12 +76,12 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
final mounted = useIsMounted(); final mounted = useIsMounted();
final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
final toggleTrackLike = useMutations.track.toggleFavorite( final toggleTrackLike = useMutations.track.toggleFavorite(
ref, ref,
track.id!, track.id!,
onMutate: (isLiked) { onMutate: (isLiked) {
print("Toggle Like onMutate: $isLiked");
if (isLiked) { if (isLiked) {
savedTracks.setData( savedTracks.setData(
savedTracks.data savedTracks.data
@ -98,12 +99,15 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
} }
return isLiked; return isLiked;
}, },
onData: (data, recoveryData) async { onData: (isLiked, recoveryData) async {
print("Toggle Like onData: $data");
await savedTracks.refresh(); await savedTracks.refresh();
if (isLiked) {
await scrobblerNotifier.love(track);
} else {
await scrobblerNotifier.unlove(track);
}
}, },
onError: (payload, isLiked) { onError: (payload, isLiked) {
print("Toggle Like onError: $payload");
if (!mounted()) return; if (!mounted()) return;
if (isLiked != true) { if (isLiked != true) {

View File

@ -19,6 +19,7 @@ import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/palette_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/next_fetcher_mixin.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.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/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/provider/youtube_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
@ -52,6 +53,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
final Ref ref; final Ref ref;
late final AudioServices notificationService; late final AudioServices notificationService;
ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier);
UserPreferences get preferences => ref.read(userPreferencesProvider); UserPreferences get preferences => ref.read(userPreferencesProvider);
YoutubeEndpoints get youtube => ref.read(youtubeProvider); YoutubeEndpoints get youtube => ref.read(youtubeProvider);
ProxyPlaylist get playlist => state; ProxyPlaylist get playlist => state;
@ -196,12 +198,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
} }
} }
final (source: _, :segments) = currentSegments.value!;
// skipping in first 2 second breaks stream // 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 && if (position.inSeconds >= segment.start &&
position.inSeconds < segment.end) { position.inSeconds < segment.end) {
await audioPlayer.seek(Duration(seconds: segment.end)); await audioPlayer.seek(Duration(seconds: segment.end));
@ -607,12 +609,30 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
@override @override
set state(state) { 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; super.state = state;
if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { if (state.tracks.isEmpty && ref.read(paletteProvider) != null) {
ref.read(paletteProvider.notifier).state = null; ref.read(paletteProvider.notifier).state = null;
} else { } else {
updatePalette(); updatePalette();
} }
audioPlayer.position.then((position) {
final isMoreThan30secs = position != null &&
(position == Duration.zero || position.inSeconds > 30);
if (hasActiveTrackChanged && oldTrack != null && isMoreThan30secs) {
scrobbler.scrobble(oldTrack);
}
});
} }
@override @override

View File

@ -1,9 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:catcher/catcher.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:scrobblenaut/scrobblenaut.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.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/persisted_state_notifier.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class ScrobblerState { class ScrobblerState {
final String username; final String username;
@ -28,9 +32,30 @@ class ScrobblerState {
class ScrobblerNotifier extends PersistedStateNotifier<ScrobblerState?> { class ScrobblerNotifier extends PersistedStateNotifier<ScrobblerState?> {
final Scrobblenaut? scrobblenaut; final Scrobblenaut? scrobblenaut;
/// Directly scrobbling in set state of [ProxyPlaylistNotifier]
/// brings extra latency in playback
final StreamController<Track> _scrobbleController =
StreamController<Track>.broadcast();
ScrobblerNotifier() ScrobblerNotifier()
: scrobblenaut = null, : 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<void> login( Future<void> login(
String username, String username,
@ -54,6 +79,24 @@ class ScrobblerNotifier extends PersistedStateNotifier<ScrobblerState?> {
state = null; state = null;
} }
void scrobble(Track track) {
_scrobbleController.add(track);
}
Future<void> love(Track track) async {
await state?.scrobblenaut.track.love(
artist: TypeConversionUtils.artists_X_String(track.artists!),
track: track.name!,
);
}
Future<void> unlove(Track track) async {
await state?.scrobblenaut.track.unLove(
artist: TypeConversionUtils.artists_X_String(track.artists!),
track: track.name!,
);
}
@override @override
FutureOr<ScrobblerState?> fromJson(Map<String, dynamic> json) async { FutureOr<ScrobblerState?> fromJson(Map<String, dynamic> json) async {
if (json.isEmpty) { if (json.isEmpty) {