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: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
build() {
return null;
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> 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<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/services/metadata/endpoints/error.dart';
class FavoritePlaylistsNotifier
class MetadataPluginSavedPlaylistsNotifier
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
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<void> 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<SpotubeSimplePlaylistObject>;
@ -54,7 +54,7 @@ class FavoritePlaylistsNotifier
Future<void> 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<void> 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<void> addTracks(String playlistId, List<String> 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<void> removeTracks(String playlistId, List<String> 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<SpotubeSimplePlaylistObject>>(
() => FavoritePlaylistsNotifier(),
() => MetadataPluginSavedPlaylistsNotifier(),
);
final metadataPluginIsSavedPlaylistProvider =

View File

@ -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<SpotubeFullTrackObject>,
String>(
final metadataPluginPlaylistTracksProvider =
AutoDisposeAsyncNotifierProviderFamily<PlaylistTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
() => PlaylistTracksNotifier(),
);

View File

@ -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<K>
// ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
Future<MetadataPlugin?> get metadataPlugin async =>
await ref.read(metadataPluginProvider.future);
Future<MetadataPlugin> 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

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/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);
}