feat: add albums metadata endpoint and provider

This commit is contained in:
Kingkor Roy Tirtho 2025-06-14 21:07:07 +06:00
parent a9ba2582fb
commit 326d8212f6
6 changed files with 173 additions and 22 deletions

View File

@ -1,8 +1,73 @@
import 'package:riverpod/riverpod.dart'; 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<void> { class MetadataPluginSavedAlbumNotifier
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
@override @override
build() { Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
return null; 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<void> addFavorite(List<SpotubeSimpleAlbumObject> 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<SpotubeSimpleAlbumObject>;
});
for (final album in albums) {
ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id));
}
}
Future<void> removeFavorite(List<SpotubeSimpleAlbumObject> 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<SpotubeSimpleAlbumObject>,
) as SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>;
});
for (final album in albums) {
ref.invalidate(metadataPluginIsSavedAlbumProvider(album.id));
}
} }
} }
final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
MetadataPluginSavedAlbumNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
() => MetadataPluginSavedAlbumNotifier(),
);
final metadataPluginIsSavedAlbumProvider =
FutureProvider.autoDispose.family<bool, String>(
(ref, albumId) async {
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
return metadataPlugin!.user
.isSavedAlbums([albumId]).then((value) => value.first);
},
);

View File

@ -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/provider/metadata_plugin/utils/paginated.dart';
import 'package:spotube/services/metadata/endpoints/error.dart'; import 'package:spotube/services/metadata/endpoints/error.dart';
class FavoritePlaylistsNotifier class MetadataPluginSavedPlaylistsNotifier
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> { extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
FavoritePlaylistsNotifier() : super(); MetadataPluginSavedPlaylistsNotifier() : super();
@override @override
fetch(int offset, int limit) async { fetch(int offset, int limit) async {
final playlists = await (await metadataPlugin) final playlists = await (await metadataPlugin)
?.user .user
.savedPlaylists(limit: limit, offset: offset); .savedPlaylists(limit: limit, offset: offset);
return playlists!; return playlists;
} }
@override @override
@ -43,7 +43,7 @@ class FavoritePlaylistsNotifier
Future<void> addFavorite(SpotubeSimplePlaylistObject playlist) async { Future<void> addFavorite(SpotubeSimplePlaylistObject playlist) async {
await update((state) async { await update((state) async {
(await metadataPlugin)!.playlist.save(playlist.id); (await metadataPlugin).playlist.save(playlist.id);
return state.copyWith( return state.copyWith(
items: [...state.items, playlist], items: [...state.items, playlist],
) as SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>; ) as SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>;
@ -54,7 +54,7 @@ class FavoritePlaylistsNotifier
Future<void> removeFavorite(SpotubeSimplePlaylistObject playlist) async { Future<void> removeFavorite(SpotubeSimplePlaylistObject playlist) async {
await update((state) async { await update((state) async {
(await metadataPlugin)!.playlist.unsave(playlist.id); (await metadataPlugin).playlist.unsave(playlist.id);
return state.copyWith( return state.copyWith(
items: state.items items: state.items
.where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id) .where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id)
@ -67,7 +67,7 @@ class FavoritePlaylistsNotifier
Future<void> delete(SpotubeSimplePlaylistObject playlist) async { Future<void> delete(SpotubeSimplePlaylistObject playlist) async {
await update((state) async { await update((state) async {
(await metadataPlugin)!.playlist.deletePlaylist(playlist.id); (await metadataPlugin).playlist.deletePlaylist(playlist.id);
return state.copyWith( return state.copyWith(
items: state.items items: state.items
.where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id) .where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id)
@ -82,7 +82,7 @@ class FavoritePlaylistsNotifier
Future<void> addTracks(String playlistId, List<String> trackIds) async { Future<void> addTracks(String playlistId, List<String> trackIds) async {
if (state.value == null) return; if (state.value == null) return;
await (await metadataPlugin)! await (await metadataPlugin)
.playlist .playlist
.addTracks(playlistId, trackIds: trackIds); .addTracks(playlistId, trackIds: trackIds);
@ -92,7 +92,7 @@ class FavoritePlaylistsNotifier
Future<void> removeTracks(String playlistId, List<String> trackIds) async { Future<void> removeTracks(String playlistId, List<String> trackIds) async {
if (state.value == null) return; if (state.value == null) return;
await (await metadataPlugin)! await (await metadataPlugin)
.playlist .playlist
.removeTracks(playlistId, trackIds: trackIds); .removeTracks(playlistId, trackIds: trackIds);
@ -101,9 +101,9 @@ class FavoritePlaylistsNotifier
} }
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider< final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
FavoritePlaylistsNotifier, MetadataPluginSavedPlaylistsNotifier,
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>( SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>(
() => FavoritePlaylistsNotifier(), () => MetadataPluginSavedPlaylistsNotifier(),
); );
final metadataPluginIsSavedPlaylistProvider = final metadataPluginIsSavedPlaylistProvider =

View File

@ -10,7 +10,7 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
@override @override
fetch(offset, limit) async { fetch(offset, limit) async {
final tracks = await (await metadataPlugin)!.playlist.tracks( final tracks = await (await metadataPlugin).playlist.tracks(
arg, arg,
offset: offset, offset: offset,
limit: limit, limit: limit,
@ -28,9 +28,8 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
} }
} }
final metadataPluginPlaylistTracksProvider = AutoDisposeAsyncNotifierProviderFamily< final metadataPluginPlaylistTracksProvider =
PlaylistTracksNotifier, AutoDisposeAsyncNotifierProviderFamily<PlaylistTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
String>(
() => PlaylistTracksNotifier(), () => PlaylistTracksNotifier(),
); );

View File

@ -6,13 +6,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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'; import 'package:spotube/services/metadata/metadata.dart';
mixin MetadataPluginMixin<K> mixin MetadataPluginMixin<K>
// ignore: invalid_use_of_internal_member // ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> { on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
Future<MetadataPlugin?> get metadataPlugin async => Future<MetadataPlugin> get metadataPlugin async {
await ref.read(metadataPluginProvider.future); final plugin = await ref.read(metadataPluginProvider.future);
if (plugin == null) {
throw MetadataPluginException.noDefaultPlugin(
"Metadata plugin is not set");
}
return plugin;
}
} }
extension AutoDisposeAsyncNotifierCacheFor extension AutoDisposeAsyncNotifierCacheFor

View File

@ -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<SpotubeFullAlbumObject> getAlbum(String id) async {
final raw =
await hetuMetadataAlbum.invoke("getAlbum", positionalArgs: [id]) as Map;
return SpotubeFullAlbumObject.fromJson(
raw.cast<String, dynamic>(),
);
}
Future<SpotubePaginationResponseObject<SpotubeFullTrackObject>> 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<String, dynamic>(),
(Map json) =>
SpotubeFullTrackObject.fromJson(json.cast<String, dynamic>()),
);
}
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> 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<String, dynamic>(),
(Map json) =>
SpotubeSimpleAlbumObject.fromJson(json.cast<String, dynamic>()),
);
}
Future<void> save(List<String> ids) async {
await hetuMetadataAlbum.invoke(
"save",
positionalArgs: [ids],
);
}
Future<void> unsave(List<String> ids) async {
await hetuMetadataAlbum.invoke(
"unsave",
positionalArgs: [ids],
);
}
}

View File

@ -11,6 +11,7 @@ import 'package:spotube/collections/routes.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/metadata/apis/localstorage.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/auth.dart';
import 'package:spotube/services/metadata/endpoints/playlist.dart'; import 'package:spotube/services/metadata/endpoints/playlist.dart';
import 'package:spotube/services/metadata/endpoints/user.dart'; import 'package:spotube/services/metadata/endpoints/user.dart';
@ -73,11 +74,13 @@ class MetadataPlugin {
final Hetu hetu; final Hetu hetu;
late final MetadataAuthEndpoint auth; late final MetadataAuthEndpoint auth;
late final MetadataPluginUserEndpoint user; late final MetadataPluginAlbumEndpoint album;
late final MetadataPluginPlaylistEndpoint playlist; late final MetadataPluginPlaylistEndpoint playlist;
late final MetadataPluginUserEndpoint user;
MetadataPlugin._(this.hetu) { MetadataPlugin._(this.hetu) {
auth = MetadataAuthEndpoint(hetu); auth = MetadataAuthEndpoint(hetu);
album = MetadataPluginAlbumEndpoint(hetu);
playlist = MetadataPluginPlaylistEndpoint(hetu); playlist = MetadataPluginPlaylistEndpoint(hetu);
user = MetadataPluginUserEndpoint(hetu); user = MetadataPluginUserEndpoint(hetu);
} }