feat: remove hetu and use rquickjs based frb bindings for plugin methods

This commit is contained in:
Kingkor Roy Tirtho 2025-12-09 20:35:23 +06:00
parent 2b9c5730c9
commit bd2275a89f
153 changed files with 4232 additions and 9333 deletions

View File

@ -3,13 +3,15 @@ import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/history/summary.dart';
abstract class FakeData {
static final SpotubeImageObject image = SpotubeImageObject(
static const SpotubeImageObject image = SpotubeImageObject(
typeName: "image",
height: 100,
width: 100,
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
);
static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
static const SpotubeFullArtistObject artist = SpotubeFullArtistObject(
typeName: "artist_full",
id: "1",
name: "What an artist",
externalUri: "https://example.com",
@ -17,6 +19,7 @@ abstract class FakeData {
genres: ["genre"],
images: [
SpotubeImageObject(
typeName: "image",
height: 100,
width: 100,
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
@ -24,7 +27,8 @@ abstract class FakeData {
],
);
static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
static const SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
typeName: "album_full",
id: "1",
name: "A good album",
externalUri: "https://example.com",
@ -37,15 +41,17 @@ abstract class FakeData {
recordLabel: "Record Label",
);
static final SpotubeSimpleArtistObject artistSimple =
static const SpotubeSimpleArtistObject artistSimple =
SpotubeSimpleArtistObject(
typeName: "artist_simple",
id: "1",
name: "What an artist",
externalUri: "https://example.com",
images: null,
);
static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
static const SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
typeName: "album_simple",
albumType: SpotubeAlbumType.album,
artists: [],
externalUri: "https://example.com",
@ -54,6 +60,7 @@ abstract class FakeData {
releaseDate: "2021-01-01",
images: [
SpotubeImageObject(
typeName: "image",
height: 1,
width: 1,
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
@ -61,24 +68,30 @@ abstract class FakeData {
],
);
static final SpotubeFullTrackObject track = SpotubeTrackObject.full(
id: "1",
name: "A good track",
externalUri: "https://example.com",
album: albumSimple,
durationMs: 3 * 60 * 1000, // 3 minutes
isrc: "USUM72112345",
explicit: false,
) as SpotubeFullTrackObject;
static const SpotubeTrackObject track = SpotubeTrackObject.full(
SpotubeFullTrackObject(
typeName: "track",
id: "1",
name: "A good track",
externalUri: "https://example.com",
album: albumSimple,
durationMs: 3 * 60 * 1000, // 3 minutes
isrc: "USUM72112345",
explicit: false,
artists: [artistSimple],
),
);
static final SpotubeUserObject user = SpotubeUserObject(
static const SpotubeUserObject user = SpotubeUserObject(
typeName: "user",
id: "1",
name: "User Name",
externalUri: "https://example.com",
images: [image],
);
static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
static const SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
typeName: "playlist_full",
id: "1",
name: "A good playlist",
description: "A very good playlist description",
@ -89,8 +102,9 @@ abstract class FakeData {
images: [image],
collaborators: [user]);
static final SpotubeSimplePlaylistObject playlistSimple =
static const SpotubeSimplePlaylistObject playlistSimple =
SpotubeSimplePlaylistObject(
typeName: "playlist_simple",
id: "1",
name: "A good playlist",
description: "A very good playlist description",
@ -99,13 +113,18 @@ abstract class FakeData {
images: [image],
);
static final SpotubeBrowseSectionObject browseSection =
static const SpotubeBrowseSectionObject browseSection =
SpotubeBrowseSectionObject(
typeName: "browse_section",
id: "section-id",
title: "Browse Section",
browseMore: true,
externalUri: "https://example.com/browse/section",
items: [playlistSimple, playlistSimple, playlistSimple]);
items: [
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
]);
static const historySummary = PlaybackHistorySummary(
albums: 1,

View File

@ -225,7 +225,7 @@ class HomeBrowseSectionItemsRoute
HomeBrowseSectionItemsRoute({
_i44.Key? key,
required String sectionId,
required _i43.SpotubeBrowseSectionObject<Object> section,
required _i43.SpotubeBrowseSectionObject section,
List<_i41.PageRouteInfo>? children,
}) : super(
HomeBrowseSectionItemsRoute.name,
@ -264,7 +264,7 @@ class HomeBrowseSectionItemsRouteArgs {
final String sectionId;
final _i43.SpotubeBrowseSectionObject<Object> section;
final _i43.SpotubeBrowseSectionObject section;
@override
String toString() {
@ -632,7 +632,7 @@ class SettingsMetadataProviderFormRoute
SettingsMetadataProviderFormRoute({
_i44.Key? key,
required String title,
required List<_i43.MetadataFormFieldObject> fields,
required List<void> fields,
List<_i41.PageRouteInfo>? children,
}) : super(
SettingsMetadataProviderFormRoute.name,
@ -670,7 +670,7 @@ class SettingsMetadataProviderFormRouteArgs {
final String title;
final List<_i43.MetadataFormFieldObject> fields;
final List<void> fields;
@override
String toString() {

View File

@ -7,7 +7,7 @@ import 'package:spotube/models/metadata/metadata.dart';
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
class ReplaceDownloadedDialog extends ConsumerWidget {
final SpotubeTrackObject track;
final SpotubeFullTrackObject track;
const ReplaceDownloadedDialog({required this.track, super.key});
@override

View File

@ -37,7 +37,8 @@ class TrackDetailsDialog extends HookConsumerWidget {
// style: const TextStyle(color: Colors.blue),
// ),
context.l10n.duration: sourcedTrack.asData != null
? sourcedTrack.asData!.value.info.duration.toHumanReadableString()
? Duration(milliseconds: sourcedTrack.asData!.value.info.duration)
.toHumanReadableString()
: Duration(milliseconds: track.durationMs).toHumanReadableString(),
if (track.album.releaseDate != null)
context.l10n.released: track.album.releaseDate,

View File

@ -65,7 +65,7 @@ class HeartButton extends HookConsumerWidget {
}
class TrackHeartButton extends HookConsumerWidget {
final SpotubeTrackObject track;
final SpotubeFullTrackObject track;
const TrackHeartButton({
super.key,
required this.track,

View File

@ -5,10 +5,11 @@ import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
typedef UseTrackToggleLike = ({
bool isLiked,
bool isLoading,
Future<void> Function(SpotubeTrackObject track) toggleTrackLike,
Future<void> Function(SpotubeFullTrackObject track) toggleTrackLike,
});
UseTrackToggleLike useTrackToggleLike(SpotubeTrackObject track, WidgetRef ref) {
UseTrackToggleLike useTrackToggleLike(
SpotubeFullTrackObject track, WidgetRef ref) {
final savedTracksNotifier =
ref.watch(metadataPluginSavedTracksProvider.notifier);

View File

@ -80,7 +80,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
required String action,
}) async {
final fullTrackObjects =
tracks.whereType<SpotubeFullTrackObject>().toList();
tracks.whereType<SpotubeTrackObject_Full>().toList();
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
@ -89,7 +89,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
) ??
false;
if (confirmed != true) return;
downloader.addAllToQueue(fullTrackObjects);
downloader.addAllToQueue(fullTrackObjects.map((e) => e.field0).toList());
notifier.deselectAllTracks();
if (!context.mounted) return;
showToastForAction(context, action, fullTrackObjects.length);

View File

@ -13,6 +13,7 @@ import 'package:spotube/components/track_presentation/use_track_tile_play_callba
import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';

View File

@ -8,7 +8,7 @@ class PaginationProps {
final bool isLoading;
final VoidCallback onFetchMore;
final Future<void> Function() onRefresh;
final Future<List<SpotubeFullTrackObject>> Function() onFetchAll;
final Future<List<SpotubeTrackObject>> Function() onFetchAll;
const PaginationProps({
required this.hasNextPage,
@ -46,7 +46,7 @@ class TrackPresentationOptions {
final String? ownerImage;
final String image;
final String routePath;
final List<SpotubeFullTrackObject> tracks;
final List<SpotubeTrackObject> tracks;
final PaginationProps pagination;
final bool isLiked;
final String? shareUrl;

View File

@ -44,7 +44,7 @@ class PresentationStateNotifier
next.whenData((value) {
state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks(
value.items,
value.items.union(),
state.sortBy,
),
);
@ -62,7 +62,7 @@ class PresentationStateNotifier
next.whenData((value) {
state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks(
value.items,
value.items.union(),
state.sortBy,
),
);
@ -109,7 +109,7 @@ class PresentationStateNotifier
} ??
<SpotubeFullTrackObject>[];
return tracks;
return tracks.union();
}
void selectTrack(SpotubeTrackObject track) {

View File

@ -11,6 +11,7 @@ import 'package:spotube/components/track_presentation/use_action_callbacks.dart'
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
class TrackPresentationTopSection extends HookConsumerWidget {

View File

@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/track_options/track_options_provider.dart';
/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
/// [track] must be a [SpotubeTrackObject] or [SpotubeLocalTrackObject]
class TrackOptions extends HookConsumerWidget {
final SpotubeTrackObject track;
final bool userPlaylist;
@ -26,8 +26,8 @@ class TrackOptions extends HookConsumerWidget {
this.icon,
this.onTapItem,
}) : assert(
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
"Track must be a SpotubeTrackObject, SpotubeLocalTrackObject",
);
@override

View File

@ -1,3 +1,4 @@
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@ -24,12 +25,15 @@ void useEndlessPlayback(WidgetRef ref) {
final track = playlist.tracks.last;
final tracks = await (await metadataPlugin)?.track.radio(track.id);
final tracks = await metadataPlugin.then(
(plugin) async =>
plugin?.track.radio(id: track.id, mpscTx: plugin.sender),
);
if (tracks == null || tracks.isEmpty) return;
await playback.addTracks(
tracks.toList()
tracks.union()
..removeWhere((e) {
final playlist = ref.read(audioPlayerProvider);
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);

View File

@ -33,23 +33,23 @@ WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
/// @nodoc
mixin _$WebSocketLoadEventData {
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError;
Object? get collection => throw _privateConstructorUsedError;
int? get initialIndex => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)
playlist,
required TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)
@ -59,15 +59,15 @@ mixin _$WebSocketLoadEventData {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)?
playlist,
TResult? Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)?
@ -77,15 +77,15 @@ mixin _$WebSocketLoadEventData {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)?
playlist,
TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)?
@ -130,8 +130,8 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> {
_$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>;
@useResult
$Res call(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
int? initialIndex});
}
@ -178,8 +178,8 @@ abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res>
@override
@useResult
$Res call(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex});
@ -243,8 +243,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
class _$WebSocketLoadEventDataPlaylistImpl
extends WebSocketLoadEventDataPlaylist {
_$WebSocketLoadEventDataPlaylistImpl(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
required final List<SpotubeTrackObject> tracks,
this.collection,
this.initialIndex,
@ -259,8 +259,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
final List<SpotubeTrackObject> _tracks;
@override
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> get tracks {
if (_tracks is EqualUnmodifiableListView) return _tracks;
// ignore: implicit_dynamic_type
@ -311,15 +311,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)
playlist,
required TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)
@ -332,15 +332,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)?
playlist,
TResult? Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)?
@ -353,15 +353,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)?
playlist,
TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)?
@ -415,8 +415,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
factory WebSocketLoadEventDataPlaylist(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
required final List<SpotubeTrackObject> tracks,
final SpotubeSimplePlaylistObject? collection,
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
@ -426,8 +426,8 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
@override
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> get tracks;
@override
SpotubeSimplePlaylistObject? get collection;
@ -453,8 +453,8 @@ abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res>
@override
@useResult
$Res call(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex});
@ -516,8 +516,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
@JsonSerializable()
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
_$WebSocketLoadEventDataAlbumImpl(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
required final List<SpotubeTrackObject> tracks,
this.collection,
this.initialIndex,
@ -532,8 +532,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
final List<SpotubeTrackObject> _tracks;
@override
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> get tracks {
if (_tracks is EqualUnmodifiableListView) return _tracks;
// ignore: implicit_dynamic_type
@ -583,15 +583,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)
playlist,
required TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)
@ -604,15 +604,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)?
playlist,
TResult? Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)?
@ -625,15 +625,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
int? initialIndex)?
playlist,
TResult Function(
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,
int? initialIndex)?
@ -687,8 +687,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
factory WebSocketLoadEventDataAlbum(
{@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
{@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
required final List<SpotubeTrackObject> tracks,
final SpotubeSimpleAlbumObject? collection,
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
@ -698,8 +698,8 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
_$WebSocketLoadEventDataAlbumImpl.fromJson;
@override
@Assert("tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject")
@Assert("tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject")
List<SpotubeTrackObject> get tracks;
@override
SpotubeSimpleAlbumObject? get collection;

View File

@ -6,8 +6,8 @@ class WebSocketLoadEventData with _$WebSocketLoadEventData {
factory WebSocketLoadEventData.playlist({
@Assert(
"tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject",
"tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject",
)
required List<SpotubeTrackObject> tracks,
SpotubeSimplePlaylistObject? collection,
@ -16,8 +16,8 @@ class WebSocketLoadEventData with _$WebSocketLoadEventData {
factory WebSocketLoadEventData.album({
@Assert(
"tracks is List<SpotubeFullTrackObject>",
"tracks must be a list of SpotubeFullTrackObject",
"tracks is List<SpotubeTrackObject>",
"tracks must be a list of SpotubeTrackObject",
)
required List<SpotubeTrackObject> tracks,
SpotubeSimpleAlbumObject? collection,

View File

@ -338,14 +338,14 @@ class WebSocketRemoveTrackEvent extends WebSocketEvent<String> {
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
}
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeFullTrackObject> {
WebSocketAddTrackEvent(SpotubeFullTrackObject data)
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeTrackObject> {
WebSocketAddTrackEvent(SpotubeTrackObject data)
: super(WsEvent.addTrack, data);
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
: super(
WsEvent.addTrack,
SpotubeFullTrackObject.fromJson(
SpotubeTrackObject.fromJson(
json["data"] as Map<String, dynamic>,
),
);

View File

@ -1,42 +0,0 @@
part of 'metadata.dart';
enum SpotubeAlbumType {
album,
single,
compilation,
}
@freezed
class SpotubeFullAlbumObject with _$SpotubeFullAlbumObject {
factory SpotubeFullAlbumObject({
required String id,
required String name,
required List<SpotubeSimpleArtistObject> artists,
@Default([]) List<SpotubeImageObject> images,
required String releaseDate,
required String externalUri,
required int totalTracks,
required SpotubeAlbumType albumType,
String? recordLabel,
List<String>? genres,
}) = _SpotubeFullAlbumObject;
factory SpotubeFullAlbumObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeFullAlbumObjectFromJson(json);
}
@freezed
class SpotubeSimpleAlbumObject with _$SpotubeSimpleAlbumObject {
factory SpotubeSimpleAlbumObject({
required String id,
required String name,
required String externalUri,
required List<SpotubeSimpleArtistObject> artists,
@Default([]) List<SpotubeImageObject> images,
required SpotubeAlbumType albumType,
String? releaseDate,
}) = _SpotubeSimpleAlbumObject;
factory SpotubeSimpleAlbumObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeSimpleAlbumObjectFromJson(json);
}

View File

@ -1,33 +1,5 @@
part of 'metadata.dart';
@freezed
class SpotubeFullArtistObject with _$SpotubeFullArtistObject {
factory SpotubeFullArtistObject({
required String id,
required String name,
required String externalUri,
@Default([]) List<SpotubeImageObject> images,
List<String>? genres,
int? followers,
}) = _SpotubeFullArtistObject;
factory SpotubeFullArtistObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeFullArtistObjectFromJson(json);
}
@freezed
class SpotubeSimpleArtistObject with _$SpotubeSimpleArtistObject {
factory SpotubeSimpleArtistObject({
required String id,
required String name,
required String externalUri,
List<SpotubeImageObject>? images,
}) = _SpotubeSimpleArtistObject;
factory SpotubeSimpleArtistObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeSimpleArtistObjectFromJson(json);
}
extension SpotubeFullArtistObjectAsString on List<SpotubeFullArtistObject> {
String asString() {
return map((e) => e.name).join(", ");

View File

@ -7,29 +7,7 @@ enum SpotubeMediaCompressionType {
lossless,
}
@Freezed(unionKey: 'type')
class SpotubeAudioSourceContainerPreset
with _$SpotubeAudioSourceContainerPreset {
const SpotubeAudioSourceContainerPreset._();
@FreezedUnionValue("lossy")
factory SpotubeAudioSourceContainerPreset.lossy({
required SpotubeMediaCompressionType type,
required String name,
required List<SpotubeAudioLossyContainerQuality> qualities,
}) = SpotubeAudioSourceContainerPresetLossy;
@FreezedUnionValue("lossless")
factory SpotubeAudioSourceContainerPreset.lossless({
required SpotubeMediaCompressionType type,
required String name,
required List<SpotubeAudioLosslessContainerQuality> qualities,
}) = SpotubeAudioSourceContainerPresetLossless;
factory SpotubeAudioSourceContainerPreset.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioSourceContainerPresetFromJson(json);
extension GetFileExtension on SpotubeAudioSourceContainerPreset {
String getFileExtension() {
return switch (name) {
"mp4" => "m4a",
@ -39,72 +17,16 @@ class SpotubeAudioSourceContainerPreset
}
}
@freezed
class SpotubeAudioLossyContainerQuality
with _$SpotubeAudioLossyContainerQuality {
const SpotubeAudioLossyContainerQuality._();
factory SpotubeAudioLossyContainerQuality({
required int bitrate, // bits per second
}) = _SpotubeAudioLossyContainerQuality;
factory SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioLossyContainerQualityFromJson(json);
@override
toString() {
extension ToStringSpotubeAudioLossyContainerQuality
on SpotubeAudioLossyContainerQuality {
toFormattedString() {
return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps";
}
}
@freezed
class SpotubeAudioLosslessContainerQuality
with _$SpotubeAudioLosslessContainerQuality {
const SpotubeAudioLosslessContainerQuality._();
factory SpotubeAudioLosslessContainerQuality({
required int bitDepth, // bit
required int sampleRate, // hz
}) = _SpotubeAudioLosslessContainerQuality;
factory SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioLosslessContainerQualityFromJson(json);
@override
toString() {
extension ToStringSpotubeAudioLosslessContainerQuality
on SpotubeAudioLosslessContainerQuality {
toFormattedString() {
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz";
}
}
@freezed
class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject {
factory SpotubeAudioSourceMatchObject({
required String id,
required String title,
required List<String> artists,
required Duration duration,
String? thumbnail,
required String externalUri,
}) = _SpotubeAudioSourceMatchObject;
factory SpotubeAudioSourceMatchObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeAudioSourceMatchObjectFromJson(json);
}
@freezed
class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject {
factory SpotubeAudioSourceStreamObject({
required String url,
required String container,
required SpotubeMediaCompressionType type,
String? codec,
double? bitrate,
int? bitDepth,
double? sampleRate,
}) = _SpotubeAudioSourceStreamObject;
factory SpotubeAudioSourceStreamObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeAudioSourceStreamObjectFromJson(json);
}

View File

@ -1,21 +1,79 @@
part of 'metadata.dart';
@Freezed(genericArgumentFactories: true)
class SpotubeBrowseSectionObject<T> with _$SpotubeBrowseSectionObject<T> {
factory SpotubeBrowseSectionObject({
required String id,
required String title,
required String externalUri,
required bool browseMore,
required List<T> items,
}) = _SpotubeBrowseSectionObject<T>;
class SpotubeFlattenedBrowseSectionObject<T> {
final String id;
final String title;
final String externalUri;
final bool browseMore;
final List<T> items;
factory SpotubeBrowseSectionObject.fromJson(
Map<String, Object?> json,
T Function(Map<String, dynamic> json) fromJsonT,
) =>
_$SpotubeBrowseSectionObjectFromJson<T>(
json,
(json) => fromJsonT(json as Map<String, dynamic>),
);
SpotubeFlattenedBrowseSectionObject({
required this.id,
required this.title,
required this.browseMore,
required this.externalUri,
required this.items,
});
static SpotubeFlattenedBrowseSectionObject<T> from<T>(
SpotubeBrowseSectionObject browseSection,
T Function(SpotubeBrowseSectionResponseObjectItem item) parse,
) {
return SpotubeFlattenedBrowseSectionObject<T>(
browseMore: browseSection.browseMore,
id: browseSection.id,
title: browseSection.title,
externalUri: browseSection.externalUri,
items: browseSection.items
.map((item) => parse(item))
.toList(growable: false),
);
}
SpotubeFlattenedBrowseSectionObject<T> copyWith({
String? id,
String? title,
String? externalUri,
bool? browseMore,
List<T>? items,
}) {
return SpotubeFlattenedBrowseSectionObject<T>(
id: id ?? this.id,
title: title ?? this.title,
externalUri: externalUri ?? this.externalUri,
browseMore: browseMore ?? this.browseMore,
items: items ?? this.items,
);
}
}
extension SpotubeBrowseSectionObjectExtension on SpotubeBrowseSectionObject {
SpotubeFlattenedBrowseSectionObject<T> flatten<T>() {
return SpotubeFlattenedBrowseSectionObject.from<T>(
this,
(item) => switch (T) {
SpotubeSimpleAlbumObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_AlbumSimple).field0
as T,
SpotubeFullAlbumObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_AlbumFull).field0
as T,
SpotubeSimpleArtistObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_ArtistSimple).field0
as T,
SpotubeFullArtistObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_ArtistFull).field0
as T,
SpotubeTrackObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_Track).field0 as T,
SpotubeSimplePlaylistObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_PlaylistSimple).field0
as T,
SpotubeFullPlaylistObject() =>
(item as SpotubeBrowseSectionResponseObjectItem_PlaylistFull).field0
as T,
_ => throw Exception("Unsupported type: $T"),
},
);
}
}

View File

@ -1,17 +1,5 @@
part of 'metadata.dart';
@freezed
class SpotubeImageObject with _$SpotubeImageObject {
factory SpotubeImageObject({
required String url,
int? width,
int? height,
}) = _SpotubeImageObject;
factory SpotubeImageObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeImageObjectFromJson(json);
}
enum ImagePlaceholder {
albumArt,
artist,

View File

@ -10,23 +10,31 @@ import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/primitive_utils.dart';
export "package:spotube/src/rust/api/plugin/models/album.dart";
export "package:spotube/src/rust/api/plugin/models/audio_source.dart";
export "package:spotube/src/rust/api/plugin/models/artist.dart";
export "package:spotube/src/rust/api/plugin/models/auth.dart";
export "package:spotube/src/rust/api/plugin/models/browse.dart";
export "package:spotube/src/rust/api/plugin/models/core.dart";
export "package:spotube/src/rust/api/plugin/models/playlist.dart";
export "package:spotube/src/rust/api/plugin/models/track.dart";
export "package:spotube/src/rust/api/plugin/models/user.dart";
export "package:spotube/src/rust/api/plugin/models/image.dart";
export "package:spotube/src/rust/api/plugin/models/pagination.dart";
export "package:spotube/src/rust/api/plugin/models/search.dart";
part 'metadata.g.dart';
part 'metadata.freezed.dart';
part 'audio_source.dart';
part 'album.dart';
part 'artist.dart';
part 'audio_source.dart';
part 'browse.dart';
part 'fields.dart';
part 'image.dart';
part 'pagination.dart';
part 'playlist.dart';
part 'search.dart';
part 'track.dart';
part 'user.dart';
part 'plugin.dart';
part 'repository.dart';

File diff suppressed because it is too large Load Diff

View File

@ -6,270 +6,6 @@ part of 'metadata.dart';
// JsonSerializableGenerator
// **************************************************************************
_$SpotubeAudioSourceContainerPresetLossyImpl
_$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) =>
_$SpotubeAudioSourceContainerPresetLossyImpl(
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
name: json['name'] as String,
qualities: (json['qualities'] as List<dynamic>)
.map((e) => SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLossyImplToJson(
_$SpotubeAudioSourceContainerPresetLossyImpl instance) =>
<String, dynamic>{
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
'name': instance.name,
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
};
const _$SpotubeMediaCompressionTypeEnumMap = {
SpotubeMediaCompressionType.lossy: 'lossy',
SpotubeMediaCompressionType.lossless: 'lossless',
};
_$SpotubeAudioSourceContainerPresetLosslessImpl
_$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) =>
_$SpotubeAudioSourceContainerPresetLosslessImpl(
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
name: json['name'] as String,
qualities: (json['qualities'] as List<dynamic>)
.map((e) => SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLosslessImplToJson(
_$SpotubeAudioSourceContainerPresetLosslessImpl instance) =>
<String, dynamic>{
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
'name': instance.name,
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
};
_$SpotubeAudioLossyContainerQualityImpl
_$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) =>
_$SpotubeAudioLossyContainerQualityImpl(
bitrate: (json['bitrate'] as num).toInt(),
);
Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson(
_$SpotubeAudioLossyContainerQualityImpl instance) =>
<String, dynamic>{
'bitrate': instance.bitrate,
};
_$SpotubeAudioLosslessContainerQualityImpl
_$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) =>
_$SpotubeAudioLosslessContainerQualityImpl(
bitDepth: (json['bitDepth'] as num).toInt(),
sampleRate: (json['sampleRate'] as num).toInt(),
);
Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson(
_$SpotubeAudioLosslessContainerQualityImpl instance) =>
<String, dynamic>{
'bitDepth': instance.bitDepth,
'sampleRate': instance.sampleRate,
};
_$SpotubeAudioSourceMatchObjectImpl
_$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) =>
_$SpotubeAudioSourceMatchObjectImpl(
id: json['id'] as String,
title: json['title'] as String,
artists: (json['artists'] as List<dynamic>)
.map((e) => e as String)
.toList(),
duration: Duration(microseconds: (json['duration'] as num).toInt()),
thumbnail: json['thumbnail'] as String?,
externalUri: json['externalUri'] as String,
);
Map<String, dynamic> _$$SpotubeAudioSourceMatchObjectImplToJson(
_$SpotubeAudioSourceMatchObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'artists': instance.artists,
'duration': instance.duration.inMicroseconds,
'thumbnail': instance.thumbnail,
'externalUri': instance.externalUri,
};
_$SpotubeAudioSourceStreamObjectImpl
_$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) =>
_$SpotubeAudioSourceStreamObjectImpl(
url: json['url'] as String,
container: json['container'] as String,
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
codec: json['codec'] as String?,
bitrate: (json['bitrate'] as num?)?.toDouble(),
bitDepth: (json['bitDepth'] as num?)?.toInt(),
sampleRate: (json['sampleRate'] as num?)?.toDouble(),
);
Map<String, dynamic> _$$SpotubeAudioSourceStreamObjectImplToJson(
_$SpotubeAudioSourceStreamObjectImpl instance) =>
<String, dynamic>{
'url': instance.url,
'container': instance.container,
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
'codec': instance.codec,
'bitrate': instance.bitrate,
'bitDepth': instance.bitDepth,
'sampleRate': instance.sampleRate,
};
_$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) =>
_$SpotubeFullAlbumObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
artists: (json['artists'] as List<dynamic>)
.map((e) => SpotubeSimpleArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
releaseDate: json['releaseDate'] as String,
externalUri: json['externalUri'] as String,
totalTracks: (json['totalTracks'] as num).toInt(),
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
recordLabel: json['recordLabel'] as String?,
genres:
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$$SpotubeFullAlbumObjectImplToJson(
_$SpotubeFullAlbumObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'artists': instance.artists.map((e) => e.toJson()).toList(),
'images': instance.images.map((e) => e.toJson()).toList(),
'releaseDate': instance.releaseDate,
'externalUri': instance.externalUri,
'totalTracks': instance.totalTracks,
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
'recordLabel': instance.recordLabel,
'genres': instance.genres,
};
const _$SpotubeAlbumTypeEnumMap = {
SpotubeAlbumType.album: 'album',
SpotubeAlbumType.single: 'single',
SpotubeAlbumType.compilation: 'compilation',
};
_$SpotubeSimpleAlbumObjectImpl _$$SpotubeSimpleAlbumObjectImplFromJson(
Map json) =>
_$SpotubeSimpleAlbumObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
externalUri: json['externalUri'] as String,
artists: (json['artists'] as List<dynamic>)
.map((e) => SpotubeSimpleArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
releaseDate: json['releaseDate'] as String?,
);
Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
_$SpotubeSimpleAlbumObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'externalUri': instance.externalUri,
'artists': instance.artists.map((e) => e.toJson()).toList(),
'images': instance.images.map((e) => e.toJson()).toList(),
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
'releaseDate': instance.releaseDate,
};
_$SpotubeFullArtistObjectImpl _$$SpotubeFullArtistObjectImplFromJson(
Map json) =>
_$SpotubeFullArtistObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
externalUri: json['externalUri'] as String,
images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
genres:
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
followers: (json['followers'] as num?)?.toInt(),
);
Map<String, dynamic> _$$SpotubeFullArtistObjectImplToJson(
_$SpotubeFullArtistObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'externalUri': instance.externalUri,
'images': instance.images.map((e) => e.toJson()).toList(),
'genres': instance.genres,
'followers': instance.followers,
};
_$SpotubeSimpleArtistObjectImpl _$$SpotubeSimpleArtistObjectImplFromJson(
Map json) =>
_$SpotubeSimpleArtistObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
externalUri: json['externalUri'] as String,
images: (json['images'] as List<dynamic>?)
?.map((e) =>
SpotubeImageObject.fromJson(Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeSimpleArtistObjectImplToJson(
_$SpotubeSimpleArtistObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'externalUri': instance.externalUri,
'images': instance.images?.map((e) => e.toJson()).toList(),
};
_$SpotubeBrowseSectionObjectImpl<T>
_$$SpotubeBrowseSectionObjectImplFromJson<T>(
Map json,
T Function(Object? json) fromJsonT,
) =>
_$SpotubeBrowseSectionObjectImpl<T>(
id: json['id'] as String,
title: json['title'] as String,
externalUri: json['externalUri'] as String,
browseMore: json['browseMore'] as bool,
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
);
Map<String, dynamic> _$$SpotubeBrowseSectionObjectImplToJson<T>(
_$SpotubeBrowseSectionObjectImpl<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'externalUri': instance.externalUri,
'browseMore': instance.browseMore,
'items': instance.items.map(toJsonT).toList(),
};
_$MetadataFormFieldInputObjectImpl _$$MetadataFormFieldInputObjectImplFromJson(
Map json) =>
_$MetadataFormFieldInputObjectImpl(
@ -316,286 +52,6 @@ Map<String, dynamic> _$$MetadataFormFieldTextObjectImplToJson(
'text': instance.text,
};
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
_$SpotubeImageObjectImpl(
url: json['url'] as String,
width: (json['width'] as num?)?.toInt(),
height: (json['height'] as num?)?.toInt(),
);
Map<String, dynamic> _$$SpotubeImageObjectImplToJson(
_$SpotubeImageObjectImpl instance) =>
<String, dynamic>{
'url': instance.url,
'width': instance.width,
'height': instance.height,
};
_$SpotubePaginationResponseObjectImpl<T>
_$$SpotubePaginationResponseObjectImplFromJson<T>(
Map json,
T Function(Object? json) fromJsonT,
) =>
_$SpotubePaginationResponseObjectImpl<T>(
limit: (json['limit'] as num).toInt(),
nextOffset: (json['nextOffset'] as num?)?.toInt(),
total: (json['total'] as num).toInt(),
hasMore: json['hasMore'] as bool,
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
);
Map<String, dynamic> _$$SpotubePaginationResponseObjectImplToJson<T>(
_$SpotubePaginationResponseObjectImpl<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'limit': instance.limit,
'nextOffset': instance.nextOffset,
'total': instance.total,
'hasMore': instance.hasMore,
'items': instance.items.map(toJsonT).toList(),
};
_$SpotubeFullPlaylistObjectImpl _$$SpotubeFullPlaylistObjectImplFromJson(
Map json) =>
_$SpotubeFullPlaylistObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
externalUri: json['externalUri'] as String,
owner: SpotubeUserObject.fromJson(
Map<String, dynamic>.from(json['owner'] as Map)),
images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
collaborators: (json['collaborators'] as List<dynamic>?)
?.map((e) => SpotubeUserObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
collaborative: json['collaborative'] as bool? ?? false,
public: json['public'] as bool? ?? false,
);
Map<String, dynamic> _$$SpotubeFullPlaylistObjectImplToJson(
_$SpotubeFullPlaylistObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'description': instance.description,
'externalUri': instance.externalUri,
'owner': instance.owner.toJson(),
'images': instance.images.map((e) => e.toJson()).toList(),
'collaborators': instance.collaborators.map((e) => e.toJson()).toList(),
'collaborative': instance.collaborative,
'public': instance.public,
};
_$SpotubeSimplePlaylistObjectImpl _$$SpotubeSimplePlaylistObjectImplFromJson(
Map json) =>
_$SpotubeSimplePlaylistObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
externalUri: json['externalUri'] as String,
owner: SpotubeUserObject.fromJson(
Map<String, dynamic>.from(json['owner'] as Map)),
images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
);
Map<String, dynamic> _$$SpotubeSimplePlaylistObjectImplToJson(
_$SpotubeSimplePlaylistObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'description': instance.description,
'externalUri': instance.externalUri,
'owner': instance.owner.toJson(),
'images': instance.images.map((e) => e.toJson()).toList(),
};
_$SpotubeSearchResponseObjectImpl _$$SpotubeSearchResponseObjectImplFromJson(
Map json) =>
_$SpotubeSearchResponseObjectImpl(
albums: (json['albums'] as List<dynamic>)
.map((e) => SpotubeSimpleAlbumObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
artists: (json['artists'] as List<dynamic>)
.map((e) => SpotubeFullArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
playlists: (json['playlists'] as List<dynamic>)
.map((e) => SpotubeSimplePlaylistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
tracks: (json['tracks'] as List<dynamic>)
.map((e) => SpotubeFullTrackObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeSearchResponseObjectImplToJson(
_$SpotubeSearchResponseObjectImpl instance) =>
<String, dynamic>{
'albums': instance.albums.map((e) => e.toJson()).toList(),
'artists': instance.artists.map((e) => e.toJson()).toList(),
'playlists': instance.playlists.map((e) => e.toJson()).toList(),
'tracks': instance.tracks.map((e) => e.toJson()).toList(),
};
_$SpotubeLocalTrackObjectImpl _$$SpotubeLocalTrackObjectImplFromJson(
Map json) =>
_$SpotubeLocalTrackObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
externalUri: json['externalUri'] as String,
artists: (json['artists'] as List<dynamic>?)
?.map((e) => SpotubeSimpleArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
album: SpotubeSimpleAlbumObject.fromJson(
Map<String, dynamic>.from(json['album'] as Map)),
durationMs: (json['durationMs'] as num).toInt(),
path: json['path'] as String,
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$SpotubeLocalTrackObjectImplToJson(
_$SpotubeLocalTrackObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'externalUri': instance.externalUri,
'artists': instance.artists.map((e) => e.toJson()).toList(),
'album': instance.album.toJson(),
'durationMs': instance.durationMs,
'path': instance.path,
'runtimeType': instance.$type,
};
_$SpotubeFullTrackObjectImpl _$$SpotubeFullTrackObjectImplFromJson(Map json) =>
_$SpotubeFullTrackObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
externalUri: json['externalUri'] as String,
artists: (json['artists'] as List<dynamic>?)
?.map((e) => SpotubeSimpleArtistObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
album: SpotubeSimpleAlbumObject.fromJson(
Map<String, dynamic>.from(json['album'] as Map)),
durationMs: (json['durationMs'] as num).toInt(),
isrc: json['isrc'] as String,
explicit: json['explicit'] as bool,
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$SpotubeFullTrackObjectImplToJson(
_$SpotubeFullTrackObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'externalUri': instance.externalUri,
'artists': instance.artists.map((e) => e.toJson()).toList(),
'album': instance.album.toJson(),
'durationMs': instance.durationMs,
'isrc': instance.isrc,
'explicit': instance.explicit,
'runtimeType': instance.$type,
};
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
_$SpotubeUserObjectImpl(
id: json['id'] as String,
name: json['name'] as String,
images: (json['images'] as List<dynamic>?)
?.map((e) => SpotubeImageObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
externalUri: json['externalUri'] as String,
);
Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
_$SpotubeUserObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'images': instance.images.map((e) => e.toJson()).toList(),
'externalUri': instance.externalUri,
};
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
_$PluginConfigurationImpl(
name: json['name'] as String,
description: json['description'] as String,
version: json['version'] as String,
author: json['author'] as String,
entryPoint: json['entryPoint'] as String,
pluginApiVersion: json['pluginApiVersion'] as String,
apis: (json['apis'] as List<dynamic>?)
?.map((e) => $enumDecode(_$PluginApisEnumMap, e))
.toList() ??
const [],
abilities: (json['abilities'] as List<dynamic>?)
?.map((e) => $enumDecode(_$PluginAbilitiesEnumMap, e))
.toList() ??
const [],
repository: json['repository'] as String?,
);
Map<String, dynamic> _$$PluginConfigurationImplToJson(
_$PluginConfigurationImpl instance) =>
<String, dynamic>{
'name': instance.name,
'description': instance.description,
'version': instance.version,
'author': instance.author,
'entryPoint': instance.entryPoint,
'pluginApiVersion': instance.pluginApiVersion,
'apis': instance.apis.map((e) => _$PluginApisEnumMap[e]!).toList(),
'abilities':
instance.abilities.map((e) => _$PluginAbilitiesEnumMap[e]!).toList(),
'repository': instance.repository,
};
const _$PluginApisEnumMap = {
PluginApis.webview: 'webview',
PluginApis.localstorage: 'localstorage',
PluginApis.timezone: 'timezone',
};
const _$PluginAbilitiesEnumMap = {
PluginAbilities.authentication: 'authentication',
PluginAbilities.scrobbling: 'scrobbling',
PluginAbilities.metadata: 'metadata',
PluginAbilities.audioSource: 'audio-source',
};
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
_$PluginUpdateAvailableImpl(
downloadUrl: json['downloadUrl'] as String,
version: json['version'] as String,
changelog: json['changelog'] as String?,
);
Map<String, dynamic> _$$PluginUpdateAvailableImplToJson(
_$PluginUpdateAvailableImpl instance) =>
<String, dynamic>{
'downloadUrl': instance.downloadUrl,
'version': instance.version,
'changelog': instance.changelog,
};
_$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
Map json) =>
_$MetadataPluginRepositoryImpl(

View File

@ -1,22 +1,78 @@
part of 'metadata.dart';
@Freezed(genericArgumentFactories: true)
class SpotubePaginationResponseObject<T>
with _$SpotubePaginationResponseObject<T> {
factory SpotubePaginationResponseObject({
required int limit,
required int? nextOffset,
required int total,
required bool hasMore,
required List<T> items,
}) = _SpotubePaginationResponseObject<T>;
class SpotubeFlattenedPaginationObject<T> {
final int limit;
final int? nextOffset;
final int total;
final bool hasMore;
final List<T> items;
factory SpotubePaginationResponseObject.fromJson(
Map<String, Object?> json,
T Function(Map<String, dynamic> json) fromJsonT,
) =>
_$SpotubePaginationResponseObjectFromJson<T>(
json,
(json) => fromJsonT(json as Map<String, dynamic>),
);
SpotubeFlattenedPaginationObject({
required this.limit,
required this.nextOffset,
required this.total,
required this.hasMore,
required this.items,
});
static SpotubeFlattenedPaginationObject<T> from<T>(
SpotubePaginationResponseObject response,
T Function(SpotubePaginationResponseObjectItem item) parse,
) {
return SpotubeFlattenedPaginationObject<T>(
limit: response.limit,
nextOffset: response.nextOffset,
total: response.total,
hasMore: response.hasMore,
items: response.items.map((item) => parse(item)).toList(growable: false),
);
}
SpotubeFlattenedPaginationObject<T> copyWith({
int? limit,
int? nextOffset,
int? total,
bool? hasMore,
List<T>? items,
}) {
return SpotubeFlattenedPaginationObject<T>(
limit: limit ?? this.limit,
nextOffset: nextOffset ?? this.nextOffset,
total: total ?? this.total,
hasMore: hasMore ?? this.hasMore,
items: items ?? this.items,
);
}
}
extension SpotubePaginationResponseObjectExtension
on SpotubePaginationResponseObject {
SpotubeFlattenedPaginationObject<T> flatten<T>() {
return SpotubeFlattenedPaginationObject.from<T>(
this,
(item) => switch (T) {
SpotubeSimpleAlbumObject() =>
(item as SpotubePaginationResponseObjectItem_AlbumSimple).field0 as T,
SpotubeFullAlbumObject() =>
(item as SpotubePaginationResponseObjectItem_AlbumFull).field0 as T,
SpotubeSimpleArtistObject() =>
(item as SpotubePaginationResponseObjectItem_ArtistSimple).field0
as T,
SpotubeFullArtistObject() =>
(item as SpotubePaginationResponseObjectItem_ArtistFull).field0 as T,
SpotubeTrackObject() =>
(item as SpotubePaginationResponseObjectItem_Track).field0 as T,
SpotubeSimplePlaylistObject() =>
(item as SpotubePaginationResponseObjectItem_PlaylistSimple).field0
as T,
SpotubeFullPlaylistObject() =>
(item as SpotubePaginationResponseObjectItem_PlaylistFull).field0
as T,
SpotubeBrowseSectionObject() =>
(item as SpotubePaginationResponseObjectItem_BrowseSection).field0
as T,
_ => throw Exception("Unsupported type: $T"),
},
);
}
}

View File

@ -1,34 +0,0 @@
part of 'metadata.dart';
@freezed
class SpotubeFullPlaylistObject with _$SpotubeFullPlaylistObject {
factory SpotubeFullPlaylistObject({
required String id,
required String name,
required String description,
required String externalUri,
required SpotubeUserObject owner,
@Default([]) List<SpotubeImageObject> images,
@Default([]) List<SpotubeUserObject> collaborators,
@Default(false) bool collaborative,
@Default(false) bool public,
}) = _SpotubeFullPlaylistObject;
factory SpotubeFullPlaylistObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeFullPlaylistObjectFromJson(json);
}
@freezed
class SpotubeSimplePlaylistObject with _$SpotubeSimplePlaylistObject {
factory SpotubeSimplePlaylistObject({
required String id,
required String name,
required String description,
required String externalUri,
required SpotubeUserObject owner,
@Default([]) List<SpotubeImageObject> images,
}) = _SpotubeSimplePlaylistObject;
factory SpotubeSimplePlaylistObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeSimplePlaylistObjectFromJson(json);
}

View File

@ -1,45 +0,0 @@
part of 'metadata.dart';
enum PluginApis { webview, localstorage, timezone }
enum PluginAbilities {
authentication,
scrobbling,
metadata,
@JsonValue('audio-source')
audioSource,
}
@freezed
class PluginConfiguration with _$PluginConfiguration {
const PluginConfiguration._();
factory PluginConfiguration({
required String name,
required String description,
required String version,
required String author,
required String entryPoint,
required String pluginApiVersion,
@Default([]) List<PluginApis> apis,
@Default([]) List<PluginAbilities> abilities,
String? repository,
}) = _PluginConfiguration;
factory PluginConfiguration.fromJson(Map<String, dynamic> json) =>
_$PluginConfigurationFromJson(json);
String get slug => name.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-');
}
@freezed
class PluginUpdateAvailable with _$PluginUpdateAvailable {
factory PluginUpdateAvailable({
required String downloadUrl,
required String version,
String? changelog,
}) = _PluginUpdateAvailable;
factory PluginUpdateAvailable.fromJson(Map<String, dynamic> json) =>
_$PluginUpdateAvailableFromJson(json);
}

View File

@ -1,14 +0,0 @@
part of 'metadata.dart';
@freezed
class SpotubeSearchResponseObject with _$SpotubeSearchResponseObject {
factory SpotubeSearchResponseObject({
required List<SpotubeSimpleAlbumObject> albums,
required List<SpotubeFullArtistObject> artists,
required List<SpotubeSimplePlaylistObject> playlists,
required List<SpotubeFullTrackObject> tracks,
}) = _SpotubeSearchResponseObject;
factory SpotubeSearchResponseObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeSearchResponseObjectFromJson(json);
}

View File

@ -1,94 +1,37 @@
part of 'metadata.dart';
@freezed
class SpotubeTrackObject with _$SpotubeTrackObject {
factory SpotubeTrackObject.local({
required String id,
required String name,
required String externalUri,
@Default([]) List<SpotubeSimpleArtistObject> artists,
required SpotubeSimpleAlbumObject album,
required int durationMs,
required String path,
}) = SpotubeLocalTrackObject;
factory SpotubeTrackObject.full({
required String id,
required String name,
required String externalUri,
@Default([]) List<SpotubeSimpleArtistObject> artists,
required SpotubeSimpleAlbumObject album,
required int durationMs,
required String isrc,
required bool explicit,
}) = SpotubeFullTrackObject;
factory SpotubeTrackObject.localTrackFromFile(
File file, {
Metadata? metadata,
String? art,
}) {
return SpotubeLocalTrackObject(
id: file.absolute.path,
name: metadata?.title ?? basenameWithoutExtension(file.path),
externalUri: "file://${file.absolute.path}",
artists: metadata?.artist?.split(",").map((a) {
return SpotubeSimpleArtistObject(
id: a.trim(),
name: a.trim(),
externalUri: "file://${file.absolute.path}",
);
}).toList() ??
[
SpotubeSimpleArtistObject(
id: "unknown",
name: "Unknown Artist",
externalUri: "file://${file.absolute.path}",
),
],
album: SpotubeSimpleAlbumObject(
albumType: SpotubeAlbumType.album,
id: metadata?.album ?? "unknown",
name: metadata?.album ?? "Unknown Album",
externalUri: "file://${file.absolute.path}",
artists: [
SpotubeSimpleArtistObject(
id: metadata?.albumArtist ?? "unknown",
name: metadata?.albumArtist ?? "Unknown Artist",
externalUri: "file://${file.absolute.path}",
),
],
releaseDate:
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
images: [
if (art != null)
SpotubeImageObject(
url: art,
width: 300,
height: 300,
),
],
),
durationMs: metadata?.durationMs?.toInt() ?? 0,
path: file.path,
);
}
factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeTrackObjectFromJson(
json.containsKey("path")
? {...json, "runtimeType": "local"}
: {...json, "runtimeType": "full"},
);
}
extension AsMediaListSpotubeTrackObject on Iterable<SpotubeTrackObject> {
List<SpotubeMedia> asMediaList() {
return map((track) => SpotubeMedia(track)).toList();
}
}
extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
extension FullAsPartialSpotubeTrackObject on Iterable<SpotubeFullTrackObject>? {
List<SpotubeTrackObject>? union() {
return this?.map((track) => SpotubeTrackObject.full(track)).toList();
}
}
extension FullAsSpotubeTrackObject on Iterable<SpotubeFullTrackObject> {
List<SpotubeTrackObject> union() {
return map((track) => SpotubeTrackObject.full(track)).toList();
}
}
extension LocalAsPartialSpotubeTrackObject
on Iterable<SpotubeLocalTrackObject>? {
List<SpotubeTrackObject>? union() {
return this?.map((track) => SpotubeTrackObject.local(track)).toList();
}
}
extension LocalAsSpotubeTrackObject on Iterable<SpotubeLocalTrackObject> {
List<SpotubeTrackObject> union() {
return map((track) => SpotubeTrackObject.local(track)).toList();
}
}
extension ToMetadataSpotubeFullTrackObject on SpotubeTrackObject {
Metadata toMetadata({
required int fileLength,
Uint8List? imageBytes,
@ -117,3 +60,91 @@ extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
);
}
}
extension CommonTrackProperties on SpotubeTrackObject {
String get id => when(
full: (track) => track.id,
local: (track) => track.id,
);
String get name => when(
full: (track) => track.name,
local: (track) => track.name,
);
String get externalUri => when(
full: (track) => track.externalUri,
local: (track) => track.externalUri,
);
int get durationMs => when(
full: (track) => track.durationMs,
local: (track) => track.durationMs,
);
SpotubeSimpleAlbumObject get album => when(
full: (track) => track.album,
local: (track) => track.album,
);
List<SpotubeSimpleArtistObject> get artists => when(
full: (track) => track.artists,
local: (track) => track.artists,
);
}
SpotubeLocalTrackObject localTrackFromFile(
File file, {
Metadata? metadata,
String? art,
}) {
return SpotubeLocalTrackObject(
typeName: "track_local",
id: file.absolute.path,
name: metadata?.title ?? basenameWithoutExtension(file.path),
externalUri: "file://${file.absolute.path}",
artists: metadata?.artist?.split(",").map((a) {
return SpotubeSimpleArtistObject(
typeName: "artist_simple",
id: a.trim(),
name: a.trim(),
externalUri: "file://${file.absolute.path}",
);
}).toList() ??
[
SpotubeSimpleArtistObject(
typeName: "artist_simple",
id: "unknown",
name: "Unknown Artist",
externalUri: "file://${file.absolute.path}",
),
],
album: SpotubeSimpleAlbumObject(
typeName: "album_simple",
albumType: SpotubeAlbumType.album,
id: metadata?.album ?? "unknown",
name: metadata?.album ?? "Unknown Album",
externalUri: "file://${file.absolute.path}",
artists: [
SpotubeSimpleArtistObject(
typeName: "artist_simple",
id: metadata?.albumArtist ?? "unknown",
name: metadata?.albumArtist ?? "Unknown Artist",
externalUri: "file://${file.absolute.path}",
),
],
releaseDate:
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
images: [
if (art != null)
SpotubeImageObject(
typeName: "image",
url: art,
width: 300,
height: 300,
),
],
),
durationMs: metadata?.durationMs?.toInt() ?? 0,
path: file.path,
);
}

View File

@ -1,14 +0,0 @@
part of 'metadata.dart';
@freezed
class SpotubeUserObject with _$SpotubeUserObject {
factory SpotubeUserObject({
required final String id,
required final String name,
@Default([]) final List<SpotubeImageObject> images,
required final String externalUri,
}) = _SpotubeUserObject;
factory SpotubeUserObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeUserObjectFromJson(json);
}

View File

@ -7,7 +7,7 @@ part of 'track_sources.dart';
// **************************************************************************
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
query: SpotubeFullTrackObject.fromJson(
query: SpotubeTrackObject.fromJson(
Map<String, dynamic>.from(json['query'] as Map)),
source: json['source'] as String,
info: SpotubeAudioSourceMatchObject.fromJson(

View File

@ -88,12 +88,12 @@ class AlbumCard extends HookConsumerWidget {
final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load(
WebSocketLoadEventData.album(
tracks: fetchedTracks,
tracks: fetchedTracks.union(),
collection: album,
),
);
} else {
await playlistNotifier.load(fetchedTracks, autoPlay: true);
await playlistNotifier.load(fetchedTracks.union(), autoPlay: true);
playlistNotifier.addCollection(album.id);
historyNotifier.addAlbums([album]);
}
@ -123,7 +123,9 @@ class AlbumCard extends HookConsumerWidget {
final fetchedTracks = await fetchAllTrack();
if (fetchedTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedTracks);
playlistNotifier.addTracks(
fetchedTracks.union(),
);
playlistNotifier.addCollection(album.id);
historyNotifier.addAlbums([album]);
if (context.mounted) {

View File

@ -14,8 +14,8 @@ import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart';
import 'package:url_launcher/url_launcher.dart';
final validAbilities = {
PluginAbilities.metadata: ("Metadata", SpotubeIcons.album),
PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music),
PluginAbility.metadata: ("Metadata", SpotubeIcons.album),
PluginAbility.audioSource: ("Audio Source", SpotubeIcons.music),
};
class MetadataInstalledPluginItem extends HookConsumerWidget {
@ -44,9 +44,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
plugin.abilities.contains(PluginAbilities.authentication);
plugin.abilities.contains(PluginAbility.authentication);
final supportsScrobbling = isDefaultMetadata &&
plugin.abilities.contains(PluginAbilities.scrobbling);
plugin.abilities.contains(PluginAbility.scrobbling);
final isMetadataAuthenticatedSnapshot =
ref.watch(metadataPluginAuthenticatedProvider);
@ -253,7 +253,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
spacing: 8,
runSpacing: 8,
children: [
if (plugin.abilities.contains(PluginAbilities.metadata))
if (plugin.abilities.contains(PluginAbility.metadata))
Button.secondary(
enabled: !isDefaultMetadata,
onPressed: () async {
@ -265,7 +265,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
: context.l10n.set_default_metadata_source,
),
),
if (plugin.abilities.contains(PluginAbilities.audioSource))
if (plugin.abilities.contains(PluginAbility.audioSource))
Button.secondary(
enabled: !isDefaultAudioSource,
onPressed: () async {
@ -378,8 +378,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
!isAuthenticated)
Button.primary(
onPressed: () async {
await pluginSnapshot?.asData?.value?.auth
.authenticate();
if ((pluginSnapshot?.hasValue ?? false) == false) {
return;
}
await pluginSnapshot!.value!.auth.authenticate(
mpscTx: pluginSnapshot.value!.sender,
);
},
leading: const Icon(SpotubeIcons.login),
child: Text(context.l10n.login),
@ -389,7 +394,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
isAuthenticated)
Button.destructive(
onPressed: () async {
await pluginSnapshot?.asData?.value?.auth.logout();
if ((pluginSnapshot?.hasValue ?? false) == false) {
return;
}
await pluginSnapshot!.value!.auth.logout(
mpscTx: pluginSnapshot.value!.sender,
);
},
leading: const Icon(SpotubeIcons.logout),
child: Text(context.l10n.logout),

View File

@ -123,7 +123,7 @@ class PlayerView extends HookConsumerWidget {
context: context,
builder: (context) {
return TrackDetailsDialog(
track: currentActiveTrack
track: currentActiveTrack?.field0
as SpotubeFullTrackObject,
);
});
@ -180,7 +180,7 @@ class PlayerView extends HookConsumerWidget {
),
if (isLocalTrack)
Text(
currentActiveTrack.artists.asString(),
currentActiveTrack?.artists.asString() ?? "",
style: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold),
)

View File

@ -38,11 +38,12 @@ class PlayerActions extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(audioPlayerProvider);
final isLocalTrack = playlist.activeTrack is SpotubeLocalTrackObject;
final isLocalTrack =
playlist.activeTrack?.field0 is SpotubeLocalTrackObject;
ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier);
final isInQueue = useMemoized(() {
if (playlist.activeTrack is! SpotubeFullTrackObject) return false;
if (playlist.activeTrack is! SpotubeTrackObject) return false;
final downloadTask =
downloader.getTaskByTrackId(playlist.activeTrack!.id);
return const [
@ -172,16 +173,19 @@ class PlayerActions extends HookConsumerWidget {
icon: Icon(
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
),
onPressed: playlist.activeTrack != null
onPressed: playlist.activeTrack != null && !isLocalTrack
? () => downloader.addToQueue(
playlist.activeTrack! as SpotubeFullTrackObject)
playlist.activeTrack?.field0
as SpotubeFullTrackObject,
)
: null,
),
),
if (playlist.activeTrack != null &&
!isLocalTrack &&
authenticated.asData?.value == true)
TrackHeartButton(track: playlist.activeTrack!),
TrackHeartButton(
track: playlist.activeTrack?.field0 as SpotubeFullTrackObject),
AdaptivePopSheetList<Duration>(
tooltip: context.l10n.sleep_timer,
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),

View File

@ -26,14 +26,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
final activeTrack =
ref.watch(audioPlayerProvider.select((e) => e.activeTrack));
if (activeTrack == null || activeTrack is! SpotubeFullTrackObject) {
if (activeTrack is! SpotubeTrackObject_Full) {
return const SafeArea(child: NotFound());
}
return HookBuilder(builder: (context) {
final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack));
final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack.field0));
final sourcedTrackNotifier =
ref.watch(sourcedTrackProvider(activeTrack).notifier);
ref.watch(sourcedTrackProvider(activeTrack.field0).notifier);
final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
() => !sourcedTrack.isLoading
@ -112,8 +112,10 @@ class SiblingTracksSheet extends HookConsumerWidget {
width: 60,
)
: null,
trailing:
Text(sourceInfo.duration.toHumanReadableString()),
trailing: Text(
Duration(milliseconds: sourceInfo.duration)
.toHumanReadableString(),
),
subtitle: Text(
sourceInfo.artists.join(", "),
maxLines: 1,

View File

@ -98,19 +98,23 @@ class PlaylistCard extends HookConsumerWidget {
final allTracks = await fetchAllTracks();
await remotePlayback.load(
WebSocketLoadEventData.playlist(
tracks: allTracks,
tracks: allTracks.union(),
collection: playlist,
),
);
} else {
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
await playlistNotifier.load(
fetchedInitialTracks.union(),
autoPlay: true,
);
playlistNotifier.addCollection(playlist.id);
historyNotifier.addPlaylists([playlist]);
final allTracks = await fetchAllTracks();
await playlistNotifier
.addTracks(allTracks.sublist(fetchedInitialTracks.length));
await playlistNotifier.addTracks(
allTracks.sublist(fetchedInitialTracks.length).union(),
);
}
} finally {
if (context.mounted) {
@ -142,7 +146,9 @@ class PlaylistCard extends HookConsumerWidget {
if (fetchedInitialTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedInitialTracks);
playlistNotifier.addTracks(
fetchedInitialTracks.union(),
);
playlistNotifier.addCollection(playlist.id);
historyNotifier.addPlaylists([playlist]);
if (context.mounted) {

View File

@ -7,10 +7,12 @@ import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/src/rust/api/plugin/models/track.dart';
class SearchTracksSection extends HookConsumerWidget {
const SearchTracksSection({
@ -41,7 +43,8 @@ class SearchTracksSection extends HookConsumerWidget {
if (search.isLoading)
const CircularProgressIndicator()
else
...tracks.mapIndexed((i, track) {
...tracks.mapIndexed((i, mehTrack) {
final track = SpotubeTrackObject.full(mehTrack);
return TrackTile(
index: i,
track: track,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart' as material;
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart';
@ -31,6 +32,10 @@ class AlbumPage extends HookConsumerWidget {
ref.watch(metadataPluginSavedAlbumsProvider.notifier);
final isSavedAlbum =
ref.watch(metadataPluginIsSavedAlbumProvider(album.id));
final tracksUnion = useMemoized(
() => tracks.asData?.value.items.union() ?? [],
[tracks.asData?.value.items],
);
return material.RefreshIndicator.adaptive(
onRefresh: () async {
@ -47,7 +52,7 @@ class AlbumPage extends HookConsumerWidget {
title: album.name,
description:
"${context.l10n.released}${album.releaseDate}${album.artists.first.name}",
tracks: tracks.asData?.value.items ?? [],
tracks: tracksUnion,
error: tracks.error,
pagination: PaginationProps(
hasNextPage: tracks.asData?.value.hasMore ?? false,
@ -56,7 +61,9 @@ class AlbumPage extends HookConsumerWidget {
await tracksNotifier.fetchMore();
},
onFetchAll: () async {
return tracksNotifier.fetchAll();
final res = await tracksNotifier.fetchAll();
return res.union();
},
onRefresh: () async {
ref.invalidate(metadataPluginAlbumTracksProvider(album.id));

View File

@ -44,7 +44,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
List.generate(10, (index) => FakeData.track);
void playPlaylist(
List<SpotubeFullTrackObject> tracks, {
List<SpotubeTrackObject> tracks, {
SpotubeTrackObject? currentTrack,
}) async {
isLoading.value = true;

View File

@ -28,7 +28,7 @@ class HomeBrowseSectionItemsPage extends HookConsumerWidget {
static const name = "home_browse_section_items";
final String sectionId;
final SpotubeBrowseSectionObject<Object> section;
final SpotubeBrowseSectionObject section;
const HomeBrowseSectionItemsPage({
super.key,
@PathParam("sectionId") required this.sectionId,

View File

@ -55,17 +55,17 @@ class LocalLibraryPage extends HookConsumerWidget {
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
currentTrack ??= tracks.first;
final isPlaylistPlaying = playlist.containsTracks(tracks);
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
if (!isPlaylistPlaying) {
var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id);
await playback.load(
tracks,
tracks.union(),
initialIndex: indexWhere,
autoPlay: true,
);
} else if (isPlaylistPlaying &&
currentTrack.id != playlist.activeTrack?.id) {
await playback.jumpToTrack(currentTrack);
await playback.jumpToTrack(SpotubeTrackObject.local(currentTrack));
}
}
@ -75,12 +75,12 @@ class LocalLibraryPage extends HookConsumerWidget {
) async {
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
final isPlaylistPlaying = playlist.containsTracks(tracks);
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
final shuffledTracks = tracks.shuffled();
if (isPlaylistPlaying) return;
await playback.load(
shuffledTracks,
shuffledTracks.union(),
initialIndex: 0,
autoPlay: true,
);
@ -93,9 +93,9 @@ class LocalLibraryPage extends HookConsumerWidget {
) async {
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
final isPlaylistPlaying = playlist.containsTracks(tracks);
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
if (isPlaylistPlaying) return;
await playback.addTracks(tracks);
await playback.addTracks(tracks.union());
if (!context.mounted) return;
showToastForAction(context, "add-to-queue", tracks.length);
}
@ -109,7 +109,7 @@ class LocalLibraryPage extends HookConsumerWidget {
final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = useMemoized(
() => playlist.containsTracks(
trackSnapshot.asData?.value[location] ?? [],
trackSnapshot.asData?.value[location]?.union() ?? [],
),
[playlist, trackSnapshot, location],
);
@ -382,7 +382,7 @@ class LocalLibraryPage extends HookConsumerWidget {
data: (tracks) {
final sortedTracks = useMemoized(() {
return ServiceUtils.sortTracks(
tracks[location] ?? <SpotubeLocalTrackObject>[],
tracks[location]?.union() ?? <SpotubeTrackObject>[],
sortBy.value);
}, [sortBy.value, tracks]);
@ -463,8 +463,12 @@ class LocalLibraryPage extends HookConsumerWidget {
onTap: () async {
await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
sortedTracks
.map((e) => e.field0
as SpotubeLocalTrackObject)
.toList(),
currentTrack: track.field0
as SpotubeLocalTrackObject,
);
},
);

View File

@ -43,6 +43,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
() => me.asData?.value == null
? null
: SpotubeSimplePlaylistObject(
typeName: "playlist_simple",
id: "user-liked-tracks",
name: context.l10n.liked_tracks,
description: context.l10n.liked_tracks_description,
@ -50,6 +51,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
owner: me.asData!.value!,
images: [
SpotubeImageObject(
typeName: "image",
url: Assets.images.likedTracks.path,
width: 300,
height: 300,

View File

@ -2,10 +2,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/extensions/context.dart';

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart';
@ -25,7 +26,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
final likedTracksNotifier =
ref.watch(metadataPluginSavedTracksProvider.notifier);
final tracks = likedTracks.asData?.value.items ?? [];
final tracks = useMemoized(
() => likedTracks.asData?.value.items.union() ?? [],
[likedTracks.asData?.value]);
return material.RefreshIndicator.adaptive(
onRefresh: () async {
@ -42,7 +45,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
await likedTracksNotifier.fetchMore();
},
onFetchAll: () async {
return await likedTracksNotifier.fetchAll();
final res = await likedTracksNotifier.fetchAll();
return res.union();
},
onRefresh: () async {
ref.invalidate(metadataPluginSavedTracksProvider);

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart' as material;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart';
@ -50,6 +51,10 @@ class PlaylistPage extends HookConsumerWidget {
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id);
final tracksMemoized = useMemoized(
() => tracks.asData?.value.items.union() ?? [],
[tracks.asData?.value],
);
return material.RefreshIndicator.adaptive(
onRefresh: () async {
@ -71,14 +76,15 @@ class PlaylistPage extends HookConsumerWidget {
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
},
onFetchAll: () async {
return await tracksNotifier.fetchAll();
final res = await tracksNotifier.fetchAll();
return res.union();
},
),
title: playlist.name,
description: playlist.description,
owner: playlist.owner.name,
ownerImage: playlist.owner.images.lastOrNull?.url,
tracks: tracks.asData?.value.items ?? [],
tracks: tracksMemoized,
error: tracks.error,
routePath: '/playlist/${playlist.id}',
isLiked: isFavoritePlaylist.asData?.value ?? false,

View File

@ -8,6 +8,7 @@ import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/search/loading.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';

View File

@ -82,8 +82,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
return plugins.asData?.value.plugins.where((d) {
return d.abilities.contains(
tabState.value == 1
? PluginAbilities.metadata
: PluginAbilities.audioSource,
? PluginAbility.metadata
: PluginAbility.audioSource,
);
}).toList();
}, [tabState.value, plugins.asData?.value]);

View File

@ -3,6 +3,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/stats/common/track_item.dart';
import 'package:spotube/extensions/context.dart';

View File

@ -44,7 +44,8 @@ class TrackPage extends HookConsumerWidget {
final trackQuery = ref.watch(metadataPluginTrackProvider(trackId));
final track = trackQuery.asData?.value ?? FakeData.track;
final track = SpotubeTrackObject.full(trackQuery.asData?.value ??
FakeData.track.field0 as SpotubeFullTrackObject);
void onPlay() async {
if (isActive) {
@ -230,7 +231,10 @@ class TrackPage extends HookConsumerWidget {
const Spacer()
else
const Gap(20),
TrackHeartButton(track: track),
TrackHeartButton(
track: track.field0
as SpotubeFullTrackObject,
),
TrackOptionsButton(
track: track,
userPlaylist: false,

View File

@ -22,16 +22,16 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
assert(
tracks.every(
(track) =>
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
),
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
'All tracks must be either SpotubeTrackObject or SpotubeLocalTrackObject',
);
}
void _assertAllowedTrack(SpotubeTrackObject tracks) {
assert(
tracks is SpotubeFullTrackObject || tracks is SpotubeLocalTrackObject,
'Track must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
tracks is SpotubeTrackObject || tracks is SpotubeLocalTrackObject,
'Track must be either SpotubeTrackObject or SpotubeLocalTrackObject',
);
}
@ -345,9 +345,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
return false;
}
return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
? a.path == b.path
: a.id == b.id;
return switch ((a.field0, b.field0)) {
(SpotubeLocalTrackObject(), SpotubeLocalTrackObject()) =>
(a.field0 as SpotubeLocalTrackObject).path ==
(b.field0 as SpotubeLocalTrackObject).path,
_ => a.id == b.id,
};
}
Future<void> load(
@ -366,12 +369,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
// Giving the initial track a boost so MediaKit won't skip
// because of timeout
final intendedActiveTrack = medias.elementAt(initialIndex);
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
ref.read(
sourcedTrackProvider(
intendedActiveTrack.track as SpotubeFullTrackObject,
).future,
);
if (intendedActiveTrack.track is SpotubeTrackObject_Full) {
ref.read(sourcedTrackProvider(
intendedActiveTrack.track.field0 as SpotubeFullTrackObject)
.future);
}
if (medias.isEmpty) return;
@ -398,7 +399,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
}
Future<void> swapActiveSource() async {
if (state.tracks.isEmpty || state.activeTrack is! SpotubeFullTrackObject) {
if (state.tracks.isEmpty || state.activeTrack is! SpotubeTrackObject) {
return;
}

View File

@ -103,7 +103,8 @@ class AudioPlayerStreamListeners {
return;
}
scrobbler.scrobble(audioPlayerState.activeTrack!);
scrobbler.scrobble(
audioPlayerState.activeTrack!.field0 as SpotubeFullTrackObject);
ref
.read(metadataPluginScrobbleProvider.notifier)
.scrobble(audioPlayerState.activeTrack!);
@ -115,13 +116,28 @@ class AudioPlayerStreamListeners {
if (activeTrack.artists.any((a) => a.images == null)) {
final metadataPlugin = await ref.read(metadataPluginProvider.future);
final artists = await Future.wait(
activeTrack.artists
.map((artist) => metadataPlugin!.artist.getArtist(artist.id)),
activeTrack.artists.map(
(artist) => metadataPlugin!.artist.getArtist(
id: artist.id,
mpscTx: metadataPlugin.sender,
),
),
);
activeTrack = activeTrack.copyWith(
artists: artists
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
.toList(),
activeTrack = activeTrack.when(
full: (field0) => SpotubeTrackObject.full(
field0.copyWith(
artists: artists
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
.toList(),
),
),
local: (field0) => SpotubeTrackObject.local(
field0.copyWith(
artists: artists
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
.toList(),
),
),
);
}
@ -155,7 +171,8 @@ class AudioPlayerStreamListeners {
try {
await ref.read(
sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future,
sourcedTrackProvider(nextTrack.field0 as SpotubeFullTrackObject)
.future,
);
} finally {
lastTrack = nextTrack.id;

View File

@ -10,14 +10,14 @@ final queryingTrackInfoProvider = Provider<bool>((ref) {
return false;
}
if (audioPlayer.activeTrack is! SpotubeFullTrackObject) {
if (audioPlayer.activeTrack is! SpotubeTrackObject_Full) {
return false;
}
return ref
.watch(
sourcedTrackProvider(
audioPlayer.activeTrack! as SpotubeFullTrackObject),
audioPlayer.activeTrack?.field0 as SpotubeFullTrackObject),
)
.isLoading;
});

View File

@ -27,9 +27,8 @@ class AudioPlayerState with _$AudioPlayerState {
List<SpotubeTrackObject> tracks = const [],
}) {
assert(
tracks.every((track) =>
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject),
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
tracks.every((track) => track is SpotubeTrackObject_Local),
'All tracks must be either SpotubeTrackObject or SpotubeLocalTrackObject',
);
return AudioPlayerState._inner(
@ -53,10 +52,12 @@ class AudioPlayerState with _$AudioPlayerState {
bool containsTrack(SpotubeTrackObject track) {
return tracks.isNotEmpty &&
tracks.any(
(t) =>
t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject
? t.path == track.path
: t.id == track.id,
(t) => switch ((t.field0, track.field0)) {
(SpotubeLocalTrackObject(), SpotubeLocalTrackObject()) =>
(t.field0 as SpotubeLocalTrackObject).path ==
(track.field0 as SpotubeLocalTrackObject).path,
_ => t.id == track.id,
},
);
}

View File

@ -208,7 +208,7 @@ class ConnectNotifier extends AsyncNotifier<ConnectState?> {
emit(WebSocketLoopEvent(value));
}
Future<void> addTrack(SpotubeFullTrackObject data) async {
Future<void> addTrack(SpotubeTrackObject data) async {
emit(WebSocketAddTrackEvent(data));
}

View File

@ -249,7 +249,7 @@ class DownloadManagerNotifier extends Notifier<List<DownloadTask>> {
);
await MetadataGod.writeMetadata(
file: savePath,
metadata: task.track.toMetadata(
metadata: SpotubeTrackObject.full(task.track).toMetadata(
fileLength: await savePathFile.length(),
imageBytes: imageBytes,
),

View File

@ -69,7 +69,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
final items = getAlbumsWithCount(await albumsQuery.get());
return SpotubePaginationResponseObject(
return SpotubeFlattenedPaginationObject(
items: items,
limit: limit,
hasMore: items.length == limit,
@ -110,7 +110,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
final historyTopAlbumsProvider = AsyncNotifierProviderFamily<
HistoryTopAlbumsNotifier,
SpotubePaginationResponseObject<PlaybackHistoryAlbum>,
SpotubeFlattenedPaginationObject<PlaybackHistoryAlbum>,
HistoryDuration>(
() => HistoryTopAlbumsNotifier(),
);

View File

@ -36,7 +36,7 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
final items = getPlaylistsWithCount(await playlistsQuery.get());
return SpotubePaginationResponseObject(
return SpotubeFlattenedPaginationObject(
items: items,
nextOffset: offset + limit,
total: items.length,
@ -80,7 +80,7 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
final historyTopPlaylistsProvider = AsyncNotifierProviderFamily<
HistoryTopPlaylistsNotifier,
SpotubePaginationResponseObject<PlaybackHistoryPlaylist>,
SpotubeFlattenedPaginationObject<PlaybackHistoryPlaylist>,
HistoryDuration>(
() => HistoryTopPlaylistsNotifier(),
);

View File

@ -81,9 +81,12 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
.nonNulls
.toList();
track = track.copyWith(artists: includedArtists);
final updatedTrack = track.when(
full: (field0) => field0.copyWith(artists: includedArtists).toJson(),
local: (field0) => field0.copyWith(artists: includedArtists).toJson(),
);
return e.copyWith(data: track.toJson());
return e.copyWith(data: updatedTrack);
});
assert(
@ -109,7 +112,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
final items = getTracksWithCount(entries);
return SpotubePaginationResponseObject<PlaybackHistoryTrack>(
return SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>(
items: items,
nextOffset: offset + limit,
total: items.length,
@ -190,7 +193,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
final historyTopTracksProvider = AsyncNotifierProviderFamily<
HistoryTopTracksNotifier,
SpotubePaginationResponseObject<PlaybackHistoryTrack>,
SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>,
HistoryDuration>(
() => HistoryTopTracksNotifier(),
);

View File

@ -130,11 +130,11 @@ final localTracksProvider =
final tracksFromMetadata = filesWithMetadata
.map(
(fileWithMetadata) => SpotubeTrackObject.localTrackFromFile(
(fileWithMetadata) => localTrackFromFile(
fileWithMetadata.file,
metadata: fileWithMetadata.metadata,
art: fileWithMetadata.art,
) as SpotubeLocalTrackObject,
),
)
.toList();

View File

@ -15,6 +15,6 @@ final metadataPluginAlbumProvider =
throw MetadataPluginException.noDefaultMetadataPlugin();
}
return metadataPlugin.album.getAlbum(id);
return metadataPlugin.album.getAlbum(id: id, mpscTx: metadataPlugin.sender);
},
);

View File

@ -6,13 +6,14 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginAlbumReleasesNotifier
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
@override
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin)
.album
.releases(limit: limit, offset: offset);
.releases(mpscTx: await mpscTx, limit: limit, offset: offset)
.then((a) => a.flatten());
}
@override
@ -24,6 +25,6 @@ class MetadataPluginAlbumReleasesNotifier
final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider<
MetadataPluginAlbumReleasesNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
() => MetadataPluginAlbumReleasesNotifier(),
);

View File

@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
class MetadataPluginArtistAlbumNotifier
extends FamilyPaginatedAsyncNotifier<SpotubeSimpleAlbumObject, String> {
@override
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).artist.albums(
arg,
return await (await metadataPlugin)
.artist
.albums(
mpscTx: await mpscTx,
id: arg,
limit: limit,
offset: offset,
);
)
.then((a) => a.flatten());
}
@override
@ -26,7 +30,7 @@ class MetadataPluginArtistAlbumNotifier
final metadataPluginArtistAlbumsProvider = AsyncNotifierProviderFamily<
MetadataPluginArtistAlbumNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>,
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>,
String>(
() => MetadataPluginArtistAlbumNotifier(),
);

View File

@ -15,6 +15,7 @@ final metadataPluginArtistProvider =
throw MetadataPluginException.noDefaultMetadataPlugin();
}
return metadataPlugin.artist.getArtist(artistId);
return metadataPlugin.artist
.getArtist(id: artistId, mpscTx: metadataPlugin.sender);
},
);

View File

@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
class MetadataPluginArtistRelatedArtistsNotifier
extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> {
@override
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).artist.related(
arg,
return await (await metadataPlugin)
.artist
.related(
id: arg,
limit: limit,
offset: offset,
);
mpscTx: await mpscTx,
)
.then((a) => a.flatten());
}
@override
@ -26,7 +30,7 @@ class MetadataPluginArtistRelatedArtistsNotifier
final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily<
MetadataPluginArtistRelatedArtistsNotifier,
SpotubePaginationResponseObject<SpotubeFullArtistObject>,
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>,
String>(
() => MetadataPluginArtistRelatedArtistsNotifier(),
);

View File

@ -5,19 +5,20 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
class MetadataPluginArtistTopTracksNotifier
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject,
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
String> {
MetadataPluginArtistTopTracksNotifier() : super();
@override
fetch(offset, limit) async {
final tracks = await (await metadataPlugin).artist.topTracks(
arg,
id: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return tracks;
return tracks.flatten();
}
@override
@ -32,7 +33,7 @@ class MetadataPluginArtistTopTracksNotifier
final metadataPluginArtistTopTracksProvider =
AutoDisposeAsyncNotifierProviderFamily<
MetadataPluginArtistTopTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>,
SpotubeFlattenedPaginationObject<SpotubeTrackObject>,
String>(
() => MetadataPluginArtistTopTracksNotifier(),
);

View File

@ -41,10 +41,10 @@ class AudioSourceAvailableQualityPresetsNotifier
listenSelf((previous, next) {
final isNewLossless =
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
is SpotubeAudioSourceContainerPresetLossless;
is SpotubeAudioSourceContainerPreset_Lossless;
final isOldLossless = previous?.presets
.elementAtOrNull(previous.selectedStreamingContainerIndex)
is SpotubeAudioSourceContainerPresetLossless;
is SpotubeAudioSourceContainerPreset_Lossless;
if (!isOldLossless && isNewLossless) {
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
} else if (isOldLossless && !isNewLossless) {
@ -72,11 +72,13 @@ class AudioSourceAvailableQualityPresetsNotifier
state =
AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr))
.copyWith(
presets: audioSource.audioSource.supportedPresets,
presets: await audioSource.audioSource
.supportedPresets(mpscTx: audioSource.sender),
);
} else {
state = AudioSourcePresetsState(
presets: audioSource.audioSource.supportedPresets,
presets: await audioSource.audioSource
.supportedPresets(mpscTx: audioSource.sender),
);
}
});

View File

@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
class MetadataPluginBrowseSectionItemsNotifier
extends FamilyPaginatedAsyncNotifier<Object, String> {
@override
Future<SpotubePaginationResponseObject<Object>> fetch(
Future<SpotubeFlattenedPaginationObject<Object>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).browse.sectionItems(
arg,
return await (await metadataPlugin)
.browse
.sectionItems(
id: arg,
limit: limit,
offset: offset,
);
mpscTx: await mpscTx,
)
.then((value) => value.flatten());
}
@override
@ -26,7 +30,7 @@ class MetadataPluginBrowseSectionItemsNotifier
final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily<
MetadataPluginBrowseSectionItemsNotifier,
SpotubePaginationResponseObject<Object>,
SpotubeFlattenedPaginationObject<Object>,
String>(
() => MetadataPluginBrowseSectionItemsNotifier(),
);

View File

@ -4,17 +4,20 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginBrowseSectionsNotifier
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject<Object>> {
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject> {
@override
Future<SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>
fetch(
Future<SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).browse.sections(
return await (await metadataPlugin)
.browse
.sections(
limit: limit,
offset: offset,
);
mpscTx: await mpscTx,
)
.then((value) => value.flatten());
}
@override
@ -26,6 +29,6 @@ class MetadataPluginBrowseSectionsNotifier
final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider<
MetadataPluginBrowseSectionsNotifier,
SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>(
SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>>(
() => MetadataPluginBrowseSectionsNotifier(),
);

View File

@ -9,7 +9,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
FutureOr<bool> build() async {
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities
.contains(PluginAbilities.authentication) !=
.contains(PluginAbility.authentication) !=
true) {
return false;
}
@ -19,15 +19,16 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
return false;
}
final sub = defaultPlugin.auth.authStateStream.listen((event) {
state = AsyncData(defaultPlugin.auth.isAuthenticated());
final sub = defaultPlugin.authState().listen((event) async {
state = AsyncData(await defaultPlugin.auth
.isAuthenticated(mpscTx: defaultPlugin.sender));
});
ref.onDispose(() {
sub.cancel();
});
return defaultPlugin.auth.isAuthenticated();
return defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender);
}
}
@ -42,7 +43,7 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
if (defaultPluginConfig
.asData?.value.defaultAudioSourcePluginConfig?.abilities
.contains(PluginAbilities.authentication) !=
.contains(PluginAbility.authentication) !=
true) {
return false;
}
@ -52,15 +53,16 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
return false;
}
final sub = defaultPlugin.auth.authStateStream.listen((event) {
state = AsyncData(defaultPlugin.auth.isAuthenticated());
final sub = defaultPlugin.authState().listen((event) async {
state = AsyncData(await defaultPlugin.auth
.isAuthenticated(mpscTx: defaultPlugin.sender));
});
ref.onDispose(() {
sub.cancel();
});
return defaultPlugin.auth.isAuthenticated();
return defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender);
}
}

View File

@ -62,7 +62,7 @@ class MetadataPluginRepositoriesNotifier
return _hasMore[response.requestOptions.uri.host] ?? false;
});
return SpotubePaginationResponseObject(
return SpotubeFlattenedPaginationObject(
items: repos,
total: responses.fold<int>(
0,
@ -85,6 +85,6 @@ class MetadataPluginRepositoriesNotifier
final metadataPluginRepositoriesProvider = AsyncNotifierProvider<
MetadataPluginRepositoriesNotifier,
SpotubePaginationResponseObject<MetadataPluginRepository>>(
SpotubeFlattenedPaginationObject<MetadataPluginRepository>>(
() => MetadataPluginRepositoriesNotifier(),
);

View File

@ -17,7 +17,7 @@ class MetadataPluginScrobbleNotifier
if (metadataPlugin.valueOrNull == null ||
pluginConfig == null ||
!pluginConfig.abilities.contains(PluginAbilities.scrobbling)) {
!pluginConfig.abilities.contains(PluginAbility.scrobbling)) {
return null;
}
@ -25,23 +25,46 @@ class MetadataPluginScrobbleNotifier
final subscription = controller.stream.listen((event) async {
try {
await metadataPlugin.valueOrNull?.core.scrobble({
"id": event.id,
"title": event.name,
"artists": event.artists
.map((artist) => {
"id": artist.id,
"name": artist.name,
})
.toList(),
"album": {
"id": event.album.id,
"name": event.album.name,
},
"timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000,
"duration_ms": event.durationMs,
"isrc": event is SpotubeFullTrackObject ? event.isrc : null,
});
final details = switch (event) {
SpotubeTrackObject_Full(:final field0) => ScrobbleDetails(
id: field0.id,
title: field0.name,
artists: field0.artists
.map((artist) => ScrobbleArtist(
id: artist.id,
name: artist.name,
))
.toList(),
album: ScrobbleAlbum(
id: field0.album.id,
name: field0.album.name,
),
timestamp: DateTime.now().millisecondsSinceEpoch,
durationMs: field0.durationMs,
isrc: field0.isrc,
),
SpotubeTrackObject_Local(:final field0) => ScrobbleDetails(
id: field0.id,
title: field0.name,
artists: field0.artists
.map((artist) => ScrobbleArtist(
id: artist.id,
name: artist.name,
))
.toList(),
album: ScrobbleAlbum(
id: field0.album.id,
name: field0.album.name,
),
timestamp: DateTime.now().millisecondsSinceEpoch,
durationMs: field0.durationMs,
),
};
await metadataPlugin.valueOrNull?.core.scrobble(
mpscTx: metadataPlugin.valueOrNull!.sender,
details: details,
);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}

View File

@ -7,7 +7,7 @@ final metadataPluginSupportTextProvider = FutureProvider<String>((ref) async {
if (metadataPlugin == null) {
throw 'No metadata plugin available';
}
return await metadataPlugin.core.support;
return await metadataPlugin.core.support(mpscTx: metadataPlugin.sender);
});
final audioSourcePluginSupportTextProvider =
@ -17,5 +17,5 @@ final audioSourcePluginSupportTextProvider =
if (audioSourcePlugin == null) {
throw 'No metadata plugin available';
}
return await audioSourcePlugin.core.support;
return await audioSourcePlugin.core.support(mpscTx: audioSourcePlugin.sender);
});

View File

@ -12,6 +12,6 @@ final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
if (!authenticated || metadataPlugin == null) {
return null;
}
return metadataPlugin.user.me();
return metadataPlugin.user.me(mpscTx: metadataPlugin.sender);
},
);

View File

@ -6,14 +6,18 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginSavedAlbumNotifier
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
@override
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
int offset,
int limit,
) async {
return await (await metadataPlugin).user.savedAlbums(
return await (await metadataPlugin)
.user
.savedAlbums(
limit: limit,
offset: offset,
);
mpscTx: await mpscTx,
)
.then((a) => a.flatten());
}
@override
@ -35,7 +39,10 @@ class MetadataPluginSavedAlbumNotifier
),
);
try {
await (await metadataPlugin).album.save(albums.map((e) => e.id).toList());
await (await metadataPlugin).album.save(
ids: albums.map((e) => e.id).toList(),
mpscTx: await mpscTx,
);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -58,7 +65,9 @@ class MetadataPluginSavedAlbumNotifier
),
);
try {
await (await metadataPlugin).album.unsave(albumIds);
await (await metadataPlugin)
.album
.unsave(ids: albumIds, mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -68,7 +77,7 @@ class MetadataPluginSavedAlbumNotifier
final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
MetadataPluginSavedAlbumNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
() => MetadataPluginSavedAlbumNotifier(),
);

View File

@ -6,16 +6,17 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginSavedArtistNotifier
extends PaginatedAsyncNotifier<SpotubeFullArtistObject> {
@override
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
int offset,
int limit,
) async {
final artists = await (await metadataPlugin).user.savedArtists(
limit: limit,
offset: offset,
mpscTx: await mpscTx,
);
return artists;
return artists.flatten();
}
@override
@ -39,7 +40,7 @@ class MetadataPluginSavedArtistNotifier
try {
await (await metadataPlugin)
.artist
.save(artists.map((e) => e.id).toList());
.save(ids: artists.map((e) => e.id).toList(), mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -63,7 +64,9 @@ class MetadataPluginSavedArtistNotifier
);
try {
await (await metadataPlugin).artist.unsave(artistIds);
await (await metadataPlugin)
.artist
.unsave(ids: artistIds, mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -73,7 +76,7 @@ class MetadataPluginSavedArtistNotifier
final metadataPluginSavedArtistsProvider = AsyncNotifierProvider<
MetadataPluginSavedArtistNotifier,
SpotubePaginationResponseObject<SpotubeFullArtistObject>>(
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>>(
() => MetadataPluginSavedArtistNotifier(),
);

View File

@ -15,9 +15,9 @@ class MetadataPluginSavedPlaylistsNotifier
fetch(int offset, int limit) async {
final playlists = await (await metadataPlugin)
.user
.savedPlaylists(limit: limit, offset: offset);
.savedPlaylists(limit: limit, offset: offset, mpscTx: await mpscTx);
return playlists;
return playlists.flatten();
}
@override
@ -58,7 +58,9 @@ class MetadataPluginSavedPlaylistsNotifier
);
try {
await (await metadataPlugin).playlist.save(playlist.id);
await (await metadataPlugin)
.playlist
.save(playlistId: playlist.id, mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -76,7 +78,9 @@ class MetadataPluginSavedPlaylistsNotifier
);
try {
await (await metadataPlugin).playlist.unsave(playlist.id);
await (await metadataPlugin)
.playlist
.unsave(playlistId: playlist.id, mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -88,7 +92,9 @@ class MetadataPluginSavedPlaylistsNotifier
final oldState = state;
try {
state = const AsyncLoading();
await (await metadataPlugin).playlist.deletePlaylist(playlistId);
await (await metadataPlugin)
.playlist
.deletePlaylist(playlistId: playlistId, mpscTx: await mpscTx);
ref.invalidateSelf();
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlistId));
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
@ -101,9 +107,8 @@ class MetadataPluginSavedPlaylistsNotifier
Future<void> addTracks(String playlistId, List<String> trackIds) async {
if (state.value == null) return;
await (await metadataPlugin)
.playlist
.addTracks(playlistId, trackIds: trackIds);
await (await metadataPlugin).playlist.addTracks(
playlistId: playlistId, trackIds: trackIds, mpscTx: await mpscTx);
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
}
@ -111,9 +116,8 @@ class MetadataPluginSavedPlaylistsNotifier
Future<void> removeTracks(String playlistId, List<String> trackIds) async {
if (state.value == null) return;
await (await metadataPlugin)
.playlist
.removeTracks(playlistId, trackIds: trackIds);
await (await metadataPlugin).playlist.removeTracks(
playlistId: playlistId, trackIds: trackIds, mpscTx: await mpscTx);
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
}
@ -121,7 +125,7 @@ class MetadataPluginSavedPlaylistsNotifier
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
MetadataPluginSavedPlaylistsNotifier,
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>(
SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>>(
() => MetadataPluginSavedPlaylistsNotifier(),
);

View File

@ -13,9 +13,10 @@ class MetadataPluginSavedTracksNotifier
final tracks = await (await metadataPlugin).user.savedTracks(
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return tracks;
return tracks.flatten();
}
@override
@ -26,7 +27,7 @@ class MetadataPluginSavedTracksNotifier
return await fetch(0, 20);
}
Future<void> addFavorite(List<SpotubeTrackObject> tracks) async {
Future<void> addFavorite(List<SpotubeFullTrackObject> tracks) async {
if (state.value == null) {
return;
}
@ -34,22 +35,21 @@ class MetadataPluginSavedTracksNotifier
final oldState = state.value;
state = AsyncData(
state.value!.copyWith(
items: [
...tracks.whereType<SpotubeFullTrackObject>(),
...state.value!.items
],
items: [...tracks, ...state.value!.items],
),
);
try {
await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList());
await (await metadataPlugin)
.track
.save(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
}
}
Future<void> removeFavorite(List<SpotubeTrackObject> tracks) async {
Future<void> removeFavorite(List<SpotubeFullTrackObject> tracks) async {
if (state.value == null) {
return;
}
@ -68,7 +68,7 @@ class MetadataPluginSavedTracksNotifier
try {
await (await metadataPlugin)
.track
.unsave(tracks.map((e) => e.id).toList());
.unsave(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
} catch (e) {
state = AsyncData(oldState!);
rethrow;
@ -78,7 +78,7 @@ class MetadataPluginSavedTracksNotifier
final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider<
MetadataPluginSavedTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>>(
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>>(
() => MetadataPluginSavedTracksNotifier(),
);

View File

@ -11,7 +11,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
import 'package:spotube/provider/server/server.dart';
import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
@ -24,6 +24,8 @@ final allowedDomainsRegex = RegExp(
r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+",
);
final kPluginApiVersion = Version.parse("2.0.0");
class MetadataPluginState {
final List<PluginConfiguration> plugins;
final int defaultMetadataPlugin;
@ -129,7 +131,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
repository: plugin.repository,
apis: plugin.apis
.map(
(e) => PluginApis.values.firstWhereOrNull(
(e) => PluginApi.values.firstWhereOrNull(
(api) => api.name == e,
),
)
@ -137,7 +139,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
.toList(),
abilities: plugin.abilities
.map(
(e) => PluginAbilities.values.firstWhereOrNull(
(e) => PluginAbility.values.firstWhereOrNull(
(ability) => ability.name == e,
),
)
@ -149,7 +151,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
final pluginJsonFile =
File(join(pluginExtractionDir.path, "plugin.json"));
final pluginBinaryFile =
File(join(pluginExtractionDir.path, "plugin.out"));
File(join(pluginExtractionDir.path, "plugin.js"));
if (!await pluginExtractionDir.exists() ||
!await pluginJsonFile.exists() ||
@ -374,13 +376,12 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
bool validatePluginApiCompatibility(PluginConfiguration plugin) {
final configPluginApiVersion = Version.parse(plugin.pluginApiVersion);
final appPluginApiVersion = MetadataPlugin.pluginApiVersion;
// Plugin API's major version must match the app's major version
if (configPluginApiVersion.major != appPluginApiVersion.major) {
if (configPluginApiVersion.major != kPluginApiVersion.major) {
return false;
}
return configPluginApiVersion >= appPluginApiVersion;
return configPluginApiVersion >= kPluginApiVersion;
}
void _assertPluginApiCompatibility(PluginConfiguration plugin) {
@ -419,18 +420,18 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
selectedForMetadata: Value(
(state.valueOrNull?.plugins
.where(
(d) => d.abilities.contains(PluginAbilities.metadata))
(d) => d.abilities.contains(PluginAbility.metadata))
.isEmpty ??
true) &&
plugin.abilities.contains(PluginAbilities.metadata),
plugin.abilities.contains(PluginAbility.metadata),
),
selectedForAudioSource: Value(
(state.valueOrNull?.plugins
.where((d) =>
d.abilities.contains(PluginAbilities.audioSource))
d.abilities.contains(PluginAbility.audioSource))
.isEmpty ??
true) &&
plugin.abilities.contains(PluginAbilities.audioSource),
plugin.abilities.contains(PluginAbility.audioSource),
),
),
);
@ -450,8 +451,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
// only when there is 1 remaining plugin
if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) {
final remainingPlugins = state.valueOrNull?.plugins.where(
(p) =>
p != plugin && p.abilities.contains(PluginAbilities.metadata),
(p) => p != plugin && p.abilities.contains(PluginAbility.metadata),
) ??
[];
if (remainingPlugins.length == 1) {
@ -462,8 +462,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) {
final remainingPlugins = state.valueOrNull?.plugins.where(
(p) =>
p != plugin &&
p.abilities.contains(PluginAbilities.audioSource),
p != plugin && p.abilities.contains(PluginAbility.audioSource),
) ??
[];
if (remainingPlugins.length == 1) {
@ -523,7 +522,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async {
assert(
plugin.abilities.contains(PluginAbilities.metadata),
plugin.abilities.contains(PluginAbility.metadata),
"Must be a metadata plugin",
);
@ -541,7 +540,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async {
assert(
plugin.abilities.contains(PluginAbilities.audioSource),
plugin.abilities.contains(PluginAbility.audioSource),
"Must be an audio-source plugin",
);
@ -556,16 +555,16 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
);
}
Future<Uint8List> getPluginByteCode(PluginConfiguration plugin) async {
Future<String> getPluginSourceCode(PluginConfiguration plugin) async {
final pluginExtractionDirPath = await _getPluginExtractionDir(plugin);
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.out"));
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.js"));
if (!libraryFile.existsSync()) {
throw MetadataPluginException.pluginByteCodeFileNotFound();
throw MetadataPluginException.pluginSourceCodeFileNotFound();
}
return await libraryFile.readAsBytes();
return await libraryFile.readAsString();
}
Future<File?> getLogoPath(PluginConfiguration plugin) async {
@ -586,27 +585,41 @@ final metadataPluginsProvider =
MetadataPluginNotifier.new,
);
final _pluginProvider =
FutureProvider.family<MetadataPlugin?, PluginConfiguration?>(
(ref, config) async {
final (:server, :port) = await ref.watch(serverProvider.future);
final serverSecret = ref.watch(serverRandomSecretProvider);
if (config == null) {
return null;
}
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
final pluginSourceCode = await pluginsNotifier.getPluginSourceCode(config);
final plugin = MetadataPlugin(
pluginScript: pluginSourceCode,
pluginConfig: config,
serverEndpointUrl: "http://${server.address.host}:$port",
serverSecret: serverSecret,
);
ref.onDispose(() {
plugin.close();
});
return plugin;
},
);
final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
(ref) async {
final defaultPlugin = await ref.watch(
metadataPluginsProvider
.selectAsync((data) => data.defaultMetadataPluginConfig),
);
final youtubeEngine = ref.read(youtubeEngineProvider);
if (defaultPlugin == null) {
return null;
}
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
final pluginByteCode =
await pluginsNotifier.getPluginByteCode(defaultPlugin);
return await MetadataPlugin.create(
youtubeEngine,
defaultPlugin,
pluginByteCode,
);
return await ref.watch(_pluginProvider(defaultPlugin).future);
},
);
@ -616,20 +629,6 @@ final audioSourcePluginProvider = FutureProvider<MetadataPlugin?>(
metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig),
);
final youtubeEngine = ref.watch(youtubeEngineProvider);
if (defaultPlugin == null) {
return null;
}
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
final pluginByteCode =
await pluginsNotifier.getPluginByteCode(defaultPlugin);
return await MetadataPlugin.create(
youtubeEngine,
defaultPlugin,
pluginByteCode,
);
return await ref.watch(_pluginProvider(defaultPlugin).future);
},
);

View File

@ -6,6 +6,7 @@ import 'package:spotube/provider/metadata_plugin/core/user.dart';
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:spotube/services/metadata/metadata.dart';
import 'package:spotube/src/rust/api/plugin/plugin.dart';
class MetadataPluginPlaylistNotifier
extends AutoDisposeFamilyAsyncNotifier<SpotubeFullPlaylistObject, String> {
@ -19,11 +20,17 @@ class MetadataPluginPlaylistNotifier
return metadataPlugin;
}
Future<OpaqueSender> get mpscTx async {
return (await metadataPlugin).sender;
}
@override
build(playlistId) async {
ref.cacheFor();
return (await metadataPlugin).playlist.getPlaylist(playlistId);
return (await metadataPlugin)
.playlist
.getPlaylist(id: playlistId, mpscTx: await mpscTx);
}
Future<void> create({
@ -40,12 +47,13 @@ class MetadataPluginPlaylistNotifier
}
state = const AsyncValue.loading();
try {
final playlist = await (await metadataPlugin).playlist.create(
userId,
final playlist = await (await metadataPlugin).playlist.createPlaylist(
userId: userId,
name: name,
description: description,
public: public,
collaborative: collaborative,
mpscTx: await mpscTx,
);
if (playlist != null) {
state = AsyncValue.data(playlist);
@ -71,12 +79,13 @@ class MetadataPluginPlaylistNotifier
collaborative == null) {
throw Exception('No modifications provided.');
}
await (await metadataPlugin).playlist.update(
arg,
await (await metadataPlugin).playlist.updatePlaylist(
playlistId: arg,
name: name,
description: description,
public: public,
collaborative: collaborative,
mpscTx: await mpscTx,
);
ref.invalidateSelf();
} on Exception catch (e) {

View File

@ -12,7 +12,7 @@ class MetadataPluginSearchAlbumsNotifier
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>(
return SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>(
limit: limit,
nextOffset: null,
total: 0,
@ -22,12 +22,13 @@ class MetadataPluginSearchAlbumsNotifier
}
final res = await (await metadataPlugin).search.albums(
arg,
query: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return res;
return res.flatten();
}
@override
@ -41,6 +42,6 @@ class MetadataPluginSearchAlbumsNotifier
final metadataPluginSearchAlbumsProvider =
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchAlbumsNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>, String>(
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>, String>(
() => MetadataPluginSearchAlbumsNotifier(),
);

View File

@ -12,7 +12,8 @@ final metadataPluginSearchAllProvider =
throw MetadataPluginException.noDefaultMetadataPlugin();
}
return metadataPlugin.search.all(query);
return metadataPlugin.search
.all(query: query, mpscTx: metadataPlugin.sender);
},
);
@ -22,5 +23,5 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async {
if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultMetadataPlugin();
}
return metadataPlugin.search.chips;
return metadataPlugin.search.chips(mpscTx: metadataPlugin.sender);
});

View File

@ -12,7 +12,7 @@ class MetadataPluginSearchArtistsNotifier
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeFullArtistObject>(
return SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>(
limit: limit,
nextOffset: null,
total: 0,
@ -22,12 +22,13 @@ class MetadataPluginSearchArtistsNotifier
}
final res = await (await metadataPlugin).search.artists(
arg,
query: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return res;
return res.flatten();
}
@override
@ -41,6 +42,6 @@ class MetadataPluginSearchArtistsNotifier
final metadataPluginSearchArtistsProvider =
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchArtistsNotifier,
SpotubePaginationResponseObject<SpotubeFullArtistObject>, String>(
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>, String>(
() => MetadataPluginSearchArtistsNotifier(),
);

View File

@ -12,7 +12,7 @@ class MetadataPluginSearchPlaylistsNotifier
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>(
return SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>(
limit: limit,
nextOffset: null,
total: 0,
@ -22,12 +22,13 @@ class MetadataPluginSearchPlaylistsNotifier
}
final res = await (await metadataPlugin).search.playlists(
arg,
query: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return res;
return res.flatten();
}
@override
@ -42,7 +43,7 @@ class MetadataPluginSearchPlaylistsNotifier
final metadataPluginSearchPlaylistsProvider =
AutoDisposeAsyncNotifierProviderFamily<
MetadataPluginSearchPlaylistsNotifier,
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>,
SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>,
String>(
() => MetadataPluginSearchPlaylistsNotifier(),
);

View File

@ -5,14 +5,14 @@ import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
class MetadataPluginSearchTracksNotifier
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject,
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
String> {
MetadataPluginSearchTracksNotifier() : super();
@override
fetch(offset, limit) async {
if (arg.isEmpty) {
return SpotubePaginationResponseObject<SpotubeFullTrackObject>(
return SpotubeFlattenedPaginationObject<SpotubeTrackObject>(
limit: limit,
nextOffset: null,
total: 0,
@ -22,12 +22,13 @@ class MetadataPluginSearchTracksNotifier
}
final tracks = await (await metadataPlugin).search.tracks(
arg,
query: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return tracks;
return tracks.flatten();
}
@override
@ -41,6 +42,6 @@ class MetadataPluginSearchTracksNotifier
final metadataPluginSearchTracksProvider =
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
SpotubeFlattenedPaginationObject<SpotubeTrackObject>, String>(
() => MetadataPluginSearchTracksNotifier(),
);

View File

@ -12,12 +12,13 @@ class MetadataPluginAlbumTracksNotifier
@override
fetch(offset, limit) async {
final tracks = await (await metadataPlugin).album.tracks(
arg,
id: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return tracks;
return tracks.flatten();
}
@override
@ -31,6 +32,6 @@ class MetadataPluginAlbumTracksNotifier
final metadataPluginAlbumTracksProvider =
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginAlbumTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>, String>(
() => MetadataPluginAlbumTracksNotifier(),
);

View File

@ -12,12 +12,13 @@ class MetadataPluginPlaylistTracksNotifier
@override
fetch(offset, limit) async {
final tracks = await (await metadataPlugin).playlist.tracks(
arg,
id: arg,
offset: offset,
limit: limit,
mpscTx: await mpscTx,
);
return tracks;
return tracks.flatten();
}
@override
@ -31,6 +32,6 @@ class MetadataPluginPlaylistTracksNotifier
final metadataPluginPlaylistTracksProvider =
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginPlaylistTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>, String>(
() => MetadataPluginPlaylistTracksNotifier(),
);

View File

@ -11,5 +11,7 @@ final metadataPluginTrackProvider =
throw MetadataPluginException.noDefaultMetadataPlugin();
}
return metadataPlugin.track.getTrack(trackId);
return await metadataPlugin.track
.getTrack(id: trackId, mpscTx: metadataPlugin.sender);
});

View File

@ -12,8 +12,10 @@ final metadataPluginUpdateCheckerProvider =
return null;
}
return metadataPlugin.core
.checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!);
return metadataPlugin.core.checkUpdate(
pluginConfig: metadataPluginConfigs.defaultMetadataPluginConfig!,
mpscTx: metadataPlugin.sender,
);
});
final audioSourcePluginUpdateCheckerProvider =
@ -27,6 +29,8 @@ final audioSourcePluginUpdateCheckerProvider =
return null;
}
return audioSourcePlugin.core
.checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!);
return audioSourcePlugin.core.checkUpdate(
pluginConfig: audioSourcePluginConfigs.defaultAudioSourcePluginConfig!,
mpscTx: audioSourcePlugin.sender,
);
});

View File

@ -8,6 +8,7 @@ import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:spotube/services/metadata/metadata.dart';
import 'package:spotube/src/rust/api/plugin/plugin.dart';
extension PaginationExtension<T> on AsyncValue<T> {
bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext;
@ -15,7 +16,7 @@ extension PaginationExtension<T> on AsyncValue<T> {
mixin MetadataPluginMixin<K>
// ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
Future<MetadataPlugin> get metadataPlugin async {
final plugin = await ref.read(metadataPluginProvider.future);
@ -25,6 +26,11 @@ mixin MetadataPluginMixin<K>
return plugin;
}
Future<OpaqueSender> get mpscTx async {
final plugin = await metadataPlugin;
return plugin.sender;
}
}
extension AutoDisposeAsyncNotifierCacheFor

View File

@ -7,9 +7,9 @@ import 'package:spotube/provider/metadata_plugin/utils/common.dart';
import 'package:spotube/services/logger/logger.dart';
abstract class FamilyPaginatedAsyncNotifier<K, A>
extends FamilyAsyncNotifier<SpotubePaginationResponseObject<K>, A>
extends FamilyAsyncNotifier<SpotubeFlattenedPaginationObject<K>, A>
with MetadataPluginMixin<K> {
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return;
@ -74,9 +74,9 @@ abstract class FamilyPaginatedAsyncNotifier<K, A>
}
abstract class AutoDisposeFamilyPaginatedAsyncNotifier<K, A>
extends AutoDisposeFamilyAsyncNotifier<SpotubePaginationResponseObject<K>,
extends AutoDisposeFamilyAsyncNotifier<SpotubeFlattenedPaginationObject<K>,
A> with MetadataPluginMixin<K> {
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return;

View File

@ -10,8 +10,8 @@ import 'package:spotube/services/logger/logger.dart';
mixin PaginatedAsyncNotifierMixin<K>
// ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return;
@ -75,9 +75,9 @@ mixin PaginatedAsyncNotifierMixin<K>
}
abstract class PaginatedAsyncNotifier<K>
extends AsyncNotifier<SpotubePaginationResponseObject<K>>
extends AsyncNotifier<SpotubeFlattenedPaginationObject<K>>
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
abstract class AutoDisposePaginatedAsyncNotifier<K>
extends AutoDisposeAsyncNotifier<SpotubePaginationResponseObject<K>>
extends AutoDisposeAsyncNotifier<SpotubeFlattenedPaginationObject<K>>
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}

View File

@ -10,8 +10,8 @@ import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/logger/logger.dart';
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
final StreamController<SpotubeTrackObject> _scrobbleController =
StreamController<SpotubeTrackObject>.broadcast();
final _scrobbleController =
StreamController<SpotubeFullTrackObject>.broadcast();
@override
build() async {
final database = ref.watch(databaseProvider);
@ -107,18 +107,18 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
await database.delete(database.scrobblerTable).go();
}
void scrobble(SpotubeTrackObject track) {
void scrobble(SpotubeFullTrackObject track) {
_scrobbleController.add(track);
}
Future<void> love(SpotubeTrackObject track) async {
Future<void> love(SpotubeFullTrackObject track) async {
await state.asData?.value?.track.love(
artist: track.artists.asString(),
track: track.name,
);
}
Future<void> unlove(SpotubeTrackObject track) async {
Future<void> unlove(SpotubeFullTrackObject track) async {
await state.asData?.value?.track.unLove(
artist: track.artists.asString(),
track: track.name,

View File

@ -16,7 +16,7 @@ final activeTrackSourcesProvider = FutureProvider<
return null;
}
if (audioPlayerState.activeTrack is SpotubeLocalTrackObject) {
if (audioPlayerState.activeTrack is SpotubeTrackObject_Local) {
return (
source: null,
notifier: null,
@ -26,12 +26,12 @@ final activeTrackSourcesProvider = FutureProvider<
final sourcedTrack = await ref.watch(
sourcedTrackProvider(
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
audioPlayerState.activeTrack?.field0 as SpotubeFullTrackObject,
).future,
);
final sourcedTrackNotifier = ref.watch(
sourcedTrackProvider(
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
audioPlayerState.activeTrack?.field0 as SpotubeFullTrackObject,
).notifier,
);

View File

@ -161,7 +161,7 @@ class ServerConnectRoutes {
event.onLoad((event) async {
await audioPlayerNotifier.load(
event.data.tracks.cast<SpotubeFullTrackObject>().toList(),
event.data.tracks.cast<SpotubeTrackObject>().toList(),
autoPlay: true,
initialIndex: event.data.initialIndex ?? 0,
);

View File

@ -70,8 +70,9 @@ class ServerPlaybackRoutes {
final sourcedTrack = activeSourcedTrack?.track.id == track.id
? activeSourcedTrack?.source
: await ref.read(
sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject)
.future,
sourcedTrackProvider(
spotubeMedia.track.field0 as SpotubeFullTrackObject,
).future,
);
return sourcedTrack;
@ -258,7 +259,7 @@ class ServerPlaybackRoutes {
await MetadataGod.writeMetadata(
file: trackCacheFile.path,
metadata: track.query.toMetadata(
metadata: SpotubeTrackObject.full(track.query).toMetadata(
imageBytes: imageBytes,
fileLength: fileLength,
),

View File

@ -98,7 +98,8 @@ class TrackOptionsActions {
throw MetadataPluginException.noDefaultMetadataPlugin();
}
final tracks = await metadataPlugin.track.radio(track.id);
final tracks = await metadataPlugin.track
.radio(id: track.id, mpscTx: metadataPlugin.sender);
bool replaceQueue = false;
@ -123,7 +124,7 @@ class TrackOptionsActions {
}
await playback.addTracks(
tracks.toList()
tracks.union()
..removeWhere((e) {
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
return e.id == track.id || isDuplicate;
@ -202,14 +203,17 @@ class TrackOptionsActions {
}
break;
case TrackOptionValue.favorite:
if (track is SpotubeTrackObject_Local) break;
final isLikedTrack = await ref.read(
metadataPluginIsSavedTrackProvider(track.id).future,
);
if (isLikedTrack) {
await favoriteTracks.removeFavorite([track]);
await favoriteTracks
.removeFavorite([track.field0 as SpotubeFullTrackObject]);
} else {
await favoriteTracks.addFavorite([track]);
await favoriteTracks
.addFavorite([track.field0 as SpotubeFullTrackObject]);
}
break;
case TrackOptionValue.addToPlaylist:
@ -236,18 +240,19 @@ class TrackOptionsActions {
actionShare(context);
break;
case TrackOptionValue.details:
if (track is! SpotubeFullTrackObject) break;
if (track is! SpotubeTrackObject_Full) break;
showDialog(
context: context,
builder: (context) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: TrackDetailsDialog(track: track as SpotubeFullTrackObject),
child: TrackDetailsDialog(
track: track.field0 as SpotubeFullTrackObject),
),
);
break;
case TrackOptionValue.download:
if (track is SpotubeLocalTrackObject) break;
downloadManager.addToQueue(track as SpotubeFullTrackObject);
if (track is SpotubeTrackObject_Local) break;
downloadManager.addToQueue(track.field0 as SpotubeFullTrackObject);
break;
case TrackOptionValue.startRadio:
actionStartRadio(context);

View File

@ -24,13 +24,14 @@ class SpotubeMedia extends mk.Media {
final SpotubeTrackObject track;
SpotubeMedia(this.track)
: assert(
track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject,
track is SpotubeTrackObject_Local ||
((track.field0 as SpotubeFullTrackObject).isrc.isNotEmpty),
"Track must be a either a local track or a full track object with ISRC",
),
// If the track is a local track, use its path, otherwise use the server URL
super(
track is SpotubeLocalTrackObject
? track.path
track is SpotubeTrackObject_Local
? track.field0.path
: "http://$_host:$serverPort/stream/${track.id}",
extras: track.toJson(),
);

View File

@ -1,78 +0,0 @@
import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesLocalStorage implements Localstorage {
final SharedPreferences _prefs;
final String pluginSlug;
SharedPreferencesLocalStorage(this._prefs, this.pluginSlug);
String prefix(String key) {
return 'spotube_plugin.$pluginSlug.$key';
}
@override
Future<void> clear() {
return _prefs.clear();
}
@override
Future<bool> containsKey(String key) async {
return _prefs.containsKey(prefix(key));
}
@override
Future<bool?> getBool(String key) async {
return _prefs.getBool(prefix(key));
}
@override
Future<double?> getDouble(String key) async {
return _prefs.getDouble(prefix(key));
}
@override
Future<int?> getInt(String key) async {
return _prefs.getInt(prefix(key));
}
@override
Future<String?> getString(String key) async {
return _prefs.getString(prefix(key));
}
@override
Future<List<String>?> getStringList(String key) async {
return _prefs.getStringList(prefix(key));
}
@override
Future<void> remove(String key) async {
await _prefs.remove(prefix(key));
}
@override
Future<void> setBool(String key, bool value) async {
await _prefs.setBool(prefix(key), value);
}
@override
Future<void> setDouble(String key, double value) async {
await _prefs.setDouble(prefix(key), value);
}
@override
Future<void> setInt(String key, int value) async {
await _prefs.setInt(prefix(key), value);
}
@override
Future<void> setString(String key, String value) async {
await _prefs.setString(prefix(key), value);
}
@override
Future<void> setStringList(String key, List<String> value) async {
await _prefs.setStringList(prefix(key), value);
}
}

View File

@ -1,75 +0,0 @@
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<SpotubeFullTrackObject>.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<SpotubeSimpleAlbumObject>.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],
);
}
}

Some files were not shown because too many files have changed in this diff Show More