mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-10 17:07:30 +00:00
feat: add lastfm scrobbling support
This commit is contained in:
parent
20bb28beb3
commit
6d05379d03
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user