From 326d8212f63fdd6451342d0289cedec15da59fd4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 14 Jun 2025 21:07:07 +0600 Subject: [PATCH] feat: add albums metadata endpoint and provider --- .../metadata_plugin/library/albums.dart | 71 +++++++++++++++++- .../metadata_plugin/library/playlists.dart | 22 +++--- .../metadata_plugin/tracks/playlist.dart | 9 +-- .../metadata_plugin/utils/common.dart | 13 +++- lib/services/metadata/endpoints/album.dart | 75 +++++++++++++++++++ lib/services/metadata/metadata.dart | 5 +- 6 files changed, 173 insertions(+), 22 deletions(-) diff --git a/lib/provider/metadata_plugin/library/albums.dart b/lib/provider/metadata_plugin/library/albums.dart index 32abe37b..1dd61b81 100644 --- a/lib/provider/metadata_plugin/library/albums.dart +++ b/lib/provider/metadata_plugin/library/albums.dart @@ -1,8 +1,73 @@ import 'package:riverpod/riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; -class LibraryAlbumsNotifier extends AsyncNotifier { +class MetadataPluginSavedAlbumNotifier + extends PaginatedAsyncNotifier { @override - build() { - return null; + Future> fetch( + int offset, + int limit, + ) async { + return await (await metadataPlugin).user.savedAlbums( + limit: limit, + offset: offset, + ); + } + + @override + build() async { + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } + + Future addFavorite(List albums) async { + await update((state) async { + (await metadataPlugin).album.save(albums.map((e) => e.id).toList()); + return state.copyWith( + items: [...state.items, albums], + ) as SpotubePaginationResponseObject; + }); + + for (final album in albums) { + ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id)); + } + } + + Future removeFavorite(List albums) async { + await update((state) async { + final albumIds = albums.map((e) => e.id).toList(); + (await metadataPlugin).album.unsave(albumIds); + return state.copyWith( + items: state.items + .where( + (e) => + albumIds.contains((e as SpotubeSimpleAlbumObject).id) == + false, + ) + .toList() as List, + ) as SpotubePaginationResponseObject; + }); + + for (final album in albums) { + ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id)); + } } } + +final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider< + MetadataPluginSavedAlbumNotifier, + SpotubePaginationResponseObject>( + () => MetadataPluginSavedAlbumNotifier(), +); + +final metadataPluginIsSavedAlbumProvider = + FutureProvider.autoDispose.family( + (ref, albumId) async { + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + return metadataPlugin!.user + .isSavedAlbums([albumId]).then((value) => value.first); + }, +); diff --git a/lib/provider/metadata_plugin/library/playlists.dart b/lib/provider/metadata_plugin/library/playlists.dart index a017f65f..cbb5b166 100644 --- a/lib/provider/metadata_plugin/library/playlists.dart +++ b/lib/provider/metadata_plugin/library/playlists.dart @@ -6,17 +6,17 @@ import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart'; import 'package:spotube/services/metadata/endpoints/error.dart'; -class FavoritePlaylistsNotifier +class MetadataPluginSavedPlaylistsNotifier extends PaginatedAsyncNotifier { - FavoritePlaylistsNotifier() : super(); + MetadataPluginSavedPlaylistsNotifier() : super(); @override fetch(int offset, int limit) async { final playlists = await (await metadataPlugin) - ?.user + .user .savedPlaylists(limit: limit, offset: offset); - return playlists!; + return playlists; } @override @@ -43,7 +43,7 @@ class FavoritePlaylistsNotifier Future addFavorite(SpotubeSimplePlaylistObject playlist) async { await update((state) async { - (await metadataPlugin)!.playlist.save(playlist.id); + (await metadataPlugin).playlist.save(playlist.id); return state.copyWith( items: [...state.items, playlist], ) as SpotubePaginationResponseObject; @@ -54,7 +54,7 @@ class FavoritePlaylistsNotifier Future removeFavorite(SpotubeSimplePlaylistObject playlist) async { await update((state) async { - (await metadataPlugin)!.playlist.unsave(playlist.id); + (await metadataPlugin).playlist.unsave(playlist.id); return state.copyWith( items: state.items .where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id) @@ -67,7 +67,7 @@ class FavoritePlaylistsNotifier Future delete(SpotubeSimplePlaylistObject playlist) async { await update((state) async { - (await metadataPlugin)!.playlist.deletePlaylist(playlist.id); + (await metadataPlugin).playlist.deletePlaylist(playlist.id); return state.copyWith( items: state.items .where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id) @@ -82,7 +82,7 @@ class FavoritePlaylistsNotifier Future addTracks(String playlistId, List trackIds) async { if (state.value == null) return; - await (await metadataPlugin)! + await (await metadataPlugin) .playlist .addTracks(playlistId, trackIds: trackIds); @@ -92,7 +92,7 @@ class FavoritePlaylistsNotifier Future removeTracks(String playlistId, List trackIds) async { if (state.value == null) return; - await (await metadataPlugin)! + await (await metadataPlugin) .playlist .removeTracks(playlistId, trackIds: trackIds); @@ -101,9 +101,9 @@ class FavoritePlaylistsNotifier } final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider< - FavoritePlaylistsNotifier, + MetadataPluginSavedPlaylistsNotifier, SpotubePaginationResponseObject>( - () => FavoritePlaylistsNotifier(), + () => MetadataPluginSavedPlaylistsNotifier(), ); final metadataPluginIsSavedPlaylistProvider = diff --git a/lib/provider/metadata_plugin/tracks/playlist.dart b/lib/provider/metadata_plugin/tracks/playlist.dart index c0302bb4..42bae41c 100644 --- a/lib/provider/metadata_plugin/tracks/playlist.dart +++ b/lib/provider/metadata_plugin/tracks/playlist.dart @@ -10,7 +10,7 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< @override fetch(offset, limit) async { - final tracks = await (await metadataPlugin)!.playlist.tracks( + final tracks = await (await metadataPlugin).playlist.tracks( arg, offset: offset, limit: limit, @@ -28,9 +28,8 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< } } -final metadataPluginPlaylistTracksProvider = AutoDisposeAsyncNotifierProviderFamily< - PlaylistTracksNotifier, - SpotubePaginationResponseObject, - String>( +final metadataPluginPlaylistTracksProvider = + AutoDisposeAsyncNotifierProviderFamily, String>( () => PlaylistTracksNotifier(), ); diff --git a/lib/provider/metadata_plugin/utils/common.dart b/lib/provider/metadata_plugin/utils/common.dart index 6a64bd91..2c9877a3 100644 --- a/lib/provider/metadata_plugin/utils/common.dart +++ b/lib/provider/metadata_plugin/utils/common.dart @@ -6,13 +6,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/services/metadata/endpoints/error.dart'; import 'package:spotube/services/metadata/metadata.dart'; mixin MetadataPluginMixin // ignore: invalid_use_of_internal_member on AsyncNotifierBase> { - Future get metadataPlugin async => - await ref.read(metadataPluginProvider.future); + Future get metadataPlugin async { + final plugin = await ref.read(metadataPluginProvider.future); + + if (plugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "Metadata plugin is not set"); + } + + return plugin; + } } extension AutoDisposeAsyncNotifierCacheFor diff --git a/lib/services/metadata/endpoints/album.dart b/lib/services/metadata/endpoints/album.dart index e69de29b..cac324d6 100644 --- a/lib/services/metadata/endpoints/album.dart +++ b/lib/services/metadata/endpoints/album.dart @@ -0,0 +1,75 @@ +import 'package:hetu_script/hetu_script.dart'; +import 'package:hetu_script/values.dart'; +import 'package:spotube/models/metadata/metadata.dart'; + +class MetadataPluginAlbumEndpoint { + final Hetu hetu; + MetadataPluginAlbumEndpoint(this.hetu); + + HTInstance get hetuMetadataAlbum => + (hetu.fetch("metadataPlugin") as HTInstance).memberGet("album") + as HTInstance; + + Future getAlbum(String id) async { + final raw = + await hetuMetadataAlbum.invoke("getAlbum", positionalArgs: [id]) as Map; + + return SpotubeFullAlbumObject.fromJson( + raw.cast(), + ); + } + + Future> tracks( + String id, { + int? offset, + int? limit, + }) async { + final raw = await hetuMetadataAlbum.invoke( + "tracks", + positionalArgs: [id], + namedArgs: { + "offset": offset, + "limit": limit, + }..removeWhere((key, value) => value == null), + ) as Map; + + return SpotubePaginationResponseObject.fromJson( + raw.cast(), + (Map json) => + SpotubeFullTrackObject.fromJson(json.cast()), + ); + } + + Future> releases({ + int? offset, + int? limit, + }) async { + final raw = await hetuMetadataAlbum.invoke( + "releases", + namedArgs: { + "offset": offset, + "limit": limit, + }..removeWhere((key, value) => value == null), + ) as Map; + + return SpotubePaginationResponseObject.fromJson( + raw.cast(), + (Map json) => + SpotubeSimpleAlbumObject.fromJson(json.cast()), + ); + } + + Future save(List ids) async { + await hetuMetadataAlbum.invoke( + "save", + positionalArgs: [ids], + ); + } + + Future unsave(List ids) async { + await hetuMetadataAlbum.invoke( + "unsave", + positionalArgs: [ids], + ); + } +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index 01e158fe..fb63403e 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -11,6 +11,7 @@ import 'package:spotube/collections/routes.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/metadata/apis/localstorage.dart'; +import 'package:spotube/services/metadata/endpoints/album.dart'; import 'package:spotube/services/metadata/endpoints/auth.dart'; import 'package:spotube/services/metadata/endpoints/playlist.dart'; import 'package:spotube/services/metadata/endpoints/user.dart'; @@ -73,11 +74,13 @@ class MetadataPlugin { final Hetu hetu; late final MetadataAuthEndpoint auth; - late final MetadataPluginUserEndpoint user; + late final MetadataPluginAlbumEndpoint album; late final MetadataPluginPlaylistEndpoint playlist; + late final MetadataPluginUserEndpoint user; MetadataPlugin._(this.hetu) { auth = MetadataAuthEndpoint(hetu); + album = MetadataPluginAlbumEndpoint(hetu); playlist = MetadataPluginPlaylistEndpoint(hetu); user = MetadataPluginUserEndpoint(hetu); }