diff --git a/lib/APIs/spotify_api.dart b/lib/APIs/spotify_api.dart index b1c872f33..d6c9e1670 100644 --- a/lib/APIs/spotify_api.dart +++ b/lib/APIs/spotify_api.dart @@ -132,12 +132,11 @@ class SpotifyApi { final List songsData = []; if (response.statusCode == 200) { final result = jsonDecode(response.body); + final playlistCache = Hive.box('cache').get( + 'spotifyPlaylists', + defaultValue: [], + ) as List; for (final element in result['items'] as List) { - final playlistCache = Hive.box('cache').get( - 'spotifyPlaylists', - defaultValue: [], - ) as List; - final cachedPlaylist = playlistCache.isNotEmpty ? playlistCache .where((x) => x['id'] == element['id']) @@ -195,6 +194,79 @@ class SpotifyApi { } } + Future getPlaylist(String accessToken, String playlistId) async { + try { + final Uri path = Uri.parse( + '$spotifyApiBaseUrl$spotifyPlaylistTrackEndpoint/$playlistId'); + + final response = await get( + path, + headers: { + 'Authorization': 'Bearer $accessToken', + 'Accept': 'application/json' + }, + ); + Map songsData = {}; + if (response.statusCode == 200) { + final result = jsonDecode(response.body); + final playlistCache = Hive.box('cache').get( + 'spotifyPlaylists', + defaultValue: [], + ) as List; + final element = result; + final cachedPlaylist = playlistCache.isNotEmpty + ? playlistCache.where((x) => x['id'] == element['id'])?.firstOrNull + : null; + + Map playlistObject = { + 'title': element['name'], + 'id': element['id'], + 'snapshot_id': element['snapshot_id'], + 'subtitle': element['description'], + 'image': element['images'][0]['url'], + 'perma_url': element['external_urls']['spotify'], + 'type': element['type'], + 'uri': element['uri'], + }; + + if (cachedPlaylist != null) { + Logger.root.info('Found cached playlist for ${element["id"]}'); + playlistObject = Map.from(cachedPlaylist as Map); + if (cachedPlaylist['snapshot_id'] != element['snapshot_id'] || + (cachedPlaylist['tracks'] as List).length != + element['tracks']['total']) { + Logger.root.info( + 'Cached playlist for ${element["id"]} has changed, update cache', + ); + playlistCache.remove(cachedPlaylist); + playlistObject['tracks'] = + await SpotifyApi().getAllTracksOfPlaylist( + accessToken, + element['id'].toString(), + ); + playlistCache.add(playlistObject); + Hive.box('cache').put('spotifyPlaylists', playlistCache); + } + } else { + Logger.root.info('New playlist ${element["id"]}, add to cache'); + playlistObject['tracks'] = await SpotifyApi().getAllTracksOfPlaylist( + accessToken, + element['id'].toString(), + ); + playlistCache.add(playlistObject); + Hive.box('cache').put('spotifyPlaylists', playlistCache); + } + songsData = playlistObject; + } else { + throw Exception('Spotify error: ${jsonDecode(response.body)}'); + } + return songsData; + } catch (e) { + Logger.root.severe('Error in getting spotify playlist $playlistId: $e'); + return null; + } + } + Future getAllTracksOfPlaylist( String accessToken, String playlistId, @@ -286,15 +358,15 @@ class SpotifyApi { 'Accept': 'application/json' }, ); + + final playlistCache = Hive.box('cache').get( + 'spotifyPlaylists', + defaultValue: [], + ) as List; final List songsData = []; if (response.statusCode == 200) { final result = jsonDecode(response.body); for (final element in result['playlists']['items'] as List) { - final playlistCache = Hive.box('cache').get( - 'spotifyPlaylists', - defaultValue: [], - ) as List; - final cachedPlaylist = playlistCache.isNotEmpty ? playlistCache .where((x) => x['id'] == element['id']) @@ -310,7 +382,7 @@ class SpotifyApi { 'perma_url': element['external_urls']['spotify'], 'type': element['type'], 'uri': element['uri'], - 'section': result['message'] + 'section': result['message'], }; if (cachedPlaylist != null) { diff --git a/lib/CustomWidgets/playlist_popupmenu.dart b/lib/CustomWidgets/playlist_popupmenu.dart index 3a438ec9a..2cd60c26c 100644 --- a/lib/CustomWidgets/playlist_popupmenu.dart +++ b/lib/CustomWidgets/playlist_popupmenu.dart @@ -29,11 +29,12 @@ import 'package:soundal/Screens/Player/audioplayer.dart'; class PlaylistPopupMenu extends StatefulWidget { final List data; final String title; - const PlaylistPopupMenu({ - super.key, - required this.data, - required this.title, - }); + final bool showSave; + const PlaylistPopupMenu( + {super.key, + required this.data, + required this.title, + required this.showSave}); @override _PlaylistPopupMenuState createState() => _PlaylistPopupMenuState(); diff --git a/lib/Helpers/format.dart b/lib/Helpers/format.dart index 090db59a5..093a20f62 100644 --- a/lib/Helpers/format.dart +++ b/lib/Helpers/format.dart @@ -562,7 +562,7 @@ class FormatResponse { if (cachedDetails.isEmpty) { cachedDetails = await SaavnAPI().fetchSongDetails(item['id'].toString()); - Hive.box('cache') + await Hive.box('cache') .put(cachedDetails['id'].toString(), cachedDetails); } list[i] = cachedDetails; @@ -620,7 +620,7 @@ class FormatResponse { final res = await Hive.box('spoty2youtube') .get(tracks[song['order'] as int]['id']); if (res == null) { - Hive.box('spoty2youtube') + await Hive.box('spoty2youtube') .put(tracks[song['order'] as int]['id'], song['id']); } songList.add(song); diff --git a/lib/Screens/Common/song_list.dart b/lib/Screens/Common/song_list.dart index b8d629acb..6491a9a32 100644 --- a/lib/Screens/Common/song_list.dart +++ b/lib/Screens/Common/song_list.dart @@ -133,7 +133,6 @@ class _SongsListPageState extends State { fetched = true; loading = false; }); - break; case 'album': //Retrieve spotify album songs then convert them to youtube Map receivedData = {}; @@ -167,17 +166,29 @@ class _SongsListPageState extends State { fetched = true; loading = false; setState(() {}); - break; case 'playlist': final List songs = []; - final playlistCached = playlistCache.firstWhere( + var playlistCached = playlistCache.firstWhere( (element) => element['id'] == widget.listItem['id'], orElse: () => null, ); - if (playlistCached == null || - (playlistCached['tracks'] as List).firstOrNull?['is_local'] != - null) { + if (playlistCached == null) { + await callSpotifyFunction( + function: (String accessToken) async => { + playlistCached = await SpotifyApi().getPlaylist( + accessToken, + widget.listItem['id'].toString(), + ) + }, + ); + } + + if ((playlistCached['tracks'] as List).firstOrNull?['is_local'] != + null) { + Logger.root.info( + 'No cached songs for ${widget.listItem['id']}, update cache to add songs', + ); if (widget.listItem['tracks'].runtimeType != List) { await callSpotifyFunction( function: (String accessToken) async => { @@ -197,7 +208,7 @@ class _SongsListPageState extends State { songs, ); playlistCached['tracks'] = songList; - Hive.box('settings').put('spotifyPlaylists', playlistCache); + Hive.box('cache').put('spotifyPlaylists', playlistCache); } else { songList = playlistCached['tracks'] as List; } @@ -206,7 +217,6 @@ class _SongsListPageState extends State { loading = false; setState(() {}); - break; case 'mix': SaavnAPI() .getSongFromToken( @@ -228,7 +238,6 @@ class _SongsListPageState extends State { ); } }); - break; case 'show': SaavnAPI() .getSongFromToken( @@ -250,7 +259,6 @@ class _SongsListPageState extends State { ); } }); - break; default: setState(() { fetched = true; @@ -306,6 +314,7 @@ class _SongsListPageState extends State { ), PlaylistPopupMenu( data: songList, + showSave: widget.listItem['snapshot_id'] == null, title: widget.listItem['title']?.toString() ?? 'Songs', ), diff --git a/lib/Screens/Home/spotify.dart b/lib/Screens/Home/spotify.dart index e3e575e38..e114db444 100644 --- a/lib/Screens/Home/spotify.dart +++ b/lib/Screens/Home/spotify.dart @@ -35,7 +35,7 @@ import 'package:soundal/CustomWidgets/song_tile_trailing_menu.dart'; import 'package:soundal/Helpers/countrycodes.dart'; import 'package:soundal/Helpers/extensions.dart'; import 'package:soundal/Helpers/image_resolution_modifier.dart'; -import 'package:soundal/Helpers/logging.dart'; +import 'package:soundal/Helpers/logger.dart'; import 'package:soundal/Helpers/spotify_helper.dart'; import 'package:soundal/Screens/Common/song_list.dart'; import 'package:soundal/Screens/Library/liked.dart'; diff --git a/lib/Screens/Search/artists.dart b/lib/Screens/Search/artists.dart index 5c394078a..125ebf1f9 100644 --- a/lib/Screens/Search/artists.dart +++ b/lib/Screens/Search/artists.dart @@ -174,6 +174,7 @@ class _ArtistSearchPageState extends State { ), if (data['Top Songs'] != null) PlaylistPopupMenu( + showSave: true, data: data['Top Songs']!, title: widget.data['title']?.toString() ?? 'Songs', diff --git a/lib/Screens/Top Charts/top.dart b/lib/Screens/Top Charts/top.dart index 4dd331a44..d7d8ed2d0 100644 --- a/lib/Screens/Top Charts/top.dart +++ b/lib/Screens/Top Charts/top.dart @@ -30,6 +30,7 @@ import 'package:soundal/CustomWidgets/like_button.dart'; import 'package:soundal/CustomWidgets/song_tile_trailing_menu.dart'; import 'package:soundal/Helpers/countrycodes.dart'; import 'package:soundal/Helpers/format.dart'; +import 'package:soundal/Helpers/logger.dart'; import 'package:soundal/Helpers/spotify_helper.dart'; import 'package:soundal/Screens/Settings/setting.dart'; import 'package:soundal/Services/player_service.dart'; @@ -154,56 +155,66 @@ Future getChartDetails(String accessToken, String type) async { final String playlistId = type == 'Global' ? globalPlaylistId : localPlaylistId; final List data = []; - final List tracks = - await SpotifyApi().getAllTracksOfPlaylist(accessToken, playlistId); + List songList = []; - for (final element in tracks) { - data.add(element['track']); - } - return FormatResponse.parallelSpotifyListToYoutubeList( - data, + final List playlistCache = Hive.box('cache').get( + 'spotifyPlaylists', + defaultValue: [], + ) as List; + + var playlistCached = playlistCache.firstWhere( + (element) => element['id'] == playlistId, + orElse: () => null, ); - /*final List futures = []; - for (final track in tracks) { - futures.add( - FormatResponse.spotifyToYoutubeTrack( - track, - tracks.indexOf(track), - ), + if (playlistCached == null) { + await callSpotifyFunction( + function: (String accessToken) async => { + playlistCached = await SpotifyApi().getPlaylist( + accessToken, + playlistId, + ) + }, + ); + playlistCache.add(playlistCached); + playlistCached = playlistCache.firstWhere( + (element) => element['id'] == playlistId, + orElse: () => null, ); } - await Future.wait(futures); - for (final element in futures) { - final song = await element; - if (song != null) { - data.add(await element); - } else { - final indexBadSong = futures.indexOf(element); - final badSong = tracks.elementAt(indexBadSong); - Logger.root.warning( - 'Song at $indexBadSong was not retrieved from Youtube Music or track is null'); + if ((playlistCached['tracks'] as List).firstOrNull?['is_local'] != null) { + Logger.root.info( + 'No cached songs for $playlistId, update cache to add songs', + ); + + if (playlistCached != null && + playlistCached['tracks']?.runtimeType != List) { + await callSpotifyFunction( + function: (String accessToken) async => { + playlistCached['tracks'] = await SpotifyApi().getAllTracksOfPlaylist( + accessToken, + playlistId, + ) + }, + ); + } + + for (final element in playlistCached['tracks'] as List) { + data.add(element['track']); } - }*/ - /*final trackName = track['track']['name']; - final imageUrlSmall = track['track']['album']['images'].last['url']; - final imageUrlBig = track['track']['album']['images'].first['url']; - final spotifyUrl = track['track']['external_urls']['spotify']; - final artistName = track['track']['artists'][0]['name'].toString(); - data.add({ - 'name': trackName, - 'artist': artistName, - 'image_url_small': imageUrlSmall, - 'image_url_big': imageUrlBig, - 'spotifyUrl': spotifyUrl, - }); - data.sort((a, b) { - return (a['order'] as int).compareTo((b['order']) as int); - }); + songList = await FormatResponse.parallelSpotifyListToYoutubeList( + data, + ); + + playlistCached['tracks'] = songList; + Hive.box('cache').put('spotifyPlaylists', playlistCache); + } else { + songList = playlistCached['tracks'] as List; + } - return data;*/ + return songList; } Future scrapData(String type, {bool signIn = false}) async { @@ -227,21 +238,21 @@ Future scrapData(String type, {bool signIn = false}) async { final link = uri.toString(); if (link.contains('code=')) { final code = link.split('code=')[1]; - Hive.box('settings').put('spotifyAppCode', code); + await Hive.box('settings').put('spotifyAppCode', code); final currentTime = DateTime.now().millisecondsSinceEpoch / 1000; final List data = await SpotifyApi().getAccessToken(code: code); if (data.isNotEmpty) { - Hive.box('settings').put('spotifyAccessToken', data[0]); - Hive.box('settings').put('spotifyRefreshToken', data[1]); - Hive.box('settings').put('spotifySigned', true); - Hive.box('settings') + await Hive.box('settings').put('spotifyAccessToken', data[0]); + await Hive.box('settings').put('spotifyRefreshToken', data[1]); + await Hive.box('settings').put('spotifySigned', true); + await Hive.box('settings') .put('spotifyTokenExpireAt', currentTime + int.parse(data[2])); } final temp = await getChartDetails(data[0], type); if (temp.isNotEmpty) { - Hive.box('cache').put('${type}_chart', temp); + await Hive.box('cache').put('${type}_chart', temp); if (type == 'Global') { globalSongs = temp; } else { @@ -259,7 +270,7 @@ Future scrapData(String type, {bool signIn = false}) async { } else { final temp = await getChartDetails(accessToken, type); if (temp.isNotEmpty) { - Hive.box('cache').put('${type}_chart', temp); + await Hive.box('cache').put('${type}_chart', temp); if (type == 'Global') { globalSongs = temp; } else { @@ -307,10 +318,10 @@ class _TopPageState extends State @override void initState() { super.initState(); - ytMusic.init().then((value) { + /*ytMusic.init().then((value) { getCachedData(widget.type); scrapData(widget.type); - }); + });*/ } @override diff --git a/lib/Screens/YouTube/youtube_playlist.dart b/lib/Screens/YouTube/youtube_playlist.dart index bff8eb201..61193a106 100644 --- a/lib/Screens/YouTube/youtube_playlist.dart +++ b/lib/Screens/YouTube/youtube_playlist.dart @@ -160,6 +160,7 @@ class _YouTubePlaylistState extends State { imageUrl: playlistImage, actions: [ PlaylistPopupMenu( + showSave: false, data: searchedList, title: playlistName, ), diff --git a/lib/Services/audio_service.dart b/lib/Services/audio_service.dart index 98ffda96c..b5d9309ff 100644 --- a/lib/Services/audio_service.dart +++ b/lib/Services/audio_service.dart @@ -578,7 +578,9 @@ class AudioPlayerHandlerImpl extends BaseAudioHandler ], ); _player = AudioPlayer( - audioPipeline: pipeline, androidOffloadSchedulingEnabled: true); + audioPipeline: pipeline, + androidOffloadSchedulingEnabled: false, + ); // Enable equalizer if used earlier final eqValue = Hive.box('settings').get('setEqualizer') as bool; @@ -596,7 +598,7 @@ class AudioPlayerHandlerImpl extends BaseAudioHandler ); } else { Logger.root.info('starting without eq pipeline'); - _player = AudioPlayer(androidOffloadSchedulingEnabled: true); + _player = AudioPlayer(androidOffloadSchedulingEnabled: false); } } diff --git a/lib/main.dart b/lib/main.dart index bbcca0752..80a2969af 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,7 +42,6 @@ import 'package:soundal/Helpers/route_handler.dart'; import 'package:soundal/Screens/About/about.dart'; import 'package:soundal/Screens/Home/home.dart'; import 'package:soundal/Screens/Library/downloads.dart'; -import 'package:soundal/Screens/Library/nowplaying.dart'; import 'package:soundal/Screens/Library/playlists.dart'; import 'package:soundal/Screens/Library/recent.dart'; import 'package:soundal/Screens/Library/stats.dart'; @@ -354,7 +353,7 @@ class _MyAppState extends State { '/setting': (context) => const SettingPage(), '/about': (context) => AboutScreen(), '/playlists': (context) => PlaylistScreen(), - '/nowplaying': (context) => NowPlaying(), + //'/nowplaying': (context) => NowPlaying(), '/recent': (context) => RecentlyPlayed(), '/downloads': (context) => const Downloads(), '/stats': (context) => const Stats(), diff --git a/pubspec.yaml b/pubspec.yaml index 6984e4004..b056a7c2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ publish_to: "none" version: 1.0.0 environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" dependencies: