mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: add playlist endpoint and providers
This commit is contained in:
parent
f8211cbcc7
commit
a9ba2582fb
@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
||||
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/tracks/playlist.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
|
||||
@ -40,61 +41,63 @@ class FavoritePlaylistsNotifier
|
||||
);
|
||||
}
|
||||
|
||||
// Future<void> addFavorite(PlaylistSimple playlist) async {
|
||||
// await update((state) async {
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.followPlaylist(playlist.id!),
|
||||
// );
|
||||
// return state.copyWith(
|
||||
// items: [...state.items, playlist],
|
||||
// );
|
||||
// });
|
||||
Future<void> addFavorite(SpotubeSimplePlaylistObject playlist) async {
|
||||
await update((state) async {
|
||||
(await metadataPlugin)!.playlist.save(playlist.id);
|
||||
return state.copyWith(
|
||||
items: [...state.items, playlist],
|
||||
) as SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>;
|
||||
});
|
||||
|
||||
// ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
|
||||
// }
|
||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
||||
}
|
||||
|
||||
// Future<void> removeFavorite(PlaylistSimple playlist) async {
|
||||
// await update((state) async {
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.unfollowPlaylist(playlist.id!),
|
||||
// );
|
||||
// return state.copyWith(
|
||||
// items: state.items.where((e) => e.id != playlist.id).toList(),
|
||||
// );
|
||||
// });
|
||||
Future<void> removeFavorite(SpotubeSimplePlaylistObject playlist) async {
|
||||
await update((state) async {
|
||||
(await metadataPlugin)!.playlist.unsave(playlist.id);
|
||||
return state.copyWith(
|
||||
items: state.items
|
||||
.where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id)
|
||||
.toList() as List<SpotubeSimplePlaylistObject>,
|
||||
) as SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>;
|
||||
});
|
||||
|
||||
// ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
|
||||
// }
|
||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
||||
}
|
||||
|
||||
// Future<void> addTracks(String playlistId, List<String> trackIds) async {
|
||||
// if (state.value == null) return;
|
||||
Future<void> delete(SpotubeSimplePlaylistObject playlist) async {
|
||||
await update((state) async {
|
||||
(await metadataPlugin)!.playlist.deletePlaylist(playlist.id);
|
||||
return state.copyWith(
|
||||
items: state.items
|
||||
.where((e) => (e as SpotubeSimplePlaylistObject).id != playlist.id)
|
||||
.toList() as List<SpotubeSimplePlaylistObject>,
|
||||
) as SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>;
|
||||
});
|
||||
|
||||
// final spotify = ref.read(spotifyProvider);
|
||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlist.id));
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
|
||||
}
|
||||
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.addTracks(
|
||||
// trackIds.map((id) => 'spotify:track:$id').toList(),
|
||||
// playlistId,
|
||||
// ),
|
||||
// );
|
||||
Future<void> addTracks(String playlistId, List<String> trackIds) async {
|
||||
if (state.value == null) return;
|
||||
|
||||
// ref.invalidate(playlistTracksProvider(playlistId));
|
||||
// }
|
||||
await (await metadataPlugin)!
|
||||
.playlist
|
||||
.addTracks(playlistId, trackIds: trackIds);
|
||||
|
||||
// Future<void> removeTracks(String playlistId, List<String> trackIds) async {
|
||||
// if (state.value == null) return;
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||
}
|
||||
|
||||
// final spotify = ref.read(spotifyProvider);
|
||||
Future<void> removeTracks(String playlistId, List<String> trackIds) async {
|
||||
if (state.value == null) return;
|
||||
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.removeTracks(
|
||||
// trackIds.map((id) => 'spotify:track:$id').toList(),
|
||||
// playlistId,
|
||||
// ),
|
||||
// );
|
||||
await (await metadataPlugin)!
|
||||
.playlist
|
||||
.removeTracks(playlistId, trackIds: trackIds);
|
||||
|
||||
// ref.invalidate(playlistTracksProvider(playlistId));
|
||||
// }
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||
}
|
||||
}
|
||||
|
||||
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
|
||||
|
36
lib/provider/metadata_plugin/tracks/playlist.dart
Normal file
36
lib/provider/metadata_plugin/tracks/playlist.dart
Normal file
@ -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 PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
SpotubeFullTrackObject, String> {
|
||||
PlaylistTracksNotifier() : super();
|
||||
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
final tracks = await (await metadataPlugin)!.playlist.tracks(
|
||||
arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
ref.cacheFor();
|
||||
|
||||
ref.watch(metadataPluginProvider);
|
||||
return await fetch(0, 20);
|
||||
}
|
||||
}
|
||||
|
||||
final metadataPluginPlaylistTracksProvider = AutoDisposeAsyncNotifierProviderFamily<
|
||||
PlaylistTracksNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>,
|
||||
String>(
|
||||
() => PlaylistTracksNotifier(),
|
||||
);
|
44
lib/provider/metadata_plugin/utils/common.dart
Normal file
44
lib/provider/metadata_plugin/utils/common.dart
Normal file
@ -0,0 +1,44 @@
|
||||
// ignore: implementation_imports
|
||||
import 'package:riverpod/src/async_notifier.dart';
|
||||
import 'dart:async';
|
||||
|
||||
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/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);
|
||||
}
|
||||
|
||||
extension AutoDisposeAsyncNotifierCacheFor
|
||||
// ignore: deprecated_member_use
|
||||
on AutoDisposeAsyncNotifierProviderRef {
|
||||
// When invoked keeps your provider alive for [duration]
|
||||
// ignore: unused_element
|
||||
void cacheFor([Duration duration = const Duration(minutes: 5)]) {
|
||||
final link = keepAlive();
|
||||
final timer = Timer(duration, () => link.close());
|
||||
onDispose(() => timer.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
extension AutoDisposeCacheFor on AutoDisposeRef {
|
||||
// When invoked keeps your provider alive for [duration]
|
||||
// ignore: unused_element
|
||||
void cacheFor([Duration duration = const Duration(minutes: 5)]) {
|
||||
final link = keepAlive();
|
||||
final timer = Timer(duration, () => link.close());
|
||||
onDispose(() => timer.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: subtype_of_sealed_class
|
||||
class AsyncLoadingNext<T> extends AsyncData<T> {
|
||||
const AsyncLoadingNext(super.value);
|
||||
}
|
103
lib/provider/metadata_plugin/utils/family_paginated.dart
Normal file
103
lib/provider/metadata_plugin/utils/family_paginated.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:riverpod/src/async_notifier.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
|
||||
abstract class FamilyPaginatedAsyncNotifier<K, A>
|
||||
extends FamilyAsyncNotifier<SpotubePaginationResponseObject<K>, A>
|
||||
with MetadataPluginMixin<K> {
|
||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
|
||||
state = AsyncLoadingNext(state.asData!.value);
|
||||
|
||||
state = await AsyncValue.guard(
|
||||
() async {
|
||||
final newState = await fetch(
|
||||
state.value!.nextOffset!,
|
||||
state.value!.limit,
|
||||
);
|
||||
return newState.copyWith(items: [
|
||||
...state.value!.items as List<K>,
|
||||
...newState.items as List<K>,
|
||||
]) as SpotubePaginationResponseObject<K>;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<K>> fetchAll() async {
|
||||
if (state.value == null) return [];
|
||||
if (!state.value!.hasMore) return state.value!.items as List<K>;
|
||||
|
||||
bool hasMore = true;
|
||||
while (hasMore) {
|
||||
await update((state) async {
|
||||
final newState = await fetch(
|
||||
state.nextOffset!,
|
||||
state.limit,
|
||||
);
|
||||
|
||||
hasMore = newState.hasMore;
|
||||
return newState.copyWith(items: [
|
||||
...state.items as List<K>,
|
||||
...newState.items as List<K>,
|
||||
]) as SpotubePaginationResponseObject<K>;
|
||||
});
|
||||
}
|
||||
|
||||
return state.value!.items as List<K>;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AutoDisposeFamilyPaginatedAsyncNotifier<K, A>
|
||||
extends AutoDisposeFamilyAsyncNotifier<SpotubePaginationResponseObject<K>,
|
||||
A> with MetadataPluginMixin<K> {
|
||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
|
||||
state = AsyncLoadingNext(state.asData!.value);
|
||||
|
||||
state = await AsyncValue.guard(
|
||||
() async {
|
||||
final newState = await fetch(
|
||||
state.value!.nextOffset!,
|
||||
state.value!.limit,
|
||||
);
|
||||
return newState.copyWith(items: [
|
||||
...state.value!.items as List<K>,
|
||||
...newState.items as List<K>,
|
||||
]) as SpotubePaginationResponseObject<K>;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<K>> fetchAll() async {
|
||||
if (state.value == null) return [];
|
||||
if (!state.value!.hasMore) return state.value!.items as List<K>;
|
||||
|
||||
bool hasMore = true;
|
||||
while (hasMore) {
|
||||
await update((state) async {
|
||||
final newState = await fetch(
|
||||
state.nextOffset!,
|
||||
state.limit,
|
||||
);
|
||||
|
||||
hasMore = newState.hasMore;
|
||||
return newState.copyWith(items: [
|
||||
...state.items as List<K>,
|
||||
...newState.items as List<K>,
|
||||
]) as SpotubePaginationResponseObject<K>;
|
||||
});
|
||||
}
|
||||
|
||||
return state.value!.items as List<K>;
|
||||
}
|
||||
}
|
@ -4,42 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:riverpod/src/async_notifier.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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);
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
extension on AutoDisposeAsyncNotifierProviderRef {
|
||||
// When invoked keeps your provider alive for [duration]
|
||||
// ignore: unused_element
|
||||
void cacheFor([Duration duration = const Duration(minutes: 5)]) {
|
||||
final link = keepAlive();
|
||||
final timer = Timer(duration, () => link.close());
|
||||
onDispose(() => timer.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
extension on AutoDisposeRef {
|
||||
// When invoked keeps your provider alive for [duration]
|
||||
// ignore: unused_element
|
||||
void cacheFor([Duration duration = const Duration(minutes: 5)]) {
|
||||
final link = keepAlive();
|
||||
final timer = Timer(duration, () => link.close());
|
||||
onDispose(() => timer.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: subtype_of_sealed_class
|
||||
class AsyncLoadingNext<T> extends AsyncData<T> {
|
||||
const AsyncLoadingNext(super.value);
|
||||
}
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
|
||||
mixin PaginatedAsyncNotifierMixin<K>
|
||||
// ignore: invalid_use_of_internal_member
|
||||
|
@ -0,0 +1,135 @@
|
||||
import 'package:hetu_script/hetu_script.dart';
|
||||
import 'package:hetu_script/values.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
|
||||
class MetadataPluginPlaylistEndpoint {
|
||||
final Hetu hetu;
|
||||
MetadataPluginPlaylistEndpoint(this.hetu);
|
||||
|
||||
HTInstance get hetuMetadataPlaylist =>
|
||||
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("playlist")
|
||||
as HTInstance;
|
||||
|
||||
Future<SpotubeFullPlaylistObject> getPlaylist(String id) async {
|
||||
final raw = await hetuMetadataPlaylist
|
||||
.invoke("getPlaylist", positionalArgs: [id]) as Map;
|
||||
|
||||
return SpotubeFullPlaylistObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubePaginationResponseObject<SpotubeFullTrackObject>> tracks(
|
||||
String id, {
|
||||
int? offset,
|
||||
int? limit,
|
||||
}) async {
|
||||
final raw = await hetuMetadataPlaylist.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<SpotubeFullPlaylistObject?> create(
|
||||
String userId, {
|
||||
required String name,
|
||||
String? description,
|
||||
bool? public,
|
||||
bool? collaborative,
|
||||
}) async {
|
||||
final raw = await hetuMetadataPlaylist.invoke(
|
||||
"create",
|
||||
positionalArgs: [userId],
|
||||
namedArgs: {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"public": public,
|
||||
"collaborative": collaborative,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
) as Map?;
|
||||
|
||||
if (raw == null) return null;
|
||||
|
||||
return SpotubeFullPlaylistObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> update(
|
||||
String playlistId, {
|
||||
String? name,
|
||||
String? description,
|
||||
bool? public,
|
||||
bool? collaborative,
|
||||
}) async {
|
||||
await hetuMetadataPlaylist.invoke(
|
||||
"update",
|
||||
positionalArgs: [playlistId],
|
||||
namedArgs: {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"public": public,
|
||||
"collaborative": collaborative,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addTracks(
|
||||
String playlistId, {
|
||||
required List<String> trackIds,
|
||||
int? position,
|
||||
}) async {
|
||||
await hetuMetadataPlaylist.invoke(
|
||||
"addTracks",
|
||||
positionalArgs: [playlistId],
|
||||
namedArgs: {
|
||||
"trackIds": trackIds,
|
||||
"position": position,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeTracks(
|
||||
String playlistId, {
|
||||
required List<String> trackIds,
|
||||
}) async {
|
||||
await hetuMetadataPlaylist.invoke(
|
||||
"removeTracks",
|
||||
positionalArgs: [playlistId],
|
||||
namedArgs: {
|
||||
"trackIds": trackIds,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> save(String playlistId) async {
|
||||
await hetuMetadataPlaylist.invoke(
|
||||
"save",
|
||||
positionalArgs: [playlistId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> unsave(String playlistId) async {
|
||||
await hetuMetadataPlaylist.invoke(
|
||||
"unsave",
|
||||
positionalArgs: [playlistId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deletePlaylist(String playlistId) async {
|
||||
await hetuMetadataPlaylist.invoke(
|
||||
"deletePlaylist",
|
||||
positionalArgs: [playlistId],
|
||||
);
|
||||
}
|
||||
}
|
@ -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/auth.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/playlist.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/user.dart';
|
||||
|
||||
const defaultMetadataLimit = "20";
|
||||
@ -73,9 +74,11 @@ class MetadataPlugin {
|
||||
|
||||
late final MetadataAuthEndpoint auth;
|
||||
late final MetadataPluginUserEndpoint user;
|
||||
late final MetadataPluginPlaylistEndpoint playlist;
|
||||
|
||||
MetadataPlugin._(this.hetu) {
|
||||
auth = MetadataAuthEndpoint(hetu);
|
||||
playlist = MetadataPluginPlaylistEndpoint(hetu);
|
||||
user = MetadataPluginUserEndpoint(hetu);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user