Compare commits

..

1 Commits

Author SHA1 Message Date
DEBAJYOTI GHOSH
2ef8bbc5ca
Merge 42dff3e7b3 into 723b6b1f38 2025-03-16 01:23:26 +00:00
38 changed files with 214 additions and 361 deletions

View File

@ -1,10 +1,6 @@
# Changelog # Changelog
## [4.0.2](https://github.com/krtirtho/spotube/compare/v4.0.1...v4.0.2) (2025-03-16) All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### Bug Fixes
- invalid access token exception #2525
## [4.0.1](https://github.com/krtirtho/spotube/compare/v4.0.0...v4.0.1) (2025-03-15) ## [4.0.1](https://github.com/krtirtho/spotube/compare/v4.0.0...v4.0.1) (2025-03-15)

View File

@ -30,6 +30,7 @@ import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -121,9 +122,8 @@ class TrackOptions extends HookConsumerWidget {
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider); final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio"; final query = "${track.name} Radio";
final pages = await spotify.invoke( final pages =
(api) => api.search.get(query, types: [SearchType.playlist]).first(), await spotify.search.get(query, types: [SearchType.playlist]).first();
);
final radios = pages final radios = pages
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? []) .expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
@ -165,9 +165,8 @@ class TrackOptions extends HookConsumerWidget {
await playback.addTrack(track); await playback.addTrack(track);
} }
final tracks = await spotify.invoke( final tracks =
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all(), await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
);
await playback.addTracks( await playback.addTracks(
tracks.toList() tracks.toList()

View File

@ -191,7 +191,8 @@ class TrackTile extends HookConsumerWidget {
const SizedBox( const SizedBox(
width: 26, width: 26,
height: 26, height: 26,
child: CircularProgressIndicator(), child:
CircularProgressIndicator(size: 1.5),
), ),
(_, _, true, _, _) => Icon( (_, _, true, _, _) => Icon(
SpotubeIcons.pause, SpotubeIcons.pause,

View File

@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
@ -27,9 +27,7 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
switch (url.pathSegments.first) { switch (url.pathSegments.first) {
case "album": case "album":
final album = await spotify.invoke((api) { final album = await spotify.albums.get(url.pathSegments.last);
return api.albums.get(url.pathSegments.last);
});
router.navigate( router.navigate(
AlbumRoute(id: album.id!, album: album), AlbumRoute(id: album.id!, album: album),
); );
@ -38,9 +36,7 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
router.navigate(ArtistRoute(artistId: url.pathSegments.last)); router.navigate(ArtistRoute(artistId: url.pathSegments.last));
break; break;
case "playlist": case "playlist":
final playlist = await spotify.invoke((api) { final playlist = await spotify.playlists.get(url.pathSegments.last);
return api.playlists.get(url.pathSegments.last);
});
router router
.navigate(PlaylistRoute(id: playlist.id!, playlist: playlist)); .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
break; break;
@ -69,9 +65,7 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
switch (startSegment) { switch (startSegment) {
case "spotify:album": case "spotify:album":
final album = await spotify.invoke((api) { final album = await spotify.albums.get(endSegment);
return api.albums.get(endSegment);
});
await router.navigate( await router.navigate(
AlbumRoute(id: album.id!, album: album), AlbumRoute(id: album.id!, album: album),
); );
@ -83,9 +77,7 @@ void useDeepLinking(WidgetRef ref, AppRouter router) {
await router.navigate(TrackRoute(trackId: endSegment)); await router.navigate(TrackRoute(trackId: endSegment));
break; break;
case "spotify:playlist": case "spotify:playlist":
final playlist = await spotify.invoke((api) { final playlist = await spotify.playlists.get(endSegment);
return api.playlists.get(endSegment);
});
await router.navigate( await router.navigate(
PlaylistRoute(id: playlist.id!, playlist: playlist), PlaylistRoute(id: playlist.id!, playlist: playlist),
); );

View File

@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.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_player/audio_player.dart';
@ -28,8 +28,8 @@ void useEndlessPlayback(WidgetRef ref) {
final track = playlist.tracks.last; final track = playlist.tracks.last;
final query = "${track.name} Radio"; final query = "${track.name} Radio";
final pages = await spotify.invoke((api) => final pages = await spotify.search
api.search.get(query, types: [SearchType.playlist]).first()); .get(query, types: [SearchType.playlist]).first();
final radios = pages final radios = pages
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[]) .expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
@ -50,8 +50,8 @@ void useEndlessPlayback(WidgetRef ref) {
orElse: () => radios.first, orElse: () => radios.first,
); );
final tracks = await spotify.invoke( final tracks =
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all()); await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
await playback.addTracks( await playback.addTracks(
tracks.toList() tracks.toList()

View File

@ -8,7 +8,7 @@ import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart';
class FriendItem extends HookConsumerWidget { class FriendItem extends HookConsumerWidget {
final SpotifyFriendActivity friend; final SpotifyFriendActivity friend;
@ -95,9 +95,8 @@ class FriendItem extends HookConsumerWidget {
text: " ${friend.track.album.name}", text: " ${friend.track.album.name}",
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
final album = await spotify.invoke( final album =
(api) => api.albums.get(friend.track.album.id), await spotify.albums.get(friend.track.album.id);
);
if (context.mounted) { if (context.mounted) {
context.navigateTo( context.navigateTo(
AlbumRoute(id: album.id!, album: album), AlbumRoute(id: album.id!, album: album),

View File

@ -19,6 +19,7 @@ import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
class PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialog extends HookConsumerWidget {
/// Track ids to add to the playlist /// Track ids to add to the playlist
@ -259,7 +260,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
class PlaylistCreateDialogButton extends HookConsumerWidget { class PlaylistCreateDialogButton extends HookConsumerWidget {
const PlaylistCreateDialogButton({super.key}); const PlaylistCreateDialogButton({super.key});
showPlaylistDialog(BuildContext context, SpotifyApiWrapper spotify) { showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
showDialog( showDialog(
context: context, context: context,
alignment: Alignment.center, alignment: Alignment.center,

View File

@ -22,6 +22,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@ -69,8 +70,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
leftSeedCount, leftSeedCount,
context.l10n.artists, context.l10n.artists,
)), )),
fetchSeeds: (textEditingValue) => spotify.invoke( fetchSeeds: (textEditingValue) => spotify.search
(api) => api.search
.get( .get(
textEditingValue.text, textEditingValue.text,
types: [SearchType.artist], types: [SearchType.artist],
@ -86,7 +86,6 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
) )
.toList(), .toList(),
), ),
),
autocompleteOptionBuilder: (option, onSelected) => ButtonTile( autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
leading: Avatar( leading: Avatar(
initials: "O", initials: "O",
@ -147,8 +146,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
leftSeedCount, leftSeedCount,
context.l10n.tracks, context.l10n.tracks,
)), )),
fetchSeeds: (textEditingValue) => spotify.invoke( fetchSeeds: (textEditingValue) => spotify.search
(api) => api.search
.get( .get(
textEditingValue.text, textEditingValue.text,
types: [SearchType.track], types: [SearchType.track],
@ -164,7 +162,6 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
) )
.toList(), .toList(),
), ),
),
autocompleteOptionBuilder: (option, onSelected) => ButtonTile( autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
leading: Avatar( leading: Avatar(
initials: option.name!.substring(0, 1), initials: option.name!.substring(0, 1),

View File

@ -15,7 +15,6 @@ import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:otp_util/otp_util.dart'; import 'package:otp_util/otp_util.dart';
// ignore: implementation_imports // ignore: implementation_imports
@ -198,34 +197,6 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
); );
} }
Future<Response> getToken({
required String totp,
required int timestamp,
String mode = "transport",
String? spDc,
}) async {
assert(mode == "transport" || mode == "init");
final accessTokenUrl = Uri.parse(
"https://open.spotify.com/get_access_token?reason=$mode&productType=web-player"
"&totp=$totp&totpVer=5&ts=$timestamp",
);
final res = await dio.getUri(
accessTokenUrl,
options: Options(
headers: {
"Cookie": spDc ?? "",
"User-Agent": ServiceUtils.randomUserAgent(
kIsDesktop ? UserAgentDevice.desktop : UserAgentDevice.mobile,
),
},
),
);
return res;
}
Future<AuthenticationTableCompanion> credentialsFromCookie( Future<AuthenticationTableCompanion> credentialsFromCookie(
String cookie, String cookie,
) async { ) async {
@ -236,34 +207,24 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
?.trim(); ?.trim();
final totp = await generateTotp(); final totp = await generateTotp();
final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor();
var res = await getToken( final accessTokenUrl = Uri.parse(
totp: totp, "https://open.spotify.com/get_access_token?reason=transport&productType=web_player"
timestamp: timestamp, "&totp=$totp&totpVer=5&ts=$timestamp",
spDc: spDc,
mode: "transport",
); );
if ((res.data["accessToken"]?.length ?? 0) != 374) { final res = await dio.getUri(
res = await getToken( accessTokenUrl,
totp: totp, options: Options(
timestamp: timestamp, headers: {
spDc: spDc, "Cookie": spDc ?? "",
mode: "init", "User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
},
),
); );
} final body = res.data;
final body = res.data as Map<String, dynamic>;
if (body["accessToken"] == null) {
AppLogger.reportError(
"The access token is only ${body["accessToken"]?.length} characters long instead of 374\n"
"Your authentication probably doesn't work",
StackTrace.current,
);
}
return AuthenticationTableCompanion.insert( return AuthenticationTableCompanion.insert(
id: const Value(0), id: const Value(0),

View File

@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart'; import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart';
final customSpotifyEndpointProvider = Provider<CustomSpotifyEndpoints>((ref) { final customSpotifyEndpointProvider = Provider<CustomSpotifyEndpoints>((ref) {

View File

@ -76,6 +76,8 @@ final localTracksProvider =
final mime = lookupMimeType(e.path) ?? final mime = lookupMimeType(e.path) ??
(extension(e.path) == ".opus" ? "audio/opus" : null); (extension(e.path) == ".opus" ? "audio/opus" : null);
print("${basename(e.path)}: $mime");
return e is File && supportedAudioTypes.contains(mime); return e is File && supportedAudioTypes.contains(mime);
}, },
).cast<File>(), ).cast<File>(),

View File

@ -22,14 +22,11 @@ class FavoriteAlbumState extends PaginatedState<AlbumSimple> {
class FavoriteAlbumNotifier class FavoriteAlbumNotifier
extends PaginatedAsyncNotifier<AlbumSimple, FavoriteAlbumState> { extends PaginatedAsyncNotifier<AlbumSimple, FavoriteAlbumState> {
@override @override
Future<List<AlbumSimple>> fetch(int offset, int limit) async { Future<List<AlbumSimple>> fetch(int offset, int limit) {
return await spotify return spotify.me
.invoke( .savedAlbums()
(api) => api.me.savedAlbums().getPage(limit, offset), .getPage(limit, offset)
) .then((value) => value.items?.toList() ?? []);
.then(
(value) => value.items?.toList() ?? <AlbumSimple>[],
);
} }
@override @override
@ -48,10 +45,8 @@ class FavoriteAlbumNotifier
if (state.value == null) return; if (state.value == null) return;
state = await AsyncValue.guard(() async { state = await AsyncValue.guard(() async {
await spotify.invoke((api) => api.me.saveAlbums(ids)); await spotify.me.saveAlbums(ids);
final albums = await spotify.invoke( final albums = await spotify.albums.list(ids);
(api) => api.albums.list(ids),
);
return state.value!.copyWith( return state.value!.copyWith(
items: [ items: [
@ -70,7 +65,7 @@ class FavoriteAlbumNotifier
if (state.value == null) return; if (state.value == null) return;
state = await AsyncValue.guard(() async { state = await AsyncValue.guard(() async {
await spotify.invoke((api) => api.me.removeAlbums(ids)); await spotify.me.removeAlbums(ids);
return state.value!.copyWith( return state.value!.copyWith(
items: state.value!.items items: state.value!.items

View File

@ -3,10 +3,8 @@ part of '../spotify.dart';
final albumsIsSavedProvider = FutureProvider.autoDispose.family<bool, String>( final albumsIsSavedProvider = FutureProvider.autoDispose.family<bool, String>(
(ref, albumId) async { (ref, albumId) async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.invoke( return spotify.me.containsSavedAlbums([albumId]).then(
(api) => api.me.containsSavedAlbums([albumId]).then(
(value) => value[albumId] ?? false, (value) => value[albumId] ?? false,
),
); );
}, },
); );

View File

@ -32,9 +32,9 @@ class AlbumReleasesNotifier
fetch(int offset, int limit) async { fetch(int offset, int limit) async {
final market = ref.read(userPreferencesProvider).market; final market = ref.read(userPreferencesProvider).market;
final albums = await spotify.invoke( final albums = await spotify.browse
(api) => api.browse.newReleases(country: market).getPage(limit, offset), .newReleases(country: market)
); .getPage(limit, offset);
return albums.items?.map((album) => album.toAlbum()).toList() ?? []; return albums.items?.map((album) => album.toAlbum()).toList() ?? [];
} }

View File

@ -30,9 +30,7 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final tracks = await spotify.invoke( final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset);
(api) => api.albums.tracks(arg.id!).getPage(limit, offset),
);
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
return ( return (

View File

@ -31,9 +31,9 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final market = ref.read(userPreferencesProvider).market; final market = ref.read(userPreferencesProvider).market;
final albums = await spotify.invoke( final albums = await spotify.artists
(api) => api.artists.albums(arg, country: market).getPage(limit, offset), .albums(arg, country: market)
); .getPage(limit, offset);
final items = albums.items?.toList() ?? []; final items = albums.items?.toList() ?? [];

View File

@ -6,5 +6,5 @@ final artistProvider =
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.invoke((api) => api.artists.get(artistId)); return spotify.artists.get(artistId);
}); });

View File

@ -33,11 +33,9 @@ class FollowedArtistsNotifier
@override @override
fetch(offset, limit) async { fetch(offset, limit) async {
final artists = await spotify.invoke( final artists = await spotify.me.following(FollowingType.artist).getPage(
(api) => api.me.following(FollowingType.artist).getPage(
limit, limit,
offset ?? '', offset ?? '',
),
); );
return (artists.items?.toList() ?? [], artists.after); return (artists.items?.toList() ?? [], artists.after);
@ -57,9 +55,7 @@ class FollowedArtistsNotifier
Future<void> _followArtists(List<String> artistIds) async { Future<void> _followArtists(List<String> artistIds) async {
try { try {
final creds = await spotify.invoke( final creds = await spotify.getCredentials();
(api) => api.getCredentials(),
);
await dio.post( await dio.post(
"https://api-partner.spotify.com/pathfinder/v1/query", "https://api-partner.spotify.com/pathfinder/v1/query",
@ -97,9 +93,7 @@ class FollowedArtistsNotifier
await _followArtists(artistIds); await _followArtists(artistIds);
state = await AsyncValue.guard(() async { state = await AsyncValue.guard(() async {
final artists = await spotify.invoke( final artists = await spotify.artists.list(artistIds);
(api) => api.artists.list(artistIds),
);
return state.value!.copyWith( return state.value!.copyWith(
items: [ items: [
@ -116,9 +110,7 @@ class FollowedArtistsNotifier
Future<void> removeArtists(List<String> artistIds) async { Future<void> removeArtists(List<String> artistIds) async {
if (state.value == null) return; if (state.value == null) return;
await spotify.invoke( await spotify.me.unfollow(FollowingType.artist, artistIds);
(api) => api.me.unfollow(FollowingType.artist, artistIds),
);
state = await AsyncValue.guard(() async { state = await AsyncValue.guard(() async {
final artists = state.value!.items.where((artist) { final artists = state.value!.items.where((artist) {
@ -144,9 +136,7 @@ final followedArtistsProvider =
final allFollowedArtistsProvider = FutureProvider<List<Artist>>( final allFollowedArtistsProvider = FutureProvider<List<Artist>>(
(ref) async { (ref) async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final artists = await spotify.invoke( final artists = await spotify.me.following(FollowingType.artist).all();
(api) => api.me.following(FollowingType.artist).all(),
);
return artists.toList(); return artists.toList();
}, },
); );

View File

@ -3,10 +3,8 @@ part of '../spotify.dart';
final artistIsFollowingProvider = FutureProvider.family( final artistIsFollowingProvider = FutureProvider.family(
(ref, String artistId) async { (ref, String artistId) async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.invoke( return spotify.me.checkFollowing(FollowingType.artist, [artistId]).then(
(api) => api.me.checkFollowing(FollowingType.artist, [artistId]).then(
(value) => value[artistId] ?? false, (value) => value[artistId] ?? false,
),
); );
}, },
); );

View File

@ -5,9 +5,7 @@ final relatedArtistsProvider = FutureProvider.autoDispose
ref.cacheFor(); ref.cacheFor();
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final artists = await spotify.invoke( final artists = await spotify.artists.relatedArtists(artistId);
(api) => api.artists.relatedArtists(artistId),
);
return artists.toList(); return artists.toList();
}); });

View File

@ -7,9 +7,7 @@ final artistTopTracksProvider =
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final market = ref.watch(userPreferencesProvider.select((s) => s.market));
final tracks = await spotify.invoke( final tracks = await spotify.artists.topTracks(artistId, market);
(api) => api.artists.topTracks(artistId, market),
);
return tracks.toList(); return tracks.toList();
}, },

View File

@ -5,16 +5,14 @@ final categoriesProvider = FutureProvider(
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final market = ref.watch(userPreferencesProvider.select((s) => s.market));
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
final categories = await spotify.invoke( final categories = await spotify.categories
(api) => api.categories
.list( .list(
country: market, country: market,
locale: Intl.canonicalizedLocale( locale: Intl.canonicalizedLocale(
locale.toString(), locale.toString(),
), ),
) )
.all(), .all();
);
return categories.toList()..shuffle(); return categories.toList()..shuffle();
}, },

View File

@ -32,7 +32,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final preferences = ref.read(userPreferencesProvider); final preferences = ref.read(userPreferencesProvider);
final playlists = await Pages<PlaylistSimple?>( final playlists = await Pages<PlaylistSimple?>(
spotify.api, spotify,
"v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}", "v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}",
(json) => json == null ? null : PlaylistSimple.fromJson(json), (json) => json == null ? null : PlaylistSimple.fromJson(json),
'playlists', 'playlists',

View File

@ -138,7 +138,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
SubtitleSimple? lyrics = cachedLyrics; SubtitleSimple? lyrics = cachedLyrics;
final token = await spotify.invoke((api) => api.getCredentials()); final token = await spotify.getCredentials();
if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) { if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) {
lyrics = await getSpotifyLyrics(token.accessToken); lyrics = await getSpotifyLyrics(token.accessToken);

View File

@ -30,11 +30,9 @@ class FavoritePlaylistsNotifier
@override @override
fetch(int offset, int limit) async { fetch(int offset, int limit) async {
final playlists = await spotify.invoke( final playlists = await spotify.playlists.me.getPage(
(api) => api.playlists.me.getPage(
limit, limit,
offset, offset,
),
); );
return playlists.items?.toList() ?? []; return playlists.items?.toList() ?? [];
@ -69,9 +67,7 @@ class FavoritePlaylistsNotifier
Future<void> addFavorite(PlaylistSimple playlist) async { Future<void> addFavorite(PlaylistSimple playlist) async {
await update((state) async { await update((state) async {
await spotify.invoke( await spotify.playlists.followPlaylist(playlist.id!);
(api) => api.playlists.followPlaylist(playlist.id!),
);
return state.copyWith( return state.copyWith(
items: [...state.items, playlist], items: [...state.items, playlist],
); );
@ -82,9 +78,7 @@ class FavoritePlaylistsNotifier
Future<void> removeFavorite(PlaylistSimple playlist) async { Future<void> removeFavorite(PlaylistSimple playlist) async {
await update((state) async { await update((state) async {
await spotify.invoke( await spotify.playlists.unfollowPlaylist(playlist.id!);
(api) => api.playlists.unfollowPlaylist(playlist.id!),
);
return state.copyWith( return state.copyWith(
items: state.items.where((e) => e.id != playlist.id).toList(), items: state.items.where((e) => e.id != playlist.id).toList(),
); );
@ -98,11 +92,9 @@ class FavoritePlaylistsNotifier
final spotify = ref.read(spotifyProvider); final spotify = ref.read(spotifyProvider);
await spotify.invoke( await spotify.playlists.addTracks(
(api) => api.playlists.addTracks(
trackIds.map((id) => 'spotify:track:$id').toList(), trackIds.map((id) => 'spotify:track:$id').toList(),
playlistId, playlistId,
),
); );
ref.invalidate(playlistTracksProvider(playlistId)); ref.invalidate(playlistTracksProvider(playlistId));
@ -113,11 +105,9 @@ class FavoritePlaylistsNotifier
final spotify = ref.read(spotifyProvider); final spotify = ref.read(spotifyProvider);
await spotify.invoke( await spotify.playlists.removeTracks(
(api) => api.playlists.removeTracks(
trackIds.map((id) => 'spotify:track:$id').toList(), trackIds.map((id) => 'spotify:track:$id').toList(),
playlistId, playlistId,
),
); );
ref.invalidate(playlistTracksProvider(playlistId)); ref.invalidate(playlistTracksProvider(playlistId));
@ -138,8 +128,8 @@ final isFavoritePlaylistProvider = FutureProvider.family<bool, String>(
return false; return false;
} }
final follows = await spotify final follows =
.invoke((api) => api.playlists.followedByUsers(id, [me.value!.id!])); await spotify.playlists.followedByUsers(id, [me.value!.id!]);
return follows[me.value!.id!] ?? false; return follows[me.value!.id!] ?? false;
}, },

View File

@ -30,8 +30,9 @@ class FeaturedPlaylistsNotifier
@override @override
fetch(int offset, int limit) async { fetch(int offset, int limit) async {
final playlists = await spotify.invoke( final playlists = await spotify.playlists.featured.getPage(
(api) => api.playlists.featured.getPage(limit, offset), limit,
offset,
); );
return playlists.items?.toList() ?? []; return playlists.items?.toList() ?? [];

View File

@ -8,8 +8,7 @@ final generatePlaylistProvider = FutureProvider.autoDispose
userPreferencesProvider.select((s) => s.market), userPreferencesProvider.select((s) => s.market),
); );
final recommendation = await spotify.invoke( final recommendation = await spotify.recommendations
(api) => api.recommendations
.get( .get(
limit: input.limit, limit: input.limit,
seedArtists: input.seedArtists?.toList(), seedArtists: input.seedArtists?.toList(),
@ -27,17 +26,14 @@ final generatePlaylistProvider = FutureProvider.autoDispose
.catchError((e, stackTrace) { .catchError((e, stackTrace) {
AppLogger.reportError(e, stackTrace); AppLogger.reportError(e, stackTrace);
return Recommendations(); return Recommendations();
}), });
);
if (recommendation.tracks?.isEmpty ?? true) { if (recommendation.tracks?.isEmpty ?? true) {
return []; return [];
} }
final tracks = await spotify.invoke( final tracks = await spotify.tracks
(api) => .list(recommendation.tracks!.map((e) => e.id!).toList());
api.tracks.list(recommendation.tracks!.map((e) => e.id!).toList()),
);
return tracks.toList(); return tracks.toList();
}, },

View File

@ -4,9 +4,7 @@ class LikedTracksNotifier extends AsyncNotifier<List<Track>> {
@override @override
FutureOr<List<Track>> build() async { FutureOr<List<Track>> build() async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final savedTracked = await spotify.invoke( final savedTracked = await spotify.tracks.me.saved.all();
(api) => api.tracks.me.saved.all(),
);
return savedTracked.map((e) => e.track!).toList(); return savedTracked.map((e) => e.track!).toList();
} }
@ -19,14 +17,10 @@ class LikedTracksNotifier extends AsyncNotifier<List<Track>> {
final isLiked = tracks.map((e) => e.id).contains(track.id); final isLiked = tracks.map((e) => e.id).contains(track.id);
if (isLiked) { if (isLiked) {
await spotify.invoke( await spotify.tracks.me.removeOne(track.id!);
(api) => api.tracks.me.removeOne(track.id!),
);
return tracks.where((e) => e.id != track.id).toList(); return tracks.where((e) => e.id != track.id).toList();
} else { } else {
await spotify.invoke( await spotify.tracks.me.saveOne(track.id!);
(api) => api.tracks.me.saveOne(track.id!),
);
return [track, ...tracks]; return [track, ...tracks];
} }
}); });

View File

@ -12,9 +12,7 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
@override @override
FutureOr<Playlist> build(String arg) { FutureOr<Playlist> build(String arg) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.invoke( return spotify.playlists.get(arg);
(api) => api.playlists.get(arg),
);
} }
Future<void> create(PlaylistInput input, [ValueChanged? onError]) async { Future<void> create(PlaylistInput input, [ValueChanged? onError]) async {
@ -28,22 +26,18 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
state = await AsyncValue.guard(() async { state = await AsyncValue.guard(() async {
try { try {
final playlist = await spotify.invoke( final playlist = await spotify.playlists.createPlaylist(
(api) => api.playlists.createPlaylist(
me.value!.id!, me.value!.id!,
input.playlistName, input.playlistName,
collaborative: input.collaborative, collaborative: input.collaborative,
description: input.description, description: input.description,
public: input.public, public: input.public,
),
); );
if (input.base64Image != null) { if (input.base64Image != null) {
await spotify.invoke( await spotify.playlists.updatePlaylistImage(
(api) => api.playlists.updatePlaylistImage(
playlist.id!, playlist.id!,
input.base64Image!, input.base64Image!,
),
); );
} }
@ -64,27 +58,21 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
await update((state) async { await update((state) async {
try { try {
await spotify.invoke( await spotify.playlists.updatePlaylist(
(api) => api.playlists.updatePlaylist(
state.id!, state.id!,
input.playlistName, input.playlistName,
collaborative: input.collaborative, collaborative: input.collaborative,
description: input.description, description: input.description,
public: input.public, public: input.public,
),
); );
if (input.base64Image != null) { if (input.base64Image != null) {
await spotify.invoke( await spotify.playlists.updatePlaylistImage(
(api) => api.playlists.updatePlaylistImage(
state.id!, state.id!,
input.base64Image!, input.base64Image!,
),
); );
final playlist = await spotify.invoke( final playlist = await spotify.playlists.get(state.id!);
(api) => api.playlists.get(state.id!),
);
ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist); ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist);
return playlist; return playlist;
@ -117,11 +105,9 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
final spotify = ref.read(spotifyProvider); final spotify = ref.read(spotifyProvider);
await spotify.invoke( await spotify.playlists.addTracks(
(api) => api.playlists.addTracks(
trackIds.map((id) => "spotify:track:$id").toList(), trackIds.map((id) => "spotify:track:$id").toList(),
state.value!.id!, state.value!.id!,
),
); );
} catch (e, stack) { } catch (e, stack) {
onError?.call(e); onError?.call(e);

View File

@ -30,9 +30,9 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final tracks = await spotify.invoke( final tracks = await spotify.playlists
(api) => api.playlists.getTracksByPlaylistId(arg).getPage(limit, offset), .getTracksByPlaylistId(arg)
); .getPage(limit, offset);
/// Filter out tracks with null id because some personal playlists /// Filter out tracks with null id because some personal playlists
/// may contain local tracks that are not available in the Spotify catalog /// may contain local tracks that are not available in the Spotify catalog

View File

@ -44,15 +44,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
nextOffset: 0, nextOffset: 0,
); );
} }
final results = await spotify.invoke( final results = await spotify.search
(api) => api.search
.get( .get(
ref.read(searchTermStateProvider), ref.read(searchTermStateProvider),
types: [arg], types: [arg],
market: ref.read(userPreferencesProvider).market, market: ref.read(userPreferencesProvider).market,
) )
.getPage(limit, offset), .getPage(limit, offset);
);
final items = results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>(); final items = results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();

View File

@ -5,7 +5,6 @@ import 'dart:math';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
@ -26,10 +25,10 @@ import 'package:spotube/models/lyrics.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/wikipedia/wikipedia.dart'; import 'package:spotube/services/wikipedia/wikipedia.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:wikipedia_api/wikipedia_api.dart'; import 'package:wikipedia_api/wikipedia_api.dart';
@ -77,57 +76,3 @@ part 'utils/provider/paginated.dart';
part 'utils/provider/cursor.dart'; part 'utils/provider/cursor.dart';
part 'utils/provider/paginated_family.dart'; part 'utils/provider/paginated_family.dart';
part 'utils/provider/cursor_family.dart'; part 'utils/provider/cursor_family.dart';
class SpotifyApiWrapper {
final SpotifyApi api;
final Ref ref;
SpotifyApiWrapper(
this.ref,
this.api,
);
bool _isRefreshing = false;
FutureOr<T> invoke<T>(
FutureOr<T> Function(SpotifyApi api) fn,
) async {
try {
return await fn(api);
} catch (e) {
if (((e is AuthorizationException && e.error == 'invalid_token') ||
e is ExpirationException) &&
!_isRefreshing) {
_isRefreshing = true;
await ref.read(authenticationProvider.notifier).refreshCredentials();
_isRefreshing = false;
return await fn(api);
}
rethrow;
}
}
}
final spotifyProvider = Provider<SpotifyApiWrapper>(
(ref) {
final authState = ref.watch(authenticationProvider);
final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets);
final wrapper = SpotifyApiWrapper(
ref,
authState.asData?.value == null
? SpotifyApi(
SpotifyApiCredentials(
anonCred["clientId"],
anonCred["clientSecret"],
),
)
: SpotifyApi.withAccessToken(
authState.asData!.value!.accessToken.value,
),
);
return wrapper;
},
);

View File

@ -6,5 +6,5 @@ final trackProvider =
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.invoke((api) => api.tracks.get(id)); return spotify.tracks.get(id);
}); });

View File

@ -2,5 +2,5 @@ part of '../spotify.dart';
final meProvider = FutureProvider<User>((ref) async { final meProvider = FutureProvider<User>((ref) async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.invoke((api) => api.me.get()); return spotify.me.get();
}); });

View File

@ -2,7 +2,7 @@ part of '../spotify.dart';
// ignore: invalid_use_of_internal_member // ignore: invalid_use_of_internal_member
mixin SpotifyMixin<T> on AsyncNotifierBase<T> { mixin SpotifyMixin<T> on AsyncNotifierBase<T> {
SpotifyApiWrapper get spotify => ref.read(spotifyProvider); SpotifyApi get spotify => ref.read(spotifyProvider);
} }
extension on AutoDisposeAsyncNotifierProviderRef { extension on AutoDisposeAsyncNotifierProviderRef {

View File

@ -0,0 +1,22 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/primitive_utils.dart';
final spotifyProvider = Provider<SpotifyApi>((ref) {
final authState = ref.watch(authenticationProvider);
final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets);
if (authState.asData?.value == null) {
return SpotifyApi(
SpotifyApiCredentials(
anonCred["clientId"],
anonCred["clientSecret"],
),
);
}
return SpotifyApi.withAccessToken(authState.asData!.value!.accessToken.value);
});

View File

@ -42,10 +42,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: app_links name: app_links
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.0" version: "6.3.2"
app_links_linux: app_links_linux:
dependency: transitive dependency: transitive
description: description:

View File

@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El
publish_to: "none" publish_to: "none"
version: 4.0.2+41 version: 4.0.1+40
homepage: https://spotube.krtirtho.dev homepage: https://spotube.krtirtho.dev
repository: https://github.com/KRTirtho/spotube repository: https://github.com/KRTirtho/spotube
@ -13,7 +13,7 @@ environment:
flutter: ">=3.29.0" flutter: ">=3.29.0"
dependencies: dependencies:
app_links: ^6.4.0 app_links: ^6.3.2
args: ^2.5.0 args: ^2.5.0
async: ^2.11.0 async: ^2.11.0
audio_service: ^0.18.13 audio_service: ^0.18.13