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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,11 @@ import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
typedef UseTrackToggleLike = ({ typedef UseTrackToggleLike = ({
bool isLiked, bool isLiked,
bool isLoading, 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 = final savedTracksNotifier =
ref.watch(metadataPluginSavedTracksProvider.notifier); ref.watch(metadataPluginSavedTracksProvider.notifier);

View File

@ -80,7 +80,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
required String action, required String action,
}) async { }) async {
final fullTrackObjects = final fullTrackObjects =
tracks.whereType<SpotubeFullTrackObject>().toList(); tracks.whereType<SpotubeTrackObject_Full>().toList();
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
@ -89,7 +89,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
) ?? ) ??
false; false;
if (confirmed != true) return; if (confirmed != true) return;
downloader.addAllToQueue(fullTrackObjects); downloader.addAllToQueue(fullTrackObjects.map((e) => e.field0).toList());
notifier.deselectAllTracks(); notifier.deselectAllTracks();
if (!context.mounted) return; if (!context.mounted) return;
showToastForAction(context, action, fullTrackObjects.length); 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_tile/track_tile.dart';
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'; import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
import 'package:spotube/extensions/context.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:spotube/provider/audio_player/audio_player.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';

View File

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

View File

@ -44,7 +44,7 @@ class PresentationStateNotifier
next.whenData((value) { next.whenData((value) {
state = state.copyWith( state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks( presentationTracks: ServiceUtils.sortTracks(
value.items, value.items.union(),
state.sortBy, state.sortBy,
), ),
); );
@ -62,7 +62,7 @@ class PresentationStateNotifier
next.whenData((value) { next.whenData((value) {
state = state.copyWith( state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks( presentationTracks: ServiceUtils.sortTracks(
value.items, value.items.union(),
state.sortBy, state.sortBy,
), ),
); );
@ -109,7 +109,7 @@ class PresentationStateNotifier
} ?? } ??
<SpotubeFullTrackObject>[]; <SpotubeFullTrackObject>[];
return tracks; return tracks.union();
} }
void selectTrack(SpotubeTrackObject track) { 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/components/track_presentation/use_is_user_playlist.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
class TrackPresentationTopSection extends HookConsumerWidget { 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/models/metadata/metadata.dart';
import 'package:spotube/provider/track_options/track_options_provider.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 { class TrackOptions extends HookConsumerWidget {
final SpotubeTrackObject track; final SpotubeTrackObject track;
final bool userPlaylist; final bool userPlaylist;
@ -26,8 +26,8 @@ class TrackOptions extends HookConsumerWidget {
this.icon, this.icon,
this.onTapItem, this.onTapItem,
}) : assert( }) : assert(
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject, track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject", "Track must be a SpotubeTrackObject, SpotubeLocalTrackObject",
); );
@override @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/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -24,12 +25,15 @@ void useEndlessPlayback(WidgetRef ref) {
final track = playlist.tracks.last; 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; if (tracks == null || tracks.isEmpty) return;
await playback.addTracks( await playback.addTracks(
tracks.toList() tracks.union()
..removeWhere((e) { ..removeWhere((e) {
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final isDuplicate = playlist.tracks.any((t) => t.id == e.id); final isDuplicate = playlist.tracks.any((t) => t.id == e.id);

View File

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

View File

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

View File

@ -338,14 +338,14 @@ class WebSocketRemoveTrackEvent extends WebSocketEvent<String> {
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data); WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
} }
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeFullTrackObject> { class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeTrackObject> {
WebSocketAddTrackEvent(SpotubeFullTrackObject data) WebSocketAddTrackEvent(SpotubeTrackObject data)
: super(WsEvent.addTrack, data); : super(WsEvent.addTrack, data);
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json) WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
: super( : super(
WsEvent.addTrack, WsEvent.addTrack,
SpotubeFullTrackObject.fromJson( SpotubeTrackObject.fromJson(
json["data"] as Map<String, dynamic>, 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'; 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> { extension SpotubeFullArtistObjectAsString on List<SpotubeFullArtistObject> {
String asString() { String asString() {
return map((e) => e.name).join(", "); return map((e) => e.name).join(", ");

View File

@ -7,29 +7,7 @@ enum SpotubeMediaCompressionType {
lossless, lossless,
} }
@Freezed(unionKey: 'type') extension GetFileExtension on SpotubeAudioSourceContainerPreset {
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);
String getFileExtension() { String getFileExtension() {
return switch (name) { return switch (name) {
"mp4" => "m4a", "mp4" => "m4a",
@ -39,72 +17,16 @@ class SpotubeAudioSourceContainerPreset
} }
} }
@freezed extension ToStringSpotubeAudioLossyContainerQuality
class SpotubeAudioLossyContainerQuality on SpotubeAudioLossyContainerQuality {
with _$SpotubeAudioLossyContainerQuality { toFormattedString() {
const SpotubeAudioLossyContainerQuality._();
factory SpotubeAudioLossyContainerQuality({
required int bitrate, // bits per second
}) = _SpotubeAudioLossyContainerQuality;
factory SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioLossyContainerQualityFromJson(json);
@override
toString() {
return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps"; return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps";
} }
} }
@freezed extension ToStringSpotubeAudioLosslessContainerQuality
class SpotubeAudioLosslessContainerQuality on SpotubeAudioLosslessContainerQuality {
with _$SpotubeAudioLosslessContainerQuality { toFormattedString() {
const SpotubeAudioLosslessContainerQuality._();
factory SpotubeAudioLosslessContainerQuality({
required int bitDepth, // bit
required int sampleRate, // hz
}) = _SpotubeAudioLosslessContainerQuality;
factory SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioLosslessContainerQualityFromJson(json);
@override
toString() {
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz"; 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'; part of 'metadata.dart';
@Freezed(genericArgumentFactories: true) class SpotubeFlattenedBrowseSectionObject<T> {
class SpotubeBrowseSectionObject<T> with _$SpotubeBrowseSectionObject<T> { final String id;
factory SpotubeBrowseSectionObject({ final String title;
required String id, final String externalUri;
required String title, final bool browseMore;
required String externalUri, final List<T> items;
required bool browseMore,
required List<T> items,
}) = _SpotubeBrowseSectionObject<T>;
factory SpotubeBrowseSectionObject.fromJson( SpotubeFlattenedBrowseSectionObject({
Map<String, Object?> json, required this.id,
T Function(Map<String, dynamic> json) fromJsonT, required this.title,
) => required this.browseMore,
_$SpotubeBrowseSectionObjectFromJson<T>( required this.externalUri,
json, required this.items,
(json) => fromJsonT(json as Map<String, dynamic>), });
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'; 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 { enum ImagePlaceholder {
albumArt, albumArt,
artist, artist,

View File

@ -10,23 +10,31 @@ import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:spotube/collections/assets.gen.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/services/audio_player/audio_player.dart';
import 'package:spotube/utils/primitive_utils.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.g.dart';
part 'metadata.freezed.dart'; part 'metadata.freezed.dart';
part 'audio_source.dart';
part 'album.dart';
part 'artist.dart'; part 'artist.dart';
part 'audio_source.dart';
part 'browse.dart'; part 'browse.dart';
part 'fields.dart'; part 'fields.dart';
part 'image.dart'; part 'image.dart';
part 'pagination.dart'; part 'pagination.dart';
part 'playlist.dart';
part 'search.dart';
part 'track.dart'; part 'track.dart';
part 'user.dart';
part 'plugin.dart';
part 'repository.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 // 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( _$MetadataFormFieldInputObjectImpl _$$MetadataFormFieldInputObjectImplFromJson(
Map json) => Map json) =>
_$MetadataFormFieldInputObjectImpl( _$MetadataFormFieldInputObjectImpl(
@ -316,286 +52,6 @@ Map<String, dynamic> _$$MetadataFormFieldTextObjectImplToJson(
'text': instance.text, '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( _$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
Map json) => Map json) =>
_$MetadataPluginRepositoryImpl( _$MetadataPluginRepositoryImpl(

View File

@ -1,22 +1,78 @@
part of 'metadata.dart'; part of 'metadata.dart';
@Freezed(genericArgumentFactories: true) class SpotubeFlattenedPaginationObject<T> {
class SpotubePaginationResponseObject<T> final int limit;
with _$SpotubePaginationResponseObject<T> { final int? nextOffset;
factory SpotubePaginationResponseObject({ final int total;
required int limit, final bool hasMore;
required int? nextOffset, final List<T> items;
required int total,
required bool hasMore,
required List<T> items,
}) = _SpotubePaginationResponseObject<T>;
factory SpotubePaginationResponseObject.fromJson( SpotubeFlattenedPaginationObject({
Map<String, Object?> json, required this.limit,
T Function(Map<String, dynamic> json) fromJsonT, required this.nextOffset,
) => required this.total,
_$SpotubePaginationResponseObjectFromJson<T>( required this.hasMore,
json, required this.items,
(json) => fromJsonT(json as Map<String, dynamic>), });
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'; 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> { extension AsMediaListSpotubeTrackObject on Iterable<SpotubeTrackObject> {
List<SpotubeMedia> asMediaList() { List<SpotubeMedia> asMediaList() {
return map((track) => SpotubeMedia(track)).toList(); 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({ Metadata toMetadata({
required int fileLength, required int fileLength,
Uint8List? imageBytes, 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( BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
query: SpotubeFullTrackObject.fromJson( query: SpotubeTrackObject.fromJson(
Map<String, dynamic>.from(json['query'] as Map)), Map<String, dynamic>.from(json['query'] as Map)),
source: json['source'] as String, source: json['source'] as String,
info: SpotubeAudioSourceMatchObject.fromJson( info: SpotubeAudioSourceMatchObject.fromJson(

View File

@ -88,12 +88,12 @@ class AlbumCard extends HookConsumerWidget {
final remotePlayback = ref.read(connectProvider.notifier); final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load( await remotePlayback.load(
WebSocketLoadEventData.album( WebSocketLoadEventData.album(
tracks: fetchedTracks, tracks: fetchedTracks.union(),
collection: album, collection: album,
), ),
); );
} else { } else {
await playlistNotifier.load(fetchedTracks, autoPlay: true); await playlistNotifier.load(fetchedTracks.union(), autoPlay: true);
playlistNotifier.addCollection(album.id); playlistNotifier.addCollection(album.id);
historyNotifier.addAlbums([album]); historyNotifier.addAlbums([album]);
} }
@ -123,7 +123,9 @@ class AlbumCard extends HookConsumerWidget {
final fetchedTracks = await fetchAllTrack(); final fetchedTracks = await fetchAllTrack();
if (fetchedTracks.isEmpty) return; if (fetchedTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addTracks(
fetchedTracks.union(),
);
playlistNotifier.addCollection(album.id); playlistNotifier.addCollection(album.id);
historyNotifier.addAlbums([album]); historyNotifier.addAlbums([album]);
if (context.mounted) { 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'; import 'package:url_launcher/url_launcher.dart';
final validAbilities = { final validAbilities = {
PluginAbilities.metadata: ("Metadata", SpotubeIcons.album), PluginAbility.metadata: ("Metadata", SpotubeIcons.album),
PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music), PluginAbility.audioSource: ("Audio Source", SpotubeIcons.music),
}; };
class MetadataInstalledPluginItem extends HookConsumerWidget { class MetadataInstalledPluginItem extends HookConsumerWidget {
@ -44,9 +44,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier); final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) && final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
plugin.abilities.contains(PluginAbilities.authentication); plugin.abilities.contains(PluginAbility.authentication);
final supportsScrobbling = isDefaultMetadata && final supportsScrobbling = isDefaultMetadata &&
plugin.abilities.contains(PluginAbilities.scrobbling); plugin.abilities.contains(PluginAbility.scrobbling);
final isMetadataAuthenticatedSnapshot = final isMetadataAuthenticatedSnapshot =
ref.watch(metadataPluginAuthenticatedProvider); ref.watch(metadataPluginAuthenticatedProvider);
@ -253,7 +253,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
if (plugin.abilities.contains(PluginAbilities.metadata)) if (plugin.abilities.contains(PluginAbility.metadata))
Button.secondary( Button.secondary(
enabled: !isDefaultMetadata, enabled: !isDefaultMetadata,
onPressed: () async { onPressed: () async {
@ -265,7 +265,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
: context.l10n.set_default_metadata_source, : context.l10n.set_default_metadata_source,
), ),
), ),
if (plugin.abilities.contains(PluginAbilities.audioSource)) if (plugin.abilities.contains(PluginAbility.audioSource))
Button.secondary( Button.secondary(
enabled: !isDefaultAudioSource, enabled: !isDefaultAudioSource,
onPressed: () async { onPressed: () async {
@ -378,8 +378,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
!isAuthenticated) !isAuthenticated)
Button.primary( Button.primary(
onPressed: () async { onPressed: () async {
await pluginSnapshot?.asData?.value?.auth if ((pluginSnapshot?.hasValue ?? false) == false) {
.authenticate(); return;
}
await pluginSnapshot!.value!.auth.authenticate(
mpscTx: pluginSnapshot.value!.sender,
);
}, },
leading: const Icon(SpotubeIcons.login), leading: const Icon(SpotubeIcons.login),
child: Text(context.l10n.login), child: Text(context.l10n.login),
@ -389,7 +394,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
isAuthenticated) isAuthenticated)
Button.destructive( Button.destructive(
onPressed: () async { 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), leading: const Icon(SpotubeIcons.logout),
child: Text(context.l10n.logout), child: Text(context.l10n.logout),

View File

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

View File

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

View File

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

View File

@ -98,19 +98,23 @@ class PlaylistCard extends HookConsumerWidget {
final allTracks = await fetchAllTracks(); final allTracks = await fetchAllTracks();
await remotePlayback.load( await remotePlayback.load(
WebSocketLoadEventData.playlist( WebSocketLoadEventData.playlist(
tracks: allTracks, tracks: allTracks.union(),
collection: playlist, collection: playlist,
), ),
); );
} else { } else {
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true); await playlistNotifier.load(
fetchedInitialTracks.union(),
autoPlay: true,
);
playlistNotifier.addCollection(playlist.id); playlistNotifier.addCollection(playlist.id);
historyNotifier.addPlaylists([playlist]); historyNotifier.addPlaylists([playlist]);
final allTracks = await fetchAllTracks(); final allTracks = await fetchAllTracks();
await playlistNotifier await playlistNotifier.addTracks(
.addTracks(allTracks.sublist(fetchedInitialTracks.length)); allTracks.sublist(fetchedInitialTracks.length).union(),
);
} }
} finally { } finally {
if (context.mounted) { if (context.mounted) {
@ -142,7 +146,9 @@ class PlaylistCard extends HookConsumerWidget {
if (fetchedInitialTracks.isEmpty) return; if (fetchedInitialTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedInitialTracks); playlistNotifier.addTracks(
fetchedInitialTracks.union(),
);
playlistNotifier.addCollection(playlist.id); playlistNotifier.addCollection(playlist.id);
historyNotifier.addPlaylists([playlist]); historyNotifier.addPlaylists([playlist]);
if (context.mounted) { 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/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.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/pages/search/search.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/metadata_plugin/search/all.dart'; import 'package:spotube/provider/metadata_plugin/search/all.dart';
import 'package:spotube/src/rust/api/plugin/models/track.dart';
class SearchTracksSection extends HookConsumerWidget { class SearchTracksSection extends HookConsumerWidget {
const SearchTracksSection({ const SearchTracksSection({
@ -41,7 +43,8 @@ class SearchTracksSection extends HookConsumerWidget {
if (search.isLoading) if (search.isLoading)
const CircularProgressIndicator() const CircularProgressIndicator()
else else
...tracks.mapIndexed((i, track) { ...tracks.mapIndexed((i, mehTrack) {
final track = SpotubeTrackObject.full(mehTrack);
return TrackTile( return TrackTile(
index: i, index: i,
track: track, track: track,

View File

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

View File

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

View File

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

View File

@ -55,17 +55,17 @@ class LocalLibraryPage extends HookConsumerWidget {
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
final isPlaylistPlaying = playlist.containsTracks(tracks); final isPlaylistPlaying = playlist.containsTracks(tracks.union());
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id); var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id);
await playback.load( await playback.load(
tracks, tracks.union(),
initialIndex: indexWhere, initialIndex: indexWhere,
autoPlay: true, autoPlay: true,
); );
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != playlist.activeTrack?.id) { currentTrack.id != playlist.activeTrack?.id) {
await playback.jumpToTrack(currentTrack); await playback.jumpToTrack(SpotubeTrackObject.local(currentTrack));
} }
} }
@ -75,12 +75,12 @@ class LocalLibraryPage extends HookConsumerWidget {
) async { ) async {
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
final isPlaylistPlaying = playlist.containsTracks(tracks); final isPlaylistPlaying = playlist.containsTracks(tracks.union());
final shuffledTracks = tracks.shuffled(); final shuffledTracks = tracks.shuffled();
if (isPlaylistPlaying) return; if (isPlaylistPlaying) return;
await playback.load( await playback.load(
shuffledTracks, shuffledTracks.union(),
initialIndex: 0, initialIndex: 0,
autoPlay: true, autoPlay: true,
); );
@ -93,9 +93,9 @@ class LocalLibraryPage extends HookConsumerWidget {
) async { ) async {
final playlist = ref.read(audioPlayerProvider); final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier); final playback = ref.read(audioPlayerProvider.notifier);
final isPlaylistPlaying = playlist.containsTracks(tracks); final isPlaylistPlaying = playlist.containsTracks(tracks.union());
if (isPlaylistPlaying) return; if (isPlaylistPlaying) return;
await playback.addTracks(tracks); await playback.addTracks(tracks.union());
if (!context.mounted) return; if (!context.mounted) return;
showToastForAction(context, "add-to-queue", tracks.length); showToastForAction(context, "add-to-queue", tracks.length);
} }
@ -109,7 +109,7 @@ class LocalLibraryPage extends HookConsumerWidget {
final trackSnapshot = ref.watch(localTracksProvider); final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = useMemoized( final isPlaylistPlaying = useMemoized(
() => playlist.containsTracks( () => playlist.containsTracks(
trackSnapshot.asData?.value[location] ?? [], trackSnapshot.asData?.value[location]?.union() ?? [],
), ),
[playlist, trackSnapshot, location], [playlist, trackSnapshot, location],
); );
@ -382,7 +382,7 @@ class LocalLibraryPage extends HookConsumerWidget {
data: (tracks) { data: (tracks) {
final sortedTracks = useMemoized(() { final sortedTracks = useMemoized(() {
return ServiceUtils.sortTracks( return ServiceUtils.sortTracks(
tracks[location] ?? <SpotubeLocalTrackObject>[], tracks[location]?.union() ?? <SpotubeTrackObject>[],
sortBy.value); sortBy.value);
}, [sortBy.value, tracks]); }, [sortBy.value, tracks]);
@ -463,8 +463,12 @@ class LocalLibraryPage extends HookConsumerWidget {
onTap: () async { onTap: () async {
await playLocalTracks( await playLocalTracks(
ref, ref,
sortedTracks, sortedTracks
currentTrack: track, .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 () => me.asData?.value == null
? null ? null
: SpotubeSimplePlaylistObject( : SpotubeSimplePlaylistObject(
typeName: "playlist_simple",
id: "user-liked-tracks", id: "user-liked-tracks",
name: context.l10n.liked_tracks, name: context.l10n.liked_tracks,
description: context.l10n.liked_tracks_description, description: context.l10n.liked_tracks_description,
@ -50,6 +51,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
owner: me.asData!.value!, owner: me.asData!.value!,
images: [ images: [
SpotubeImageObject( SpotubeImageObject(
typeName: "image",
url: Assets.images.likedTracks.path, url: Assets.images.likedTracks.path,
width: 300, width: 300,
height: 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.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:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.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_controls.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';

View File

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

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart' as material; import 'package:flutter/material.dart' as material;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
@ -50,6 +51,10 @@ class PlaylistPage extends HookConsumerWidget {
ref.watch(metadataPluginSavedPlaylistsProvider.notifier); ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id); final isUserPlaylist = useIsUserPlaylist(ref, playlist.id);
final tracksMemoized = useMemoized(
() => tracks.asData?.value.items.union() ?? [],
[tracks.asData?.value],
);
return material.RefreshIndicator.adaptive( return material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
@ -71,14 +76,15 @@ class PlaylistPage extends HookConsumerWidget {
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id)); ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
}, },
onFetchAll: () async { onFetchAll: () async {
return await tracksNotifier.fetchAll(); final res = await tracksNotifier.fetchAll();
return res.union();
}, },
), ),
title: playlist.name, title: playlist.name,
description: playlist.description, description: playlist.description,
owner: playlist.owner.name, owner: playlist.owner.name,
ownerImage: playlist.owner.images.lastOrNull?.url, ownerImage: playlist.owner.images.lastOrNull?.url,
tracks: tracks.asData?.value.items ?? [], tracks: tracksMemoized,
error: tracks.error, error: tracks.error,
routePath: '/playlist/${playlist.id}', routePath: '/playlist/${playlist.id}',
isLiked: isFavoritePlaylist.asData?.value ?? false, 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/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.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/modules/search/loading.dart';
import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/audio_player/audio_player.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 plugins.asData?.value.plugins.where((d) {
return d.abilities.contains( return d.abilities.contains(
tabState.value == 1 tabState.value == 1
? PluginAbilities.metadata ? PluginAbility.metadata
: PluginAbilities.audioSource, : PluginAbility.audioSource,
); );
}).toList(); }).toList();
}, [tabState.value, plugins.asData?.value]); }, [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:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/modules/stats/common/track_item.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';

View File

@ -44,7 +44,8 @@ class TrackPage extends HookConsumerWidget {
final trackQuery = ref.watch(metadataPluginTrackProvider(trackId)); 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 { void onPlay() async {
if (isActive) { if (isActive) {
@ -230,7 +231,10 @@ class TrackPage extends HookConsumerWidget {
const Spacer() const Spacer()
else else
const Gap(20), const Gap(20),
TrackHeartButton(track: track), TrackHeartButton(
track: track.field0
as SpotubeFullTrackObject,
),
TrackOptionsButton( TrackOptionsButton(
track: track, track: track,
userPlaylist: false, userPlaylist: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,9 +81,12 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
.nonNulls .nonNulls
.toList(); .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( assert(
@ -109,7 +112,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
final items = getTracksWithCount(entries); final items = getTracksWithCount(entries);
return SpotubePaginationResponseObject<PlaybackHistoryTrack>( return SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>(
items: items, items: items,
nextOffset: offset + limit, nextOffset: offset + limit,
total: items.length, total: items.length,
@ -190,7 +193,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
final historyTopTracksProvider = AsyncNotifierProviderFamily< final historyTopTracksProvider = AsyncNotifierProviderFamily<
HistoryTopTracksNotifier, HistoryTopTracksNotifier,
SpotubePaginationResponseObject<PlaybackHistoryTrack>, SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>,
HistoryDuration>( HistoryDuration>(
() => HistoryTopTracksNotifier(), () => HistoryTopTracksNotifier(),
); );

View File

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

View File

@ -15,6 +15,6 @@ final metadataPluginAlbumProvider =
throw MetadataPluginException.noDefaultMetadataPlugin(); 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 class MetadataPluginAlbumReleasesNotifier
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> { extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
@override @override
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch( Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
int offset, int offset,
int limit, int limit,
) async { ) async {
return await (await metadataPlugin) return await (await metadataPlugin)
.album .album
.releases(limit: limit, offset: offset); .releases(mpscTx: await mpscTx, limit: limit, offset: offset)
.then((a) => a.flatten());
} }
@override @override
@ -24,6 +25,6 @@ class MetadataPluginAlbumReleasesNotifier
final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider< final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider<
MetadataPluginAlbumReleasesNotifier, MetadataPluginAlbumReleasesNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>( SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
() => MetadataPluginAlbumReleasesNotifier(), () => MetadataPluginAlbumReleasesNotifier(),
); );

View File

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

View File

@ -15,6 +15,7 @@ final metadataPluginArtistProvider =
throw MetadataPluginException.noDefaultMetadataPlugin(); 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 class MetadataPluginArtistRelatedArtistsNotifier
extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> { extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> {
@override @override
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch( Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
int offset, int offset,
int limit, int limit,
) async { ) async {
return await (await metadataPlugin).artist.related( return await (await metadataPlugin)
arg, .artist
.related(
id: arg,
limit: limit, limit: limit,
offset: offset, offset: offset,
); mpscTx: await mpscTx,
)
.then((a) => a.flatten());
} }
@override @override
@ -26,7 +30,7 @@ class MetadataPluginArtistRelatedArtistsNotifier
final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily< final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily<
MetadataPluginArtistRelatedArtistsNotifier, MetadataPluginArtistRelatedArtistsNotifier,
SpotubePaginationResponseObject<SpotubeFullArtistObject>, SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>,
String>( String>(
() => MetadataPluginArtistRelatedArtistsNotifier(), () => 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'; import 'package:spotube/provider/metadata_plugin/utils/common.dart';
class MetadataPluginArtistTopTracksNotifier class MetadataPluginArtistTopTracksNotifier
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject, extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
String> { String> {
MetadataPluginArtistTopTracksNotifier() : super(); MetadataPluginArtistTopTracksNotifier() : super();
@override @override
fetch(offset, limit) async { fetch(offset, limit) async {
final tracks = await (await metadataPlugin).artist.topTracks( final tracks = await (await metadataPlugin).artist.topTracks(
arg, id: arg,
offset: offset, offset: offset,
limit: limit, limit: limit,
mpscTx: await mpscTx,
); );
return tracks; return tracks.flatten();
} }
@override @override
@ -32,7 +33,7 @@ class MetadataPluginArtistTopTracksNotifier
final metadataPluginArtistTopTracksProvider = final metadataPluginArtistTopTracksProvider =
AutoDisposeAsyncNotifierProviderFamily< AutoDisposeAsyncNotifierProviderFamily<
MetadataPluginArtistTopTracksNotifier, MetadataPluginArtistTopTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>, SpotubeFlattenedPaginationObject<SpotubeTrackObject>,
String>( String>(
() => MetadataPluginArtistTopTracksNotifier(), () => MetadataPluginArtistTopTracksNotifier(),
); );

View File

@ -41,10 +41,10 @@ class AudioSourceAvailableQualityPresetsNotifier
listenSelf((previous, next) { listenSelf((previous, next) {
final isNewLossless = final isNewLossless =
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex) next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
is SpotubeAudioSourceContainerPresetLossless; is SpotubeAudioSourceContainerPreset_Lossless;
final isOldLossless = previous?.presets final isOldLossless = previous?.presets
.elementAtOrNull(previous.selectedStreamingContainerIndex) .elementAtOrNull(previous.selectedStreamingContainerIndex)
is SpotubeAudioSourceContainerPresetLossless; is SpotubeAudioSourceContainerPreset_Lossless;
if (!isOldLossless && isNewLossless) { if (!isOldLossless && isNewLossless) {
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
} else if (isOldLossless && !isNewLossless) { } else if (isOldLossless && !isNewLossless) {
@ -72,11 +72,13 @@ class AudioSourceAvailableQualityPresetsNotifier
state = state =
AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)) AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr))
.copyWith( .copyWith(
presets: audioSource.audioSource.supportedPresets, presets: await audioSource.audioSource
.supportedPresets(mpscTx: audioSource.sender),
); );
} else { } else {
state = AudioSourcePresetsState( 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 class MetadataPluginBrowseSectionItemsNotifier
extends FamilyPaginatedAsyncNotifier<Object, String> { extends FamilyPaginatedAsyncNotifier<Object, String> {
@override @override
Future<SpotubePaginationResponseObject<Object>> fetch( Future<SpotubeFlattenedPaginationObject<Object>> fetch(
int offset, int offset,
int limit, int limit,
) async { ) async {
return await (await metadataPlugin).browse.sectionItems( return await (await metadataPlugin)
arg, .browse
.sectionItems(
id: arg,
limit: limit, limit: limit,
offset: offset, offset: offset,
); mpscTx: await mpscTx,
)
.then((value) => value.flatten());
} }
@override @override
@ -26,7 +30,7 @@ class MetadataPluginBrowseSectionItemsNotifier
final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily< final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily<
MetadataPluginBrowseSectionItemsNotifier, MetadataPluginBrowseSectionItemsNotifier,
SpotubePaginationResponseObject<Object>, SpotubeFlattenedPaginationObject<Object>,
String>( String>(
() => MetadataPluginBrowseSectionItemsNotifier(), () => 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'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginBrowseSectionsNotifier class MetadataPluginBrowseSectionsNotifier
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject<Object>> { extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject> {
@override @override
Future<SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>> Future<SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>> fetch(
fetch(
int offset, int offset,
int limit, int limit,
) async { ) async {
return await (await metadataPlugin).browse.sections( return await (await metadataPlugin)
.browse
.sections(
limit: limit, limit: limit,
offset: offset, offset: offset,
); mpscTx: await mpscTx,
)
.then((value) => value.flatten());
} }
@override @override
@ -26,6 +29,6 @@ class MetadataPluginBrowseSectionsNotifier
final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider< final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider<
MetadataPluginBrowseSectionsNotifier, MetadataPluginBrowseSectionsNotifier,
SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>( SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>>(
() => MetadataPluginBrowseSectionsNotifier(), () => MetadataPluginBrowseSectionsNotifier(),
); );

View File

@ -9,7 +9,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
FutureOr<bool> build() async { FutureOr<bool> build() async {
final defaultPluginConfig = ref.watch(metadataPluginsProvider); final defaultPluginConfig = ref.watch(metadataPluginsProvider);
if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities
.contains(PluginAbilities.authentication) != .contains(PluginAbility.authentication) !=
true) { true) {
return false; return false;
} }
@ -19,15 +19,16 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
return false; return false;
} }
final sub = defaultPlugin.auth.authStateStream.listen((event) { final sub = defaultPlugin.authState().listen((event) async {
state = AsyncData(defaultPlugin.auth.isAuthenticated()); state = AsyncData(await defaultPlugin.auth
.isAuthenticated(mpscTx: defaultPlugin.sender));
}); });
ref.onDispose(() { ref.onDispose(() {
sub.cancel(); 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); final defaultPluginConfig = ref.watch(metadataPluginsProvider);
if (defaultPluginConfig if (defaultPluginConfig
.asData?.value.defaultAudioSourcePluginConfig?.abilities .asData?.value.defaultAudioSourcePluginConfig?.abilities
.contains(PluginAbilities.authentication) != .contains(PluginAbility.authentication) !=
true) { true) {
return false; return false;
} }
@ -52,15 +53,16 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
return false; return false;
} }
final sub = defaultPlugin.auth.authStateStream.listen((event) { final sub = defaultPlugin.authState().listen((event) async {
state = AsyncData(defaultPlugin.auth.isAuthenticated()); state = AsyncData(await defaultPlugin.auth
.isAuthenticated(mpscTx: defaultPlugin.sender));
}); });
ref.onDispose(() { ref.onDispose(() {
sub.cancel(); 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 _hasMore[response.requestOptions.uri.host] ?? false;
}); });
return SpotubePaginationResponseObject( return SpotubeFlattenedPaginationObject(
items: repos, items: repos,
total: responses.fold<int>( total: responses.fold<int>(
0, 0,
@ -85,6 +85,6 @@ class MetadataPluginRepositoriesNotifier
final metadataPluginRepositoriesProvider = AsyncNotifierProvider< final metadataPluginRepositoriesProvider = AsyncNotifierProvider<
MetadataPluginRepositoriesNotifier, MetadataPluginRepositoriesNotifier,
SpotubePaginationResponseObject<MetadataPluginRepository>>( SpotubeFlattenedPaginationObject<MetadataPluginRepository>>(
() => MetadataPluginRepositoriesNotifier(), () => MetadataPluginRepositoriesNotifier(),
); );

View File

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

View File

@ -7,7 +7,7 @@ final metadataPluginSupportTextProvider = FutureProvider<String>((ref) async {
if (metadataPlugin == null) { if (metadataPlugin == null) {
throw 'No metadata plugin available'; throw 'No metadata plugin available';
} }
return await metadataPlugin.core.support; return await metadataPlugin.core.support(mpscTx: metadataPlugin.sender);
}); });
final audioSourcePluginSupportTextProvider = final audioSourcePluginSupportTextProvider =
@ -17,5 +17,5 @@ final audioSourcePluginSupportTextProvider =
if (audioSourcePlugin == null) { if (audioSourcePlugin == null) {
throw 'No metadata plugin available'; 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) { if (!authenticated || metadataPlugin == null) {
return 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 class MetadataPluginSavedAlbumNotifier
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> { extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
@override @override
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch( Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
int offset, int offset,
int limit, int limit,
) async { ) async {
return await (await metadataPlugin).user.savedAlbums( return await (await metadataPlugin)
.user
.savedAlbums(
limit: limit, limit: limit,
offset: offset, offset: offset,
); mpscTx: await mpscTx,
)
.then((a) => a.flatten());
} }
@override @override
@ -35,7 +39,10 @@ class MetadataPluginSavedAlbumNotifier
), ),
); );
try { 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) { } catch (e) {
state = AsyncData(oldState!); state = AsyncData(oldState!);
rethrow; rethrow;
@ -58,7 +65,9 @@ class MetadataPluginSavedAlbumNotifier
), ),
); );
try { try {
await (await metadataPlugin).album.unsave(albumIds); await (await metadataPlugin)
.album
.unsave(ids: albumIds, mpscTx: await mpscTx);
} catch (e) { } catch (e) {
state = AsyncData(oldState!); state = AsyncData(oldState!);
rethrow; rethrow;
@ -68,7 +77,7 @@ class MetadataPluginSavedAlbumNotifier
final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider< final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
MetadataPluginSavedAlbumNotifier, MetadataPluginSavedAlbumNotifier,
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>( SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
() => MetadataPluginSavedAlbumNotifier(), () => MetadataPluginSavedAlbumNotifier(),
); );

View File

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

View File

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

View File

@ -13,9 +13,10 @@ class MetadataPluginSavedTracksNotifier
final tracks = await (await metadataPlugin).user.savedTracks( final tracks = await (await metadataPlugin).user.savedTracks(
offset: offset, offset: offset,
limit: limit, limit: limit,
mpscTx: await mpscTx,
); );
return tracks; return tracks.flatten();
} }
@override @override
@ -26,7 +27,7 @@ class MetadataPluginSavedTracksNotifier
return await fetch(0, 20); return await fetch(0, 20);
} }
Future<void> addFavorite(List<SpotubeTrackObject> tracks) async { Future<void> addFavorite(List<SpotubeFullTrackObject> tracks) async {
if (state.value == null) { if (state.value == null) {
return; return;
} }
@ -34,22 +35,21 @@ class MetadataPluginSavedTracksNotifier
final oldState = state.value; final oldState = state.value;
state = AsyncData( state = AsyncData(
state.value!.copyWith( state.value!.copyWith(
items: [ items: [...tracks, ...state.value!.items],
...tracks.whereType<SpotubeFullTrackObject>(),
...state.value!.items
],
), ),
); );
try { 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) { } catch (e) {
state = AsyncData(oldState!); state = AsyncData(oldState!);
rethrow; rethrow;
} }
} }
Future<void> removeFavorite(List<SpotubeTrackObject> tracks) async { Future<void> removeFavorite(List<SpotubeFullTrackObject> tracks) async {
if (state.value == null) { if (state.value == null) {
return; return;
} }
@ -68,7 +68,7 @@ class MetadataPluginSavedTracksNotifier
try { try {
await (await metadataPlugin) await (await metadataPlugin)
.track .track
.unsave(tracks.map((e) => e.id).toList()); .unsave(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
} catch (e) { } catch (e) {
state = AsyncData(oldState!); state = AsyncData(oldState!);
rethrow; rethrow;
@ -78,7 +78,7 @@ class MetadataPluginSavedTracksNotifier
final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider< final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider<
MetadataPluginSavedTracksNotifier, MetadataPluginSavedTracksNotifier,
SpotubePaginationResponseObject<SpotubeFullTrackObject>>( SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>>(
() => MetadataPluginSavedTracksNotifier(), () => 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/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/database/database.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/dio/dio.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart'; import 'package:spotube/services/metadata/errors/exceptions.dart';
@ -24,6 +24,8 @@ final allowedDomainsRegex = RegExp(
r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+", r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+",
); );
final kPluginApiVersion = Version.parse("2.0.0");
class MetadataPluginState { class MetadataPluginState {
final List<PluginConfiguration> plugins; final List<PluginConfiguration> plugins;
final int defaultMetadataPlugin; final int defaultMetadataPlugin;
@ -129,7 +131,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
repository: plugin.repository, repository: plugin.repository,
apis: plugin.apis apis: plugin.apis
.map( .map(
(e) => PluginApis.values.firstWhereOrNull( (e) => PluginApi.values.firstWhereOrNull(
(api) => api.name == e, (api) => api.name == e,
), ),
) )
@ -137,7 +139,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
.toList(), .toList(),
abilities: plugin.abilities abilities: plugin.abilities
.map( .map(
(e) => PluginAbilities.values.firstWhereOrNull( (e) => PluginAbility.values.firstWhereOrNull(
(ability) => ability.name == e, (ability) => ability.name == e,
), ),
) )
@ -149,7 +151,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
final pluginJsonFile = final pluginJsonFile =
File(join(pluginExtractionDir.path, "plugin.json")); File(join(pluginExtractionDir.path, "plugin.json"));
final pluginBinaryFile = final pluginBinaryFile =
File(join(pluginExtractionDir.path, "plugin.out")); File(join(pluginExtractionDir.path, "plugin.js"));
if (!await pluginExtractionDir.exists() || if (!await pluginExtractionDir.exists() ||
!await pluginJsonFile.exists() || !await pluginJsonFile.exists() ||
@ -374,13 +376,12 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
bool validatePluginApiCompatibility(PluginConfiguration plugin) { bool validatePluginApiCompatibility(PluginConfiguration plugin) {
final configPluginApiVersion = Version.parse(plugin.pluginApiVersion); final configPluginApiVersion = Version.parse(plugin.pluginApiVersion);
final appPluginApiVersion = MetadataPlugin.pluginApiVersion;
// Plugin API's major version must match the app's major version // 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 false;
} }
return configPluginApiVersion >= appPluginApiVersion; return configPluginApiVersion >= kPluginApiVersion;
} }
void _assertPluginApiCompatibility(PluginConfiguration plugin) { void _assertPluginApiCompatibility(PluginConfiguration plugin) {
@ -419,18 +420,18 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
selectedForMetadata: Value( selectedForMetadata: Value(
(state.valueOrNull?.plugins (state.valueOrNull?.plugins
.where( .where(
(d) => d.abilities.contains(PluginAbilities.metadata)) (d) => d.abilities.contains(PluginAbility.metadata))
.isEmpty ?? .isEmpty ??
true) && true) &&
plugin.abilities.contains(PluginAbilities.metadata), plugin.abilities.contains(PluginAbility.metadata),
), ),
selectedForAudioSource: Value( selectedForAudioSource: Value(
(state.valueOrNull?.plugins (state.valueOrNull?.plugins
.where((d) => .where((d) =>
d.abilities.contains(PluginAbilities.audioSource)) d.abilities.contains(PluginAbility.audioSource))
.isEmpty ?? .isEmpty ??
true) && 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 // only when there is 1 remaining plugin
if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) { if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) {
final remainingPlugins = state.valueOrNull?.plugins.where( final remainingPlugins = state.valueOrNull?.plugins.where(
(p) => (p) => p != plugin && p.abilities.contains(PluginAbility.metadata),
p != plugin && p.abilities.contains(PluginAbilities.metadata),
) ?? ) ??
[]; [];
if (remainingPlugins.length == 1) { if (remainingPlugins.length == 1) {
@ -462,8 +462,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) { if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) {
final remainingPlugins = state.valueOrNull?.plugins.where( final remainingPlugins = state.valueOrNull?.plugins.where(
(p) => (p) =>
p != plugin && p != plugin && p.abilities.contains(PluginAbility.audioSource),
p.abilities.contains(PluginAbilities.audioSource),
) ?? ) ??
[]; [];
if (remainingPlugins.length == 1) { if (remainingPlugins.length == 1) {
@ -523,7 +522,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async { Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async {
assert( assert(
plugin.abilities.contains(PluginAbilities.metadata), plugin.abilities.contains(PluginAbility.metadata),
"Must be a metadata plugin", "Must be a metadata plugin",
); );
@ -541,7 +540,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async { Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async {
assert( assert(
plugin.abilities.contains(PluginAbilities.audioSource), plugin.abilities.contains(PluginAbility.audioSource),
"Must be an audio-source plugin", "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 pluginExtractionDirPath = await _getPluginExtractionDir(plugin);
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.out")); final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.js"));
if (!libraryFile.existsSync()) { if (!libraryFile.existsSync()) {
throw MetadataPluginException.pluginByteCodeFileNotFound(); throw MetadataPluginException.pluginSourceCodeFileNotFound();
} }
return await libraryFile.readAsBytes(); return await libraryFile.readAsString();
} }
Future<File?> getLogoPath(PluginConfiguration plugin) async { Future<File?> getLogoPath(PluginConfiguration plugin) async {
@ -586,27 +585,41 @@ final metadataPluginsProvider =
MetadataPluginNotifier.new, 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?>( final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
(ref) async { (ref) async {
final defaultPlugin = await ref.watch( final defaultPlugin = await ref.watch(
metadataPluginsProvider metadataPluginsProvider
.selectAsync((data) => data.defaultMetadataPluginConfig), .selectAsync((data) => data.defaultMetadataPluginConfig),
); );
final youtubeEngine = ref.read(youtubeEngineProvider); return await ref.watch(_pluginProvider(defaultPlugin).future);
if (defaultPlugin == null) {
return null;
}
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
final pluginByteCode =
await pluginsNotifier.getPluginByteCode(defaultPlugin);
return await MetadataPlugin.create(
youtubeEngine,
defaultPlugin,
pluginByteCode,
);
}, },
); );
@ -616,20 +629,6 @@ final audioSourcePluginProvider = FutureProvider<MetadataPlugin?>(
metadataPluginsProvider metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig), .selectAsync((data) => data.defaultAudioSourcePluginConfig),
); );
final youtubeEngine = ref.watch(youtubeEngineProvider); return await ref.watch(_pluginProvider(defaultPlugin).future);
if (defaultPlugin == null) {
return null;
}
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
final pluginByteCode =
await pluginsNotifier.getPluginByteCode(defaultPlugin);
return await MetadataPlugin.create(
youtubeEngine,
defaultPlugin,
pluginByteCode,
);
}, },
); );

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

View File

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

View File

@ -12,7 +12,8 @@ final metadataPluginSearchAllProvider =
throw MetadataPluginException.noDefaultMetadataPlugin(); 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) { if (metadataPlugin == null) {
throw MetadataPluginException.noDefaultMetadataPlugin(); throw MetadataPluginException.noDefaultMetadataPlugin();
} }
return metadataPlugin.search.chips; return metadataPlugin.search.chips(mpscTx: metadataPlugin.sender);
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -11,5 +11,7 @@ final metadataPluginTrackProvider =
throw MetadataPluginException.noDefaultMetadataPlugin(); 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 null;
} }
return metadataPlugin.core return metadataPlugin.core.checkUpdate(
.checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!); pluginConfig: metadataPluginConfigs.defaultMetadataPluginConfig!,
mpscTx: metadataPlugin.sender,
);
}); });
final audioSourcePluginUpdateCheckerProvider = final audioSourcePluginUpdateCheckerProvider =
@ -27,6 +29,8 @@ final audioSourcePluginUpdateCheckerProvider =
return null; return null;
} }
return audioSourcePlugin.core return audioSourcePlugin.core.checkUpdate(
.checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!); 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/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/metadata/errors/exceptions.dart'; import 'package:spotube/services/metadata/errors/exceptions.dart';
import 'package:spotube/services/metadata/metadata.dart'; import 'package:spotube/services/metadata/metadata.dart';
import 'package:spotube/src/rust/api/plugin/plugin.dart';
extension PaginationExtension<T> on AsyncValue<T> { extension PaginationExtension<T> on AsyncValue<T> {
bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext; bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext;
@ -15,7 +16,7 @@ extension PaginationExtension<T> on AsyncValue<T> {
mixin MetadataPluginMixin<K> mixin MetadataPluginMixin<K>
// ignore: invalid_use_of_internal_member // ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> { on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
Future<MetadataPlugin> get metadataPlugin async { Future<MetadataPlugin> get metadataPlugin async {
final plugin = await ref.read(metadataPluginProvider.future); final plugin = await ref.read(metadataPluginProvider.future);
@ -25,6 +26,11 @@ mixin MetadataPluginMixin<K>
return plugin; return plugin;
} }
Future<OpaqueSender> get mpscTx async {
final plugin = await metadataPlugin;
return plugin.sender;
}
} }
extension AutoDisposeAsyncNotifierCacheFor extension AutoDisposeAsyncNotifierCacheFor

View File

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

View File

@ -10,8 +10,8 @@ import 'package:spotube/services/logger/logger.dart';
mixin PaginatedAsyncNotifierMixin<K> mixin PaginatedAsyncNotifierMixin<K>
// ignore: invalid_use_of_internal_member // ignore: invalid_use_of_internal_member
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> { on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit); Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
@ -75,9 +75,9 @@ mixin PaginatedAsyncNotifierMixin<K>
} }
abstract class PaginatedAsyncNotifier<K> abstract class PaginatedAsyncNotifier<K>
extends AsyncNotifier<SpotubePaginationResponseObject<K>> extends AsyncNotifier<SpotubeFlattenedPaginationObject<K>>
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {} with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
abstract class AutoDisposePaginatedAsyncNotifier<K> abstract class AutoDisposePaginatedAsyncNotifier<K>
extends AutoDisposeAsyncNotifier<SpotubePaginationResponseObject<K>> extends AutoDisposeAsyncNotifier<SpotubeFlattenedPaginationObject<K>>
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<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'; import 'package:spotube/services/logger/logger.dart';
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> { class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
final StreamController<SpotubeTrackObject> _scrobbleController = final _scrobbleController =
StreamController<SpotubeTrackObject>.broadcast(); StreamController<SpotubeFullTrackObject>.broadcast();
@override @override
build() async { build() async {
final database = ref.watch(databaseProvider); final database = ref.watch(databaseProvider);
@ -107,18 +107,18 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
await database.delete(database.scrobblerTable).go(); await database.delete(database.scrobblerTable).go();
} }
void scrobble(SpotubeTrackObject track) { void scrobble(SpotubeFullTrackObject track) {
_scrobbleController.add(track); _scrobbleController.add(track);
} }
Future<void> love(SpotubeTrackObject track) async { Future<void> love(SpotubeFullTrackObject track) async {
await state.asData?.value?.track.love( await state.asData?.value?.track.love(
artist: track.artists.asString(), artist: track.artists.asString(),
track: track.name, track: track.name,
); );
} }
Future<void> unlove(SpotubeTrackObject track) async { Future<void> unlove(SpotubeFullTrackObject track) async {
await state.asData?.value?.track.unLove( await state.asData?.value?.track.unLove(
artist: track.artists.asString(), artist: track.artists.asString(),
track: track.name, track: track.name,

View File

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

View File

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

View File

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

View File

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

View File

@ -24,13 +24,14 @@ class SpotubeMedia extends mk.Media {
final SpotubeTrackObject track; final SpotubeTrackObject track;
SpotubeMedia(this.track) SpotubeMedia(this.track)
: assert( : 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", "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 // If the track is a local track, use its path, otherwise use the server URL
super( super(
track is SpotubeLocalTrackObject track is SpotubeTrackObject_Local
? track.path ? track.field0.path
: "http://$_host:$serverPort/stream/${track.id}", : "http://$_host:$serverPort/stream/${track.id}",
extras: track.toJson(), 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