diff --git a/lib/provider/metadata_plugin/album/album.dart b/lib/provider/metadata_plugin/album/album.dart new file mode 100644 index 00000000..72c62202 --- /dev/null +++ b/lib/provider/metadata_plugin/album/album.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_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/common.dart'; +import 'package:spotube/services/metadata/endpoints/error.dart'; + +final metadataPluginAlbumProvider = + FutureProvider.autoDispose.family( + (ref, id) async { + ref.cacheFor(); + + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + if (metadataPlugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "No metadata plugin is not set", + ); + } + + return metadataPlugin.album.getAlbum(id); + }, +); diff --git a/lib/provider/metadata_plugin/artist/albums.dart b/lib/provider/metadata_plugin/artist/albums.dart new file mode 100644 index 00000000..c9fd87b9 --- /dev/null +++ b/lib/provider/metadata_plugin/artist/albums.dart @@ -0,0 +1,32 @@ +import 'package:flutter_riverpod/flutter_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/family_paginated.dart'; + +class MetadataPluginArtistAlbumNotifier + extends FamilyPaginatedAsyncNotifier { + @override + Future> fetch( + int offset, + int limit, + ) async { + return await (await metadataPlugin).artist.albums( + arg, + limit: limit, + offset: offset, + ); + } + + @override + build(arg) async { + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginArtistAlbumsProvider = AsyncNotifierFamilyProvider< + MetadataPluginArtistAlbumNotifier, + SpotubePaginationResponseObject, + String>( + () => MetadataPluginArtistAlbumNotifier(), +); diff --git a/lib/provider/metadata_plugin/artist/artist.dart b/lib/provider/metadata_plugin/artist/artist.dart new file mode 100644 index 00000000..e55d6103 --- /dev/null +++ b/lib/provider/metadata_plugin/artist/artist.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_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/common.dart'; +import 'package:spotube/services/metadata/endpoints/error.dart'; + +final metadataPluginArtistProvider = + FutureProvider.autoDispose.family( + (ref, artistId) async { + ref.cacheFor(); + + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + if (metadataPlugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "No metadata plugin is not set", + ); + } + + return metadataPlugin.artist.getArtist(artistId); + }, +); diff --git a/lib/provider/metadata_plugin/artist/top_tracks.dart b/lib/provider/metadata_plugin/artist/top_tracks.dart new file mode 100644 index 00000000..c622a738 --- /dev/null +++ b/lib/provider/metadata_plugin/artist/top_tracks.dart @@ -0,0 +1,38 @@ +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/provider/metadata_plugin/utils/family_paginated.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; + +class MetadataPluginArtistTopTracksNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginArtistTopTracksNotifier() : super(); + + @override + fetch(offset, limit) async { + final tracks = await (await metadataPlugin).artist.topTracks( + arg, + offset: offset, + limit: limit, + ); + + return tracks; + } + + @override + build(arg) async { + ref.cacheFor(); + + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginArtistTopTracksProvider = + AutoDisposeAsyncNotifierProviderFamily< + MetadataPluginArtistTopTracksNotifier, + SpotubePaginationResponseObject, + String>( + () => MetadataPluginArtistTopTracksNotifier(), +); diff --git a/lib/provider/metadata_plugin/library/artists.dart b/lib/provider/metadata_plugin/library/artists.dart index e69de29b..5b8de747 100644 --- a/lib/provider/metadata_plugin/library/artists.dart +++ b/lib/provider/metadata_plugin/library/artists.dart @@ -0,0 +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 MetadataPluginSavedArtistNotifier + extends PaginatedAsyncNotifier { + @override + Future> fetch( + int offset, + int limit, + ) async { + return await (await metadataPlugin).user.savedArtists( + limit: limit, + offset: offset, + ); + } + + @override + build() async { + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } + + Future addFavorite(List artists) async { + await update((state) async { + (await metadataPlugin).artist.save(artists.map((e) => e.id).toList()); + return state.copyWith( + items: [...state.items, artists], + ) as SpotubePaginationResponseObject; + }); + + for (final artist in artists) { + ref.invalidate(metadataPluginIsSavedArtistProvider(artist.id)); + } + } + + Future removeFavorite(List artists) async { + await update((state) async { + final artistIds = artists.map((e) => e.id).toList(); + (await metadataPlugin).artist.unsave(artistIds); + return state.copyWith( + items: state.items + .where( + (e) => + artistIds.contains((e as SpotubeFullArtistObject).id) == + false, + ) + .toList() as List, + ) as SpotubePaginationResponseObject; + }); + + for (final artist in artists) { + ref.invalidate(metadataPluginIsSavedArtistProvider(artist.id)); + } + } +} + +final metadataPluginSavedArtistsProvider = AsyncNotifierProvider< + MetadataPluginSavedArtistNotifier, + SpotubePaginationResponseObject>( + () => MetadataPluginSavedArtistNotifier(), +); + +final metadataPluginIsSavedArtistProvider = + FutureProvider.autoDispose.family( + (ref, artistId) async { + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + return metadataPlugin!.user + .isSavedArtists([artistId]).then((value) => value.first); + }, +); diff --git a/lib/provider/metadata_plugin/playlist/playlist.dart b/lib/provider/metadata_plugin/playlist/playlist.dart new file mode 100644 index 00000000..9752cb21 --- /dev/null +++ b/lib/provider/metadata_plugin/playlist/playlist.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_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/common.dart'; +import 'package:spotube/services/metadata/endpoints/error.dart'; + +final metadataPluginPlaylistProvider = + FutureProvider.autoDispose.family( + (ref, id) async { + ref.cacheFor(); + + final metadataPlugin = await ref.watch(metadataPluginProvider.future); + + if (metadataPlugin == null) { + throw MetadataPluginException.noDefaultPlugin( + "No metadata plugin is not set", + ); + } + + return metadataPlugin.playlist.getPlaylist(id); + }, +); diff --git a/lib/provider/metadata_plugin/tracks/album.dart b/lib/provider/metadata_plugin/tracks/album.dart new file mode 100644 index 00000000..5491bdd0 --- /dev/null +++ b/lib/provider/metadata_plugin/tracks/album.dart @@ -0,0 +1,36 @@ +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/provider/metadata_plugin/utils/family_paginated.dart'; +import 'package:spotube/provider/metadata_plugin/utils/common.dart'; + +class MetadataPluginAlbumTracksNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginAlbumTracksNotifier() : super(); + + @override + fetch(offset, limit) async { + final tracks = await (await metadataPlugin).album.tracks( + arg, + offset: offset, + limit: limit, + ); + + return tracks; + } + + @override + build(arg) async { + ref.cacheFor(); + + ref.watch(metadataPluginProvider); + return await fetch(0, 20); + } +} + +final metadataPluginAlbumTracksProvider = + AutoDisposeAsyncNotifierProviderFamily, String>( + () => MetadataPluginAlbumTracksNotifier(), +); diff --git a/lib/provider/metadata_plugin/tracks/playlist.dart b/lib/provider/metadata_plugin/tracks/playlist.dart index 42bae41c..7fdd47db 100644 --- a/lib/provider/metadata_plugin/tracks/playlist.dart +++ b/lib/provider/metadata_plugin/tracks/playlist.dart @@ -4,9 +4,10 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart'; import 'package:spotube/provider/metadata_plugin/utils/common.dart'; -class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< - SpotubeFullTrackObject, String> { - PlaylistTracksNotifier() : super(); +class MetadataPluginPlaylistTracksNotifier + extends AutoDisposeFamilyPaginatedAsyncNotifier { + MetadataPluginPlaylistTracksNotifier() : super(); @override fetch(offset, limit) async { @@ -29,7 +30,7 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< } final metadataPluginPlaylistTracksProvider = - AutoDisposeAsyncNotifierProviderFamily, String>( - () => PlaylistTracksNotifier(), + () => MetadataPluginPlaylistTracksNotifier(), ); diff --git a/lib/services/metadata/endpoints/artist.dart b/lib/services/metadata/endpoints/artist.dart index e69de29b..f8ff22f8 100644 --- a/lib/services/metadata/endpoints/artist.dart +++ b/lib/services/metadata/endpoints/artist.dart @@ -0,0 +1,79 @@ +import 'package:hetu_script/hetu_script.dart'; +import 'package:hetu_script/values.dart'; +import 'package:spotube/models/metadata/metadata.dart'; + +class MetadataPluginArtistEndpoint { + final Hetu hetu; + MetadataPluginArtistEndpoint(this.hetu); + + HTInstance get hetuMetadataArtist => + (hetu.fetch("metadataPlugin") as HTInstance).memberGet("artist") + as HTInstance; + + Future getArtist(String id) async { + final raw = await hetuMetadataArtist + .invoke("getArtist", positionalArgs: [id]) as Map; + + return SpotubeFullArtistObject.fromJson( + raw.cast(), + ); + } + + Future> topTracks( + String id, { + int? offset, + int? limit, + }) async { + final raw = await hetuMetadataArtist.invoke( + "topTracks", + 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> albums( + String id, { + int? offset, + int? limit, + }) async { + final raw = await hetuMetadataArtist.invoke( + "albums", + positionalArgs: [id], + 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 hetuMetadataArtist.invoke( + "save", + positionalArgs: [ids], + ); + } + + Future unsave(List ids) async { + await hetuMetadataArtist.invoke( + "unsave", + positionalArgs: [ids], + ); + } +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index fb63403e..38d35d23 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -12,6 +12,7 @@ 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/artist.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'; @@ -74,12 +75,16 @@ class MetadataPlugin { final Hetu hetu; late final MetadataAuthEndpoint auth; + late final MetadataPluginAlbumEndpoint album; + late final MetadataPluginArtistEndpoint artist; late final MetadataPluginPlaylistEndpoint playlist; late final MetadataPluginUserEndpoint user; MetadataPlugin._(this.hetu) { auth = MetadataAuthEndpoint(hetu); + + artist = MetadataPluginArtistEndpoint(hetu); album = MetadataPluginAlbumEndpoint(hetu); playlist = MetadataPluginPlaylistEndpoint(hetu); user = MetadataPluginUserEndpoint(hetu);