diff --git a/CHANGELOG.md b/CHANGELOG.md index 90535cb3..bf906a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -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. +## [4.0.2](https://github.com/krtirtho/spotube/compare/v4.0.1...v4.0.2) (2025-03-16) + +### 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) diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 10a43c71..05e67d02 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -30,7 +30,6 @@ import 'package:spotube/provider/download_manager_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/spotify/spotify.dart'; -import 'package:spotube/provider/spotify_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -122,8 +121,9 @@ class TrackOptions extends HookConsumerWidget { final playlist = ref.read(audioPlayerProvider); final spotify = ref.read(spotifyProvider); final query = "${track.name} Radio"; - final pages = - await spotify.search.get(query, types: [SearchType.playlist]).first(); + final pages = await spotify.invoke( + (api) => api.search.get(query, types: [SearchType.playlist]).first(), + ); final radios = pages .expand((e) => e.items?.cast().toList() ?? []) @@ -165,8 +165,9 @@ class TrackOptions extends HookConsumerWidget { await playback.addTrack(track); } - final tracks = - await spotify.playlists.getTracksByPlaylistId(radio.id!).all(); + final tracks = await spotify.invoke( + (api) => api.playlists.getTracksByPlaylistId(radio.id!).all(), + ); await playback.addTracks( tracks.toList() diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 9bb300f4..524575e5 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -191,8 +191,7 @@ class TrackTile extends HookConsumerWidget { const SizedBox( width: 26, height: 26, - child: - CircularProgressIndicator(size: 1.5), + child: CircularProgressIndicator(), ), (_, _, true, _, _) => Icon( SpotubeIcons.pause, diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index 67000d49..a141a21d 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.gr.dart'; -import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -27,7 +27,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) { switch (url.pathSegments.first) { case "album": - final album = await spotify.albums.get(url.pathSegments.last); + final album = await spotify.invoke((api) { + return api.albums.get(url.pathSegments.last); + }); router.navigate( AlbumRoute(id: album.id!, album: album), ); @@ -36,7 +38,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) { router.navigate(ArtistRoute(artistId: url.pathSegments.last)); break; case "playlist": - final playlist = await spotify.playlists.get(url.pathSegments.last); + final playlist = await spotify.invoke((api) { + return api.playlists.get(url.pathSegments.last); + }); router .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist)); break; @@ -65,7 +69,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) { switch (startSegment) { case "spotify:album": - final album = await spotify.albums.get(endSegment); + final album = await spotify.invoke((api) { + return api.albums.get(endSegment); + }); await router.navigate( AlbumRoute(id: album.id!, album: album), ); @@ -77,7 +83,9 @@ void useDeepLinking(WidgetRef ref, AppRouter router) { await router.navigate(TrackRoute(trackId: endSegment)); break; case "spotify:playlist": - final playlist = await spotify.playlists.get(endSegment); + final playlist = await spotify.invoke((api) { + return api.playlists.get(endSegment); + }); await router.navigate( PlaylistRoute(id: playlist.id!, playlist: playlist), ); diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index e2fb1e6e..b86a4865 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -28,8 +28,8 @@ void useEndlessPlayback(WidgetRef ref) { final track = playlist.tracks.last; final query = "${track.name} Radio"; - final pages = await spotify.search - .get(query, types: [SearchType.playlist]).first(); + final pages = await spotify.invoke((api) => + api.search.get(query, types: [SearchType.playlist]).first()); final radios = pages .expand((e) => e.items?.toList() ?? []) @@ -50,8 +50,8 @@ void useEndlessPlayback(WidgetRef ref) { orElse: () => radios.first, ); - final tracks = - await spotify.playlists.getTracksByPlaylistId(radio.id!).all(); + final tracks = await spotify.invoke( + (api) => api.playlists.getTracksByPlaylistId(radio.id!).all()); await playback.addTracks( tracks.toList() diff --git a/lib/modules/home/sections/friends/friend_item.dart b/lib/modules/home/sections/friends/friend_item.dart index 8e91ab66..bf04558f 100644 --- a/lib/modules/home/sections/friends/friend_item.dart +++ b/lib/modules/home/sections/friends/friend_item.dart @@ -8,7 +8,7 @@ import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/models/spotify_friends.dart'; -import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class FriendItem extends HookConsumerWidget { final SpotifyFriendActivity friend; @@ -95,8 +95,9 @@ class FriendItem extends HookConsumerWidget { text: " ${friend.track.album.name}", recognizer: TapGestureRecognizer() ..onTap = () async { - final album = - await spotify.albums.get(friend.track.album.id); + final album = await spotify.invoke( + (api) => api.albums.get(friend.track.album.id), + ); if (context.mounted) { context.navigateTo( AlbumRoute(id: album.id!, album: album), diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index 516182e4..3ee39583 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -19,7 +19,6 @@ import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; -import 'package:spotube/provider/spotify_provider.dart'; class PlaylistCreateDialog extends HookConsumerWidget { /// Track ids to add to the playlist @@ -260,7 +259,7 @@ class PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialogButton extends HookConsumerWidget { const PlaylistCreateDialogButton({super.key}); - showPlaylistDialog(BuildContext context, SpotifyApi spotify) { + showPlaylistDialog(BuildContext context, SpotifyApiWrapper spotify) { showDialog( context: context, alignment: Alignment.center, diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index c0b77452..f1eca306 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -22,7 +22,6 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/spotify/recommendation_seeds.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:auto_route/auto_route.dart'; @@ -70,22 +69,24 @@ class PlaylistGeneratorPage extends HookConsumerWidget { leftSeedCount, context.l10n.artists, )), - fetchSeeds: (textEditingValue) => spotify.search - .get( - textEditingValue.text, - types: [SearchType.artist], - ) - .first(6) - .then( - (v) => List.castFrom( - v.expand((e) => e.items ?? []).toList(), + fetchSeeds: (textEditingValue) => spotify.invoke( + (api) => api.search + .get( + textEditingValue.text, + types: [SearchType.artist], ) - .where( - (element) => - artists.value.none((artist) => element.id == artist.id), - ) - .toList(), - ), + .first(6) + .then( + (v) => List.castFrom( + v.expand((e) => e.items ?? []).toList(), + ) + .where( + (element) => + artists.value.none((artist) => element.id == artist.id), + ) + .toList(), + ), + ), autocompleteOptionBuilder: (option, onSelected) => ButtonTile( leading: Avatar( initials: "O", @@ -146,22 +147,24 @@ class PlaylistGeneratorPage extends HookConsumerWidget { leftSeedCount, context.l10n.tracks, )), - fetchSeeds: (textEditingValue) => spotify.search - .get( - textEditingValue.text, - types: [SearchType.track], - ) - .first(6) - .then( - (v) => List.castFrom( - v.expand((e) => e.items ?? []).toList(), + fetchSeeds: (textEditingValue) => spotify.invoke( + (api) => api.search + .get( + textEditingValue.text, + types: [SearchType.track], ) - .where( - (element) => - tracks.value.none((track) => element.id == track.id), - ) - .toList(), - ), + .first(6) + .then( + (v) => List.castFrom( + v.expand((e) => e.items ?? []).toList(), + ) + .where( + (element) => + tracks.value.none((track) => element.id == track.id), + ) + .toList(), + ), + ), autocompleteOptionBuilder: (option, onSelected) => ButtonTile( leading: Avatar( initials: option.name!.substring(0, 1), diff --git a/lib/provider/authentication/authentication.dart b/lib/provider/authentication/authentication.dart index a1cee311..583955b0 100644 --- a/lib/provider/authentication/authentication.dart +++ b/lib/provider/authentication/authentication.dart @@ -15,6 +15,7 @@ import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/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:otp_util/otp_util.dart'; // ignore: implementation_imports @@ -197,6 +198,34 @@ class AuthenticationNotifier extends AsyncNotifier { ); } + Future 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 credentialsFromCookie( String cookie, ) async { @@ -207,24 +236,34 @@ class AuthenticationNotifier extends AsyncNotifier { ?.trim(); final totp = await generateTotp(); + final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); - final accessTokenUrl = Uri.parse( - "https://open.spotify.com/get_access_token?reason=transport&productType=web_player" - "&totp=$totp&totpVer=5&ts=$timestamp", + var res = await getToken( + totp: totp, + timestamp: timestamp, + spDc: spDc, + mode: "transport", ); - final res = await dio.getUri( - accessTokenUrl, - options: Options( - headers: { - "Cookie": spDc ?? "", - "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; + if ((res.data["accessToken"]?.length ?? 0) != 374) { + res = await getToken( + totp: totp, + timestamp: timestamp, + spDc: spDc, + mode: "init", + ); + } + + final body = res.data as Map; + + 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( id: const Value(0), diff --git a/lib/provider/custom_spotify_endpoint_provider.dart b/lib/provider/custom_spotify_endpoint_provider.dart index ad0c389a..1f36282a 100644 --- a/lib/provider/custom_spotify_endpoint_provider.dart +++ b/lib/provider/custom_spotify_endpoint_provider.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart'; final customSpotifyEndpointProvider = Provider((ref) { diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart index a0e2ecea..b33fd7f6 100644 --- a/lib/provider/local_tracks/local_tracks_provider.dart +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -76,8 +76,6 @@ final localTracksProvider = final mime = lookupMimeType(e.path) ?? (extension(e.path) == ".opus" ? "audio/opus" : null); - print("${basename(e.path)}: $mime"); - return e is File && supportedAudioTypes.contains(mime); }, ).cast(), diff --git a/lib/provider/spotify/album/favorite.dart b/lib/provider/spotify/album/favorite.dart index cf444d49..157ab225 100644 --- a/lib/provider/spotify/album/favorite.dart +++ b/lib/provider/spotify/album/favorite.dart @@ -22,11 +22,14 @@ class FavoriteAlbumState extends PaginatedState { class FavoriteAlbumNotifier extends PaginatedAsyncNotifier { @override - Future> fetch(int offset, int limit) { - return spotify.me - .savedAlbums() - .getPage(limit, offset) - .then((value) => value.items?.toList() ?? []); + Future> fetch(int offset, int limit) async { + return await spotify + .invoke( + (api) => api.me.savedAlbums().getPage(limit, offset), + ) + .then( + (value) => value.items?.toList() ?? [], + ); } @override @@ -45,8 +48,10 @@ class FavoriteAlbumNotifier if (state.value == null) return; state = await AsyncValue.guard(() async { - await spotify.me.saveAlbums(ids); - final albums = await spotify.albums.list(ids); + await spotify.invoke((api) => api.me.saveAlbums(ids)); + final albums = await spotify.invoke( + (api) => api.albums.list(ids), + ); return state.value!.copyWith( items: [ @@ -65,7 +70,7 @@ class FavoriteAlbumNotifier if (state.value == null) return; state = await AsyncValue.guard(() async { - await spotify.me.removeAlbums(ids); + await spotify.invoke((api) => api.me.removeAlbums(ids)); return state.value!.copyWith( items: state.value!.items diff --git a/lib/provider/spotify/album/is_saved.dart b/lib/provider/spotify/album/is_saved.dart index 987ccdf2..aa48dfa0 100644 --- a/lib/provider/spotify/album/is_saved.dart +++ b/lib/provider/spotify/album/is_saved.dart @@ -3,8 +3,10 @@ part of '../spotify.dart'; final albumsIsSavedProvider = FutureProvider.autoDispose.family( (ref, albumId) async { final spotify = ref.watch(spotifyProvider); - return spotify.me.containsSavedAlbums([albumId]).then( - (value) => value[albumId] ?? false, + return spotify.invoke( + (api) => api.me.containsSavedAlbums([albumId]).then( + (value) => value[albumId] ?? false, + ), ); }, ); diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart index 43d2e474..25bb46b4 100644 --- a/lib/provider/spotify/album/releases.dart +++ b/lib/provider/spotify/album/releases.dart @@ -32,9 +32,9 @@ class AlbumReleasesNotifier fetch(int offset, int limit) async { final market = ref.read(userPreferencesProvider).market; - final albums = await spotify.browse - .newReleases(country: market) - .getPage(limit, offset); + final albums = await spotify.invoke( + (api) => api.browse.newReleases(country: market).getPage(limit, offset), + ); return albums.items?.map((album) => album.toAlbum()).toList() ?? []; } diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart index e39abad5..d886d180 100644 --- a/lib/provider/spotify/album/tracks.dart +++ b/lib/provider/spotify/album/tracks.dart @@ -30,7 +30,9 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier api.albums.tracks(arg.id!).getPage(limit, offset), + ); final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; return ( diff --git a/lib/provider/spotify/artist/albums.dart b/lib/provider/spotify/artist/albums.dart index f3fb682f..7852738a 100644 --- a/lib/provider/spotify/artist/albums.dart +++ b/lib/provider/spotify/artist/albums.dart @@ -31,9 +31,9 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< @override fetch(arg, offset, limit) async { final market = ref.read(userPreferencesProvider).market; - final albums = await spotify.artists - .albums(arg, country: market) - .getPage(limit, offset); + final albums = await spotify.invoke( + (api) => api.artists.albums(arg, country: market).getPage(limit, offset), + ); final items = albums.items?.toList() ?? []; diff --git a/lib/provider/spotify/artist/artist.dart b/lib/provider/spotify/artist/artist.dart index c69badd2..dfee03e9 100644 --- a/lib/provider/spotify/artist/artist.dart +++ b/lib/provider/spotify/artist/artist.dart @@ -6,5 +6,5 @@ final artistProvider = final spotify = ref.watch(spotifyProvider); - return spotify.artists.get(artistId); + return spotify.invoke((api) => api.artists.get(artistId)); }); diff --git a/lib/provider/spotify/artist/following.dart b/lib/provider/spotify/artist/following.dart index 31fa0c5c..3a3795b7 100644 --- a/lib/provider/spotify/artist/following.dart +++ b/lib/provider/spotify/artist/following.dart @@ -33,10 +33,12 @@ class FollowedArtistsNotifier @override fetch(offset, limit) async { - final artists = await spotify.me.following(FollowingType.artist).getPage( - limit, - offset ?? '', - ); + final artists = await spotify.invoke( + (api) => api.me.following(FollowingType.artist).getPage( + limit, + offset ?? '', + ), + ); return (artists.items?.toList() ?? [], artists.after); } @@ -55,7 +57,9 @@ class FollowedArtistsNotifier Future _followArtists(List artistIds) async { try { - final creds = await spotify.getCredentials(); + final creds = await spotify.invoke( + (api) => api.getCredentials(), + ); await dio.post( "https://api-partner.spotify.com/pathfinder/v1/query", @@ -93,7 +97,9 @@ class FollowedArtistsNotifier await _followArtists(artistIds); state = await AsyncValue.guard(() async { - final artists = await spotify.artists.list(artistIds); + final artists = await spotify.invoke( + (api) => api.artists.list(artistIds), + ); return state.value!.copyWith( items: [ @@ -110,7 +116,9 @@ class FollowedArtistsNotifier Future removeArtists(List artistIds) async { if (state.value == null) return; - await spotify.me.unfollow(FollowingType.artist, artistIds); + await spotify.invoke( + (api) => api.me.unfollow(FollowingType.artist, artistIds), + ); state = await AsyncValue.guard(() async { final artists = state.value!.items.where((artist) { @@ -136,7 +144,9 @@ final followedArtistsProvider = final allFollowedArtistsProvider = FutureProvider>( (ref) async { final spotify = ref.watch(spotifyProvider); - final artists = await spotify.me.following(FollowingType.artist).all(); + final artists = await spotify.invoke( + (api) => api.me.following(FollowingType.artist).all(), + ); return artists.toList(); }, ); diff --git a/lib/provider/spotify/artist/is_following.dart b/lib/provider/spotify/artist/is_following.dart index db1be184..fb519518 100644 --- a/lib/provider/spotify/artist/is_following.dart +++ b/lib/provider/spotify/artist/is_following.dart @@ -3,8 +3,10 @@ part of '../spotify.dart'; final artistIsFollowingProvider = FutureProvider.family( (ref, String artistId) async { final spotify = ref.watch(spotifyProvider); - return spotify.me.checkFollowing(FollowingType.artist, [artistId]).then( - (value) => value[artistId] ?? false, + return spotify.invoke( + (api) => api.me.checkFollowing(FollowingType.artist, [artistId]).then( + (value) => value[artistId] ?? false, + ), ); }, ); diff --git a/lib/provider/spotify/artist/related.dart b/lib/provider/spotify/artist/related.dart index 317feba3..7246fa11 100644 --- a/lib/provider/spotify/artist/related.dart +++ b/lib/provider/spotify/artist/related.dart @@ -5,7 +5,9 @@ final relatedArtistsProvider = FutureProvider.autoDispose ref.cacheFor(); final spotify = ref.watch(spotifyProvider); - final artists = await spotify.artists.relatedArtists(artistId); + final artists = await spotify.invoke( + (api) => api.artists.relatedArtists(artistId), + ); return artists.toList(); }); diff --git a/lib/provider/spotify/artist/top_tracks.dart b/lib/provider/spotify/artist/top_tracks.dart index a2862c3d..51321b21 100644 --- a/lib/provider/spotify/artist/top_tracks.dart +++ b/lib/provider/spotify/artist/top_tracks.dart @@ -7,7 +7,9 @@ final artistTopTracksProvider = final spotify = ref.watch(spotifyProvider); final market = ref.watch(userPreferencesProvider.select((s) => s.market)); - final tracks = await spotify.artists.topTracks(artistId, market); + final tracks = await spotify.invoke( + (api) => api.artists.topTracks(artistId, market), + ); return tracks.toList(); }, diff --git a/lib/provider/spotify/category/categories.dart b/lib/provider/spotify/category/categories.dart index 6237b64c..67476f34 100644 --- a/lib/provider/spotify/category/categories.dart +++ b/lib/provider/spotify/category/categories.dart @@ -5,14 +5,16 @@ final categoriesProvider = FutureProvider( final spotify = ref.watch(spotifyProvider); final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); - final categories = await spotify.categories - .list( - country: market, - locale: Intl.canonicalizedLocale( - locale.toString(), - ), - ) - .all(); + final categories = await spotify.invoke( + (api) => api.categories + .list( + country: market, + locale: Intl.canonicalizedLocale( + locale.toString(), + ), + ) + .all(), + ); return categories.toList()..shuffle(); }, diff --git a/lib/provider/spotify/category/playlists.dart b/lib/provider/spotify/category/playlists.dart index 79ac7cd2..2afd8d97 100644 --- a/lib/provider/spotify/category/playlists.dart +++ b/lib/provider/spotify/category/playlists.dart @@ -32,7 +32,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final preferences = ref.read(userPreferencesProvider); final playlists = await Pages( - spotify, + spotify.api, "v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}", (json) => json == null ? null : PlaylistSimple.fromJson(json), 'playlists', diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index c6c0d6e3..ff2a73f1 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -138,7 +138,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { SubtitleSimple? lyrics = cachedLyrics; - final token = await spotify.getCredentials(); + final token = await spotify.invoke((api) => api.getCredentials()); if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) { lyrics = await getSpotifyLyrics(token.accessToken); diff --git a/lib/provider/spotify/playlist/favorite.dart b/lib/provider/spotify/playlist/favorite.dart index 000001ad..4df888ce 100644 --- a/lib/provider/spotify/playlist/favorite.dart +++ b/lib/provider/spotify/playlist/favorite.dart @@ -30,9 +30,11 @@ class FavoritePlaylistsNotifier @override fetch(int offset, int limit) async { - final playlists = await spotify.playlists.me.getPage( - limit, - offset, + final playlists = await spotify.invoke( + (api) => api.playlists.me.getPage( + limit, + offset, + ), ); return playlists.items?.toList() ?? []; @@ -67,7 +69,9 @@ class FavoritePlaylistsNotifier Future addFavorite(PlaylistSimple playlist) async { await update((state) async { - await spotify.playlists.followPlaylist(playlist.id!); + await spotify.invoke( + (api) => api.playlists.followPlaylist(playlist.id!), + ); return state.copyWith( items: [...state.items, playlist], ); @@ -78,7 +82,9 @@ class FavoritePlaylistsNotifier Future removeFavorite(PlaylistSimple playlist) async { await update((state) async { - await spotify.playlists.unfollowPlaylist(playlist.id!); + await spotify.invoke( + (api) => api.playlists.unfollowPlaylist(playlist.id!), + ); return state.copyWith( items: state.items.where((e) => e.id != playlist.id).toList(), ); @@ -92,9 +98,11 @@ class FavoritePlaylistsNotifier final spotify = ref.read(spotifyProvider); - await spotify.playlists.addTracks( - trackIds.map((id) => 'spotify:track:$id').toList(), - playlistId, + await spotify.invoke( + (api) => api.playlists.addTracks( + trackIds.map((id) => 'spotify:track:$id').toList(), + playlistId, + ), ); ref.invalidate(playlistTracksProvider(playlistId)); @@ -105,9 +113,11 @@ class FavoritePlaylistsNotifier final spotify = ref.read(spotifyProvider); - await spotify.playlists.removeTracks( - trackIds.map((id) => 'spotify:track:$id').toList(), - playlistId, + await spotify.invoke( + (api) => api.playlists.removeTracks( + trackIds.map((id) => 'spotify:track:$id').toList(), + playlistId, + ), ); ref.invalidate(playlistTracksProvider(playlistId)); @@ -128,8 +138,8 @@ final isFavoritePlaylistProvider = FutureProvider.family( return false; } - final follows = - await spotify.playlists.followedByUsers(id, [me.value!.id!]); + final follows = await spotify + .invoke((api) => api.playlists.followedByUsers(id, [me.value!.id!])); return follows[me.value!.id!] ?? false; }, diff --git a/lib/provider/spotify/playlist/featured.dart b/lib/provider/spotify/playlist/featured.dart index 69057e5d..9f751909 100644 --- a/lib/provider/spotify/playlist/featured.dart +++ b/lib/provider/spotify/playlist/featured.dart @@ -30,9 +30,8 @@ class FeaturedPlaylistsNotifier @override fetch(int offset, int limit) async { - final playlists = await spotify.playlists.featured.getPage( - limit, - offset, + final playlists = await spotify.invoke( + (api) => api.playlists.featured.getPage(limit, offset), ); return playlists.items?.toList() ?? []; diff --git a/lib/provider/spotify/playlist/generate.dart b/lib/provider/spotify/playlist/generate.dart index 0832003e..b2250df6 100644 --- a/lib/provider/spotify/playlist/generate.dart +++ b/lib/provider/spotify/playlist/generate.dart @@ -8,32 +8,36 @@ final generatePlaylistProvider = FutureProvider.autoDispose userPreferencesProvider.select((s) => s.market), ); - final recommendation = await spotify.recommendations - .get( - limit: input.limit, - seedArtists: input.seedArtists?.toList(), - seedGenres: input.seedGenres?.toList(), - seedTracks: input.seedTracks?.toList(), - market: market, - max: (input.max?.toJson()?..removeWhere((key, value) => value == null)) - ?.cast(), - min: (input.min?.toJson()?..removeWhere((key, value) => value == null)) - ?.cast(), - target: (input.target?.toJson() - ?..removeWhere((key, value) => value == null)) - ?.cast(), - ) - .catchError((e, stackTrace) { - AppLogger.reportError(e, stackTrace); - return Recommendations(); - }); + final recommendation = await spotify.invoke( + (api) => api.recommendations + .get( + limit: input.limit, + seedArtists: input.seedArtists?.toList(), + seedGenres: input.seedGenres?.toList(), + seedTracks: input.seedTracks?.toList(), + market: market, + max: (input.max?.toJson()?..removeWhere((key, value) => value == null)) + ?.cast(), + min: (input.min?.toJson()?..removeWhere((key, value) => value == null)) + ?.cast(), + target: (input.target?.toJson() + ?..removeWhere((key, value) => value == null)) + ?.cast(), + ) + .catchError((e, stackTrace) { + AppLogger.reportError(e, stackTrace); + return Recommendations(); + }), + ); if (recommendation.tracks?.isEmpty ?? true) { return []; } - final tracks = await spotify.tracks - .list(recommendation.tracks!.map((e) => e.id!).toList()); + final tracks = await spotify.invoke( + (api) => + api.tracks.list(recommendation.tracks!.map((e) => e.id!).toList()), + ); return tracks.toList(); }, diff --git a/lib/provider/spotify/playlist/liked.dart b/lib/provider/spotify/playlist/liked.dart index 27c3e2b6..99c75719 100644 --- a/lib/provider/spotify/playlist/liked.dart +++ b/lib/provider/spotify/playlist/liked.dart @@ -4,7 +4,9 @@ class LikedTracksNotifier extends AsyncNotifier> { @override FutureOr> build() async { final spotify = ref.watch(spotifyProvider); - final savedTracked = await spotify.tracks.me.saved.all(); + final savedTracked = await spotify.invoke( + (api) => api.tracks.me.saved.all(), + ); return savedTracked.map((e) => e.track!).toList(); } @@ -17,10 +19,14 @@ class LikedTracksNotifier extends AsyncNotifier> { final isLiked = tracks.map((e) => e.id).contains(track.id); if (isLiked) { - await spotify.tracks.me.removeOne(track.id!); + await spotify.invoke( + (api) => api.tracks.me.removeOne(track.id!), + ); return tracks.where((e) => e.id != track.id).toList(); } else { - await spotify.tracks.me.saveOne(track.id!); + await spotify.invoke( + (api) => api.tracks.me.saveOne(track.id!), + ); return [track, ...tracks]; } }); diff --git a/lib/provider/spotify/playlist/playlist.dart b/lib/provider/spotify/playlist/playlist.dart index 28dc8726..34d1fe8e 100644 --- a/lib/provider/spotify/playlist/playlist.dart +++ b/lib/provider/spotify/playlist/playlist.dart @@ -12,7 +12,9 @@ class PlaylistNotifier extends FamilyAsyncNotifier { @override FutureOr build(String arg) { final spotify = ref.watch(spotifyProvider); - return spotify.playlists.get(arg); + return spotify.invoke( + (api) => api.playlists.get(arg), + ); } Future create(PlaylistInput input, [ValueChanged? onError]) async { @@ -26,18 +28,22 @@ class PlaylistNotifier extends FamilyAsyncNotifier { state = await AsyncValue.guard(() async { try { - final playlist = await spotify.playlists.createPlaylist( - me.value!.id!, - input.playlistName, - collaborative: input.collaborative, - description: input.description, - public: input.public, + final playlist = await spotify.invoke( + (api) => api.playlists.createPlaylist( + me.value!.id!, + input.playlistName, + collaborative: input.collaborative, + description: input.description, + public: input.public, + ), ); if (input.base64Image != null) { - await spotify.playlists.updatePlaylistImage( - playlist.id!, - input.base64Image!, + await spotify.invoke( + (api) => api.playlists.updatePlaylistImage( + playlist.id!, + input.base64Image!, + ), ); } @@ -58,21 +64,27 @@ class PlaylistNotifier extends FamilyAsyncNotifier { await update((state) async { try { - await spotify.playlists.updatePlaylist( - state.id!, - input.playlistName, - collaborative: input.collaborative, - description: input.description, - public: input.public, + await spotify.invoke( + (api) => api.playlists.updatePlaylist( + state.id!, + input.playlistName, + collaborative: input.collaborative, + description: input.description, + public: input.public, + ), ); if (input.base64Image != null) { - await spotify.playlists.updatePlaylistImage( - state.id!, - input.base64Image!, + await spotify.invoke( + (api) => api.playlists.updatePlaylistImage( + state.id!, + input.base64Image!, + ), ); - final playlist = await spotify.playlists.get(state.id!); + final playlist = await spotify.invoke( + (api) => api.playlists.get(state.id!), + ); ref.read(favoritePlaylistsProvider.notifier).updatePlaylist(playlist); return playlist; @@ -105,9 +117,11 @@ class PlaylistNotifier extends FamilyAsyncNotifier { final spotify = ref.read(spotifyProvider); - await spotify.playlists.addTracks( - trackIds.map((id) => "spotify:track:$id").toList(), - state.value!.id!, + await spotify.invoke( + (api) => api.playlists.addTracks( + trackIds.map((id) => "spotify:track:$id").toList(), + state.value!.id!, + ), ); } catch (e, stack) { onError?.call(e); diff --git a/lib/provider/spotify/playlist/tracks.dart b/lib/provider/spotify/playlist/tracks.dart index 379ad110..1dbb83be 100644 --- a/lib/provider/spotify/playlist/tracks.dart +++ b/lib/provider/spotify/playlist/tracks.dart @@ -30,9 +30,9 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< @override fetch(arg, offset, limit) async { - final tracks = await spotify.playlists - .getTracksByPlaylistId(arg) - .getPage(limit, offset); + final tracks = await spotify.invoke( + (api) => api.playlists.getTracksByPlaylistId(arg).getPage(limit, offset), + ); /// Filter out tracks with null id because some personal playlists /// may contain local tracks that are not available in the Spotify catalog diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart index 5bbc02e4..828cc382 100644 --- a/lib/provider/spotify/search/search.dart +++ b/lib/provider/spotify/search/search.dart @@ -44,13 +44,15 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier api.search + .get( + ref.read(searchTermStateProvider), + types: [arg], + market: ref.read(userPreferencesProvider).market, + ) + .getPage(limit, offset), + ); final items = results.expand((e) => e.items ?? []).toList().cast(); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index d43e34cd..a0753fcb 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:drift/drift.dart'; import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/collections/env.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/database/database.dart'; @@ -25,10 +26,10 @@ import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/models/spotify_friends.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/services/dio/dio.dart'; import 'package:spotube/services/wikipedia/wikipedia.dart'; +import 'package:spotube/utils/primitive_utils.dart'; import 'package:wikipedia_api/wikipedia_api.dart'; @@ -76,3 +77,57 @@ part 'utils/provider/paginated.dart'; part 'utils/provider/cursor.dart'; part 'utils/provider/paginated_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 invoke( + FutureOr 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( + (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; + }, +); diff --git a/lib/provider/spotify/tracks/track.dart b/lib/provider/spotify/tracks/track.dart index e3913b1f..9863aa25 100644 --- a/lib/provider/spotify/tracks/track.dart +++ b/lib/provider/spotify/tracks/track.dart @@ -6,5 +6,5 @@ final trackProvider = final spotify = ref.watch(spotifyProvider); - return spotify.tracks.get(id); + return spotify.invoke((api) => api.tracks.get(id)); }); diff --git a/lib/provider/spotify/user/me.dart b/lib/provider/spotify/user/me.dart index c5949e1f..09f5fc2d 100644 --- a/lib/provider/spotify/user/me.dart +++ b/lib/provider/spotify/user/me.dart @@ -2,5 +2,5 @@ part of '../spotify.dart'; final meProvider = FutureProvider((ref) async { final spotify = ref.watch(spotifyProvider); - return spotify.me.get(); + return spotify.invoke((api) => api.me.get()); }); diff --git a/lib/provider/spotify/utils/mixin.dart b/lib/provider/spotify/utils/mixin.dart index 0da14c6f..60788814 100644 --- a/lib/provider/spotify/utils/mixin.dart +++ b/lib/provider/spotify/utils/mixin.dart @@ -2,7 +2,7 @@ part of '../spotify.dart'; // ignore: invalid_use_of_internal_member mixin SpotifyMixin on AsyncNotifierBase { - SpotifyApi get spotify => ref.read(spotifyProvider); + SpotifyApiWrapper get spotify => ref.read(spotifyProvider); } extension on AutoDisposeAsyncNotifierProviderRef { diff --git a/lib/provider/spotify_provider.dart b/lib/provider/spotify_provider.dart deleted file mode 100644 index 5824cce0..00000000 --- a/lib/provider/spotify_provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -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((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); -}); diff --git a/pubspec.lock b/pubspec.lock index 325e9dfd..8a9de0d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,10 +42,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99 + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.4.0" app_links_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 41050675..fd2cf9f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 4.0.1+40 +version: 4.0.2+41 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube @@ -13,7 +13,7 @@ environment: flutter: ">=3.29.0" dependencies: - app_links: ^6.3.2 + app_links: ^6.4.0 args: ^2.5.0 async: ^2.11.0 audio_service: ^0.18.13