mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-02-03 23:52:52 +00:00
Compare commits
2 Commits
2b9c5730c9
...
1e1f2ca82c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e1f2ca82c | ||
|
|
bd2275a89f |
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -52,7 +52,7 @@
|
||||
"--flavor",
|
||||
"dev"
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
"compounds": []
|
||||
}
|
||||
@ -3,13 +3,15 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/history/summary.dart';
|
||||
|
||||
abstract class FakeData {
|
||||
static final SpotubeImageObject image = SpotubeImageObject(
|
||||
static const SpotubeImageObject image = SpotubeImageObject(
|
||||
typeName: "image",
|
||||
height: 100,
|
||||
width: 100,
|
||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||
);
|
||||
|
||||
static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
|
||||
static const SpotubeFullArtistObject artist = SpotubeFullArtistObject(
|
||||
typeName: "artist_full",
|
||||
id: "1",
|
||||
name: "What an artist",
|
||||
externalUri: "https://example.com",
|
||||
@ -17,6 +19,7 @@ abstract class FakeData {
|
||||
genres: ["genre"],
|
||||
images: [
|
||||
SpotubeImageObject(
|
||||
typeName: "image",
|
||||
height: 100,
|
||||
width: 100,
|
||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||
@ -24,7 +27,8 @@ abstract class FakeData {
|
||||
],
|
||||
);
|
||||
|
||||
static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
|
||||
static const SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
|
||||
typeName: "album_full",
|
||||
id: "1",
|
||||
name: "A good album",
|
||||
externalUri: "https://example.com",
|
||||
@ -37,15 +41,17 @@ abstract class FakeData {
|
||||
recordLabel: "Record Label",
|
||||
);
|
||||
|
||||
static final SpotubeSimpleArtistObject artistSimple =
|
||||
static const SpotubeSimpleArtistObject artistSimple =
|
||||
SpotubeSimpleArtistObject(
|
||||
typeName: "artist_simple",
|
||||
id: "1",
|
||||
name: "What an artist",
|
||||
externalUri: "https://example.com",
|
||||
images: null,
|
||||
);
|
||||
|
||||
static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
|
||||
static const SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
|
||||
typeName: "album_simple",
|
||||
albumType: SpotubeAlbumType.album,
|
||||
artists: [],
|
||||
externalUri: "https://example.com",
|
||||
@ -54,6 +60,7 @@ abstract class FakeData {
|
||||
releaseDate: "2021-01-01",
|
||||
images: [
|
||||
SpotubeImageObject(
|
||||
typeName: "image",
|
||||
height: 1,
|
||||
width: 1,
|
||||
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||
@ -61,7 +68,9 @@ abstract class FakeData {
|
||||
],
|
||||
);
|
||||
|
||||
static final SpotubeFullTrackObject track = SpotubeTrackObject.full(
|
||||
static const SpotubeTrackObject track = SpotubeTrackObject.full(
|
||||
SpotubeFullTrackObject(
|
||||
typeName: "track",
|
||||
id: "1",
|
||||
name: "A good track",
|
||||
externalUri: "https://example.com",
|
||||
@ -69,16 +78,20 @@ abstract class FakeData {
|
||||
durationMs: 3 * 60 * 1000, // 3 minutes
|
||||
isrc: "USUM72112345",
|
||||
explicit: false,
|
||||
) as SpotubeFullTrackObject;
|
||||
artists: [artistSimple],
|
||||
),
|
||||
);
|
||||
|
||||
static final SpotubeUserObject user = SpotubeUserObject(
|
||||
static const SpotubeUserObject user = SpotubeUserObject(
|
||||
typeName: "user",
|
||||
id: "1",
|
||||
name: "User Name",
|
||||
externalUri: "https://example.com",
|
||||
images: [image],
|
||||
);
|
||||
|
||||
static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
|
||||
static const SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
|
||||
typeName: "playlist_full",
|
||||
id: "1",
|
||||
name: "A good playlist",
|
||||
description: "A very good playlist description",
|
||||
@ -89,8 +102,9 @@ abstract class FakeData {
|
||||
images: [image],
|
||||
collaborators: [user]);
|
||||
|
||||
static final SpotubeSimplePlaylistObject playlistSimple =
|
||||
static const SpotubeSimplePlaylistObject playlistSimple =
|
||||
SpotubeSimplePlaylistObject(
|
||||
typeName: "playlist_simple",
|
||||
id: "1",
|
||||
name: "A good playlist",
|
||||
description: "A very good playlist description",
|
||||
@ -99,13 +113,18 @@ abstract class FakeData {
|
||||
images: [image],
|
||||
);
|
||||
|
||||
static final SpotubeBrowseSectionObject browseSection =
|
||||
static const SpotubeBrowseSectionObject browseSection =
|
||||
SpotubeBrowseSectionObject(
|
||||
typeName: "browse_section",
|
||||
id: "section-id",
|
||||
title: "Browse Section",
|
||||
browseMore: true,
|
||||
externalUri: "https://example.com/browse/section",
|
||||
items: [playlistSimple, playlistSimple, playlistSimple]);
|
||||
items: [
|
||||
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
|
||||
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
|
||||
SpotubeBrowseSectionResponseObjectItem.playlistSimple(playlistSimple),
|
||||
]);
|
||||
|
||||
static const historySummary = PlaybackHistorySummary(
|
||||
albums: 1,
|
||||
|
||||
@ -225,7 +225,7 @@ class HomeBrowseSectionItemsRoute
|
||||
HomeBrowseSectionItemsRoute({
|
||||
_i44.Key? key,
|
||||
required String sectionId,
|
||||
required _i43.SpotubeBrowseSectionObject<Object> section,
|
||||
required _i43.SpotubeBrowseSectionObject section,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
HomeBrowseSectionItemsRoute.name,
|
||||
@ -264,7 +264,7 @@ class HomeBrowseSectionItemsRouteArgs {
|
||||
|
||||
final String sectionId;
|
||||
|
||||
final _i43.SpotubeBrowseSectionObject<Object> section;
|
||||
final _i43.SpotubeBrowseSectionObject section;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -632,7 +632,7 @@ class SettingsMetadataProviderFormRoute
|
||||
SettingsMetadataProviderFormRoute({
|
||||
_i44.Key? key,
|
||||
required String title,
|
||||
required List<_i43.MetadataFormFieldObject> fields,
|
||||
required List<dynamic> fields,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SettingsMetadataProviderFormRoute.name,
|
||||
@ -670,7 +670,7 @@ class SettingsMetadataProviderFormRouteArgs {
|
||||
|
||||
final String title;
|
||||
|
||||
final List<_i43.MetadataFormFieldObject> fields;
|
||||
final List<dynamic> fields;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@ -7,7 +7,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
|
||||
|
||||
class ReplaceDownloadedDialog extends ConsumerWidget {
|
||||
final SpotubeTrackObject track;
|
||||
final SpotubeFullTrackObject track;
|
||||
const ReplaceDownloadedDialog({required this.track, super.key});
|
||||
|
||||
@override
|
||||
|
||||
@ -37,7 +37,8 @@ class TrackDetailsDialog extends HookConsumerWidget {
|
||||
// style: const TextStyle(color: Colors.blue),
|
||||
// ),
|
||||
context.l10n.duration: sourcedTrack.asData != null
|
||||
? sourcedTrack.asData!.value.info.duration.toHumanReadableString()
|
||||
? Duration(milliseconds: sourcedTrack.asData!.value.info.duration)
|
||||
.toHumanReadableString()
|
||||
: Duration(milliseconds: track.durationMs).toHumanReadableString(),
|
||||
if (track.album.releaseDate != null)
|
||||
context.l10n.released: track.album.releaseDate,
|
||||
|
||||
@ -65,7 +65,7 @@ class HeartButton extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class TrackHeartButton extends HookConsumerWidget {
|
||||
final SpotubeTrackObject track;
|
||||
final SpotubeFullTrackObject track;
|
||||
const TrackHeartButton({
|
||||
super.key,
|
||||
required this.track,
|
||||
|
||||
@ -5,10 +5,11 @@ import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||
typedef UseTrackToggleLike = ({
|
||||
bool isLiked,
|
||||
bool isLoading,
|
||||
Future<void> Function(SpotubeTrackObject track) toggleTrackLike,
|
||||
Future<void> Function(SpotubeFullTrackObject track) toggleTrackLike,
|
||||
});
|
||||
|
||||
UseTrackToggleLike useTrackToggleLike(SpotubeTrackObject track, WidgetRef ref) {
|
||||
UseTrackToggleLike useTrackToggleLike(
|
||||
SpotubeFullTrackObject track, WidgetRef ref) {
|
||||
final savedTracksNotifier =
|
||||
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
||||
required String action,
|
||||
}) async {
|
||||
final fullTrackObjects =
|
||||
tracks.whereType<SpotubeFullTrackObject>().toList();
|
||||
tracks.whereType<SpotubeTrackObject_Full>().toList();
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@ -89,7 +89,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
||||
) ??
|
||||
false;
|
||||
if (confirmed != true) return;
|
||||
downloader.addAllToQueue(fullTrackObjects);
|
||||
downloader.addAllToQueue(fullTrackObjects.map((e) => e.field0).toList());
|
||||
notifier.deselectAllTracks();
|
||||
if (!context.mounted) return;
|
||||
showToastForAction(context, action, fullTrackObjects.length);
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:spotube/components/track_presentation/use_track_tile_play_callba
|
||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ class PaginationProps {
|
||||
final bool isLoading;
|
||||
final VoidCallback onFetchMore;
|
||||
final Future<void> Function() onRefresh;
|
||||
final Future<List<SpotubeFullTrackObject>> Function() onFetchAll;
|
||||
final Future<List<SpotubeTrackObject>> Function() onFetchAll;
|
||||
|
||||
const PaginationProps({
|
||||
required this.hasNextPage,
|
||||
@ -46,7 +46,7 @@ class TrackPresentationOptions {
|
||||
final String? ownerImage;
|
||||
final String image;
|
||||
final String routePath;
|
||||
final List<SpotubeFullTrackObject> tracks;
|
||||
final List<SpotubeTrackObject> tracks;
|
||||
final PaginationProps pagination;
|
||||
final bool isLiked;
|
||||
final String? shareUrl;
|
||||
|
||||
@ -44,7 +44,7 @@ class PresentationStateNotifier
|
||||
next.whenData((value) {
|
||||
state = state.copyWith(
|
||||
presentationTracks: ServiceUtils.sortTracks(
|
||||
value.items,
|
||||
value.items.union(),
|
||||
state.sortBy,
|
||||
),
|
||||
);
|
||||
@ -62,7 +62,7 @@ class PresentationStateNotifier
|
||||
next.whenData((value) {
|
||||
state = state.copyWith(
|
||||
presentationTracks: ServiceUtils.sortTracks(
|
||||
value.items,
|
||||
value.items.union(),
|
||||
state.sortBy,
|
||||
),
|
||||
);
|
||||
@ -109,7 +109,7 @@ class PresentationStateNotifier
|
||||
} ??
|
||||
<SpotubeFullTrackObject>[];
|
||||
|
||||
return tracks;
|
||||
return tracks.union();
|
||||
}
|
||||
|
||||
void selectTrack(SpotubeTrackObject track) {
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:spotube/components/track_presentation/use_action_callbacks.dart'
|
||||
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||
|
||||
class TrackPresentationTopSection extends HookConsumerWidget {
|
||||
|
||||
@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/track_options/track_options_provider.dart';
|
||||
|
||||
/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
|
||||
/// [track] must be a [SpotubeTrackObject] or [SpotubeLocalTrackObject]
|
||||
class TrackOptions extends HookConsumerWidget {
|
||||
final SpotubeTrackObject track;
|
||||
final bool userPlaylist;
|
||||
@ -26,8 +26,8 @@ class TrackOptions extends HookConsumerWidget {
|
||||
this.icon,
|
||||
this.onTapItem,
|
||||
}) : assert(
|
||||
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
|
||||
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
|
||||
track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
|
||||
"Track must be a SpotubeTrackObject, SpotubeLocalTrackObject",
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -24,12 +25,15 @@ void useEndlessPlayback(WidgetRef ref) {
|
||||
|
||||
final track = playlist.tracks.last;
|
||||
|
||||
final tracks = await (await metadataPlugin)?.track.radio(track.id);
|
||||
final tracks = await metadataPlugin.then(
|
||||
(plugin) async =>
|
||||
plugin?.track.radio(id: track.id, mpscTx: plugin.sender),
|
||||
);
|
||||
|
||||
if (tracks == null || tracks.isEmpty) return;
|
||||
|
||||
await playback.addTracks(
|
||||
tracks.toList()
|
||||
tracks.union()
|
||||
..removeWhere((e) {
|
||||
final playlist = ref.read(audioPlayerProvider);
|
||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||
|
||||
@ -33,23 +33,23 @@ WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WebSocketLoadEventData {
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError;
|
||||
Object? get collection => throw _privateConstructorUsedError;
|
||||
int? get initialIndex => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)
|
||||
playlist,
|
||||
required TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)
|
||||
@ -59,15 +59,15 @@ mixin _$WebSocketLoadEventData {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)?
|
||||
playlist,
|
||||
TResult? Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)?
|
||||
@ -77,15 +77,15 @@ mixin _$WebSocketLoadEventData {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)?
|
||||
playlist,
|
||||
TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)?
|
||||
@ -130,8 +130,8 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> {
|
||||
_$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
int? initialIndex});
|
||||
}
|
||||
@ -178,8 +178,8 @@ abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res>
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex});
|
||||
@ -243,8 +243,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
|
||||
class _$WebSocketLoadEventDataPlaylistImpl
|
||||
extends WebSocketLoadEventDataPlaylist {
|
||||
_$WebSocketLoadEventDataPlaylistImpl(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
required final List<SpotubeTrackObject> tracks,
|
||||
this.collection,
|
||||
this.initialIndex,
|
||||
@ -259,8 +259,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
||||
|
||||
final List<SpotubeTrackObject> _tracks;
|
||||
@override
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> get tracks {
|
||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||
// ignore: implicit_dynamic_type
|
||||
@ -311,15 +311,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)
|
||||
playlist,
|
||||
required TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)
|
||||
@ -332,15 +332,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)?
|
||||
playlist,
|
||||
TResult? Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)?
|
||||
@ -353,15 +353,15 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)?
|
||||
playlist,
|
||||
TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)?
|
||||
@ -415,8 +415,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
||||
|
||||
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
||||
factory WebSocketLoadEventDataPlaylist(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
required final List<SpotubeTrackObject> tracks,
|
||||
final SpotubeSimplePlaylistObject? collection,
|
||||
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
|
||||
@ -426,8 +426,8 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
||||
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
|
||||
|
||||
@override
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> get tracks;
|
||||
@override
|
||||
SpotubeSimplePlaylistObject? get collection;
|
||||
@ -453,8 +453,8 @@ abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res>
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex});
|
||||
@ -516,8 +516,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||
_$WebSocketLoadEventDataAlbumImpl(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
required final List<SpotubeTrackObject> tracks,
|
||||
this.collection,
|
||||
this.initialIndex,
|
||||
@ -532,8 +532,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||
|
||||
final List<SpotubeTrackObject> _tracks;
|
||||
@override
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> get tracks {
|
||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||
// ignore: implicit_dynamic_type
|
||||
@ -583,15 +583,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)
|
||||
playlist,
|
||||
required TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)
|
||||
@ -604,15 +604,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)?
|
||||
playlist,
|
||||
TResult? Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)?
|
||||
@ -625,15 +625,15 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
int? initialIndex)?
|
||||
playlist,
|
||||
TResult Function(
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
int? initialIndex)?
|
||||
@ -687,8 +687,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||
|
||||
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
||||
factory WebSocketLoadEventDataAlbum(
|
||||
{@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
{@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
required final List<SpotubeTrackObject> tracks,
|
||||
final SpotubeSimpleAlbumObject? collection,
|
||||
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
|
||||
@ -698,8 +698,8 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
||||
_$WebSocketLoadEventDataAlbumImpl.fromJson;
|
||||
|
||||
@override
|
||||
@Assert("tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject")
|
||||
@Assert("tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject")
|
||||
List<SpotubeTrackObject> get tracks;
|
||||
@override
|
||||
SpotubeSimpleAlbumObject? get collection;
|
||||
|
||||
@ -6,8 +6,8 @@ class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
||||
|
||||
factory WebSocketLoadEventData.playlist({
|
||||
@Assert(
|
||||
"tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject",
|
||||
"tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject",
|
||||
)
|
||||
required List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimplePlaylistObject? collection,
|
||||
@ -16,8 +16,8 @@ class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
||||
|
||||
factory WebSocketLoadEventData.album({
|
||||
@Assert(
|
||||
"tracks is List<SpotubeFullTrackObject>",
|
||||
"tracks must be a list of SpotubeFullTrackObject",
|
||||
"tracks is List<SpotubeTrackObject>",
|
||||
"tracks must be a list of SpotubeTrackObject",
|
||||
)
|
||||
required List<SpotubeTrackObject> tracks,
|
||||
SpotubeSimpleAlbumObject? collection,
|
||||
|
||||
@ -338,14 +338,14 @@ class WebSocketRemoveTrackEvent extends WebSocketEvent<String> {
|
||||
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
|
||||
}
|
||||
|
||||
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeFullTrackObject> {
|
||||
WebSocketAddTrackEvent(SpotubeFullTrackObject data)
|
||||
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeTrackObject> {
|
||||
WebSocketAddTrackEvent(SpotubeTrackObject data)
|
||||
: super(WsEvent.addTrack, data);
|
||||
|
||||
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
|
||||
: super(
|
||||
WsEvent.addTrack,
|
||||
SpotubeFullTrackObject.fromJson(
|
||||
SpotubeTrackObject.fromJson(
|
||||
json["data"] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -1,33 +1,5 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
@freezed
|
||||
class SpotubeFullArtistObject with _$SpotubeFullArtistObject {
|
||||
factory SpotubeFullArtistObject({
|
||||
required String id,
|
||||
required String name,
|
||||
required String externalUri,
|
||||
@Default([]) List<SpotubeImageObject> images,
|
||||
List<String>? genres,
|
||||
int? followers,
|
||||
}) = _SpotubeFullArtistObject;
|
||||
|
||||
factory SpotubeFullArtistObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeFullArtistObjectFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeSimpleArtistObject with _$SpotubeSimpleArtistObject {
|
||||
factory SpotubeSimpleArtistObject({
|
||||
required String id,
|
||||
required String name,
|
||||
required String externalUri,
|
||||
List<SpotubeImageObject>? images,
|
||||
}) = _SpotubeSimpleArtistObject;
|
||||
|
||||
factory SpotubeSimpleArtistObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeSimpleArtistObjectFromJson(json);
|
||||
}
|
||||
|
||||
extension SpotubeFullArtistObjectAsString on List<SpotubeFullArtistObject> {
|
||||
String asString() {
|
||||
return map((e) => e.name).join(", ");
|
||||
|
||||
@ -7,29 +7,7 @@ enum SpotubeMediaCompressionType {
|
||||
lossless,
|
||||
}
|
||||
|
||||
@Freezed(unionKey: 'type')
|
||||
class SpotubeAudioSourceContainerPreset
|
||||
with _$SpotubeAudioSourceContainerPreset {
|
||||
const SpotubeAudioSourceContainerPreset._();
|
||||
|
||||
@FreezedUnionValue("lossy")
|
||||
factory SpotubeAudioSourceContainerPreset.lossy({
|
||||
required SpotubeMediaCompressionType type,
|
||||
required String name,
|
||||
required List<SpotubeAudioLossyContainerQuality> qualities,
|
||||
}) = SpotubeAudioSourceContainerPresetLossy;
|
||||
|
||||
@FreezedUnionValue("lossless")
|
||||
factory SpotubeAudioSourceContainerPreset.lossless({
|
||||
required SpotubeMediaCompressionType type,
|
||||
required String name,
|
||||
required List<SpotubeAudioLosslessContainerQuality> qualities,
|
||||
}) = SpotubeAudioSourceContainerPresetLossless;
|
||||
|
||||
factory SpotubeAudioSourceContainerPreset.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceContainerPresetFromJson(json);
|
||||
|
||||
extension GetFileExtension on SpotubeAudioSourceContainerPreset {
|
||||
String getFileExtension() {
|
||||
return switch (name) {
|
||||
"mp4" => "m4a",
|
||||
@ -39,72 +17,16 @@ class SpotubeAudioSourceContainerPreset
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioLossyContainerQuality
|
||||
with _$SpotubeAudioLossyContainerQuality {
|
||||
const SpotubeAudioLossyContainerQuality._();
|
||||
|
||||
factory SpotubeAudioLossyContainerQuality({
|
||||
required int bitrate, // bits per second
|
||||
}) = _SpotubeAudioLossyContainerQuality;
|
||||
|
||||
factory SpotubeAudioLossyContainerQuality.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioLossyContainerQualityFromJson(json);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
extension ToStringSpotubeAudioLossyContainerQuality
|
||||
on SpotubeAudioLossyContainerQuality {
|
||||
toFormattedString() {
|
||||
return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps";
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioLosslessContainerQuality
|
||||
with _$SpotubeAudioLosslessContainerQuality {
|
||||
const SpotubeAudioLosslessContainerQuality._();
|
||||
|
||||
factory SpotubeAudioLosslessContainerQuality({
|
||||
required int bitDepth, // bit
|
||||
required int sampleRate, // hz
|
||||
}) = _SpotubeAudioLosslessContainerQuality;
|
||||
|
||||
factory SpotubeAudioLosslessContainerQuality.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioLosslessContainerQualityFromJson(json);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
extension ToStringSpotubeAudioLosslessContainerQuality
|
||||
on SpotubeAudioLosslessContainerQuality {
|
||||
toFormattedString() {
|
||||
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz";
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject {
|
||||
factory SpotubeAudioSourceMatchObject({
|
||||
required String id,
|
||||
required String title,
|
||||
required List<String> artists,
|
||||
required Duration duration,
|
||||
String? thumbnail,
|
||||
required String externalUri,
|
||||
}) = _SpotubeAudioSourceMatchObject;
|
||||
|
||||
factory SpotubeAudioSourceMatchObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceMatchObjectFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject {
|
||||
factory SpotubeAudioSourceStreamObject({
|
||||
required String url,
|
||||
required String container,
|
||||
required SpotubeMediaCompressionType type,
|
||||
String? codec,
|
||||
double? bitrate,
|
||||
int? bitDepth,
|
||||
double? sampleRate,
|
||||
}) = _SpotubeAudioSourceStreamObject;
|
||||
|
||||
factory SpotubeAudioSourceStreamObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceStreamObjectFromJson(json);
|
||||
}
|
||||
|
||||
@ -1,21 +1,79 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
@Freezed(genericArgumentFactories: true)
|
||||
class SpotubeBrowseSectionObject<T> with _$SpotubeBrowseSectionObject<T> {
|
||||
factory SpotubeBrowseSectionObject({
|
||||
required String id,
|
||||
required String title,
|
||||
required String externalUri,
|
||||
required bool browseMore,
|
||||
required List<T> items,
|
||||
}) = _SpotubeBrowseSectionObject<T>;
|
||||
class SpotubeFlattenedBrowseSectionObject<T> {
|
||||
final String id;
|
||||
final String title;
|
||||
final String externalUri;
|
||||
final bool browseMore;
|
||||
final List<T> items;
|
||||
|
||||
factory SpotubeBrowseSectionObject.fromJson(
|
||||
Map<String, Object?> json,
|
||||
T Function(Map<String, dynamic> json) fromJsonT,
|
||||
) =>
|
||||
_$SpotubeBrowseSectionObjectFromJson<T>(
|
||||
json,
|
||||
(json) => fromJsonT(json as Map<String, dynamic>),
|
||||
SpotubeFlattenedBrowseSectionObject({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.browseMore,
|
||||
required this.externalUri,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
static SpotubeFlattenedBrowseSectionObject<T> from<T>(
|
||||
SpotubeBrowseSectionObject browseSection,
|
||||
T Function(SpotubeBrowseSectionResponseObjectItem item) parse,
|
||||
) {
|
||||
return SpotubeFlattenedBrowseSectionObject<T>(
|
||||
browseMore: browseSection.browseMore,
|
||||
id: browseSection.id,
|
||||
title: browseSection.title,
|
||||
externalUri: browseSection.externalUri,
|
||||
items: browseSection.items
|
||||
.map((item) => parse(item))
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
SpotubeFlattenedBrowseSectionObject<T> copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? externalUri,
|
||||
bool? browseMore,
|
||||
List<T>? items,
|
||||
}) {
|
||||
return SpotubeFlattenedBrowseSectionObject<T>(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
externalUri: externalUri ?? this.externalUri,
|
||||
browseMore: browseMore ?? this.browseMore,
|
||||
items: items ?? this.items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension SpotubeBrowseSectionObjectExtension on SpotubeBrowseSectionObject {
|
||||
SpotubeFlattenedBrowseSectionObject<T> flatten<T>() {
|
||||
return SpotubeFlattenedBrowseSectionObject.from<T>(
|
||||
this,
|
||||
(item) => switch (T) {
|
||||
SpotubeSimpleAlbumObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_AlbumSimple).field0
|
||||
as T,
|
||||
SpotubeFullAlbumObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_AlbumFull).field0
|
||||
as T,
|
||||
SpotubeSimpleArtistObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_ArtistSimple).field0
|
||||
as T,
|
||||
SpotubeFullArtistObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_ArtistFull).field0
|
||||
as T,
|
||||
SpotubeTrackObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_Track).field0 as T,
|
||||
SpotubeSimplePlaylistObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_PlaylistSimple).field0
|
||||
as T,
|
||||
SpotubeFullPlaylistObject() =>
|
||||
(item as SpotubeBrowseSectionResponseObjectItem_PlaylistFull).field0
|
||||
as T,
|
||||
_ => throw Exception("Unsupported type: $T"),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,5 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
@freezed
|
||||
class SpotubeImageObject with _$SpotubeImageObject {
|
||||
factory SpotubeImageObject({
|
||||
required String url,
|
||||
int? width,
|
||||
int? height,
|
||||
}) = _SpotubeImageObject;
|
||||
|
||||
factory SpotubeImageObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeImageObjectFromJson(json);
|
||||
}
|
||||
|
||||
enum ImagePlaceholder {
|
||||
albumArt,
|
||||
artist,
|
||||
|
||||
@ -10,23 +10,31 @@ import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
export "package:spotube/src/rust/api/plugin/models/album.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/audio_source.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/artist.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/auth.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/browse.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/core.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/playlist.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/track.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/user.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/image.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/pagination.dart";
|
||||
export "package:spotube/src/rust/api/plugin/models/search.dart";
|
||||
|
||||
part 'metadata.g.dart';
|
||||
part 'metadata.freezed.dart';
|
||||
|
||||
part 'audio_source.dart';
|
||||
part 'album.dart';
|
||||
part 'artist.dart';
|
||||
part 'audio_source.dart';
|
||||
part 'browse.dart';
|
||||
part 'fields.dart';
|
||||
part 'image.dart';
|
||||
part 'pagination.dart';
|
||||
part 'playlist.dart';
|
||||
part 'search.dart';
|
||||
part 'track.dart';
|
||||
part 'user.dart';
|
||||
|
||||
part 'plugin.dart';
|
||||
part 'repository.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,270 +6,6 @@ part of 'metadata.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl
|
||||
_$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl(
|
||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
||||
name: json['name'] as String,
|
||||
qualities: (json['qualities'] as List<dynamic>)
|
||||
.map((e) => SpotubeAudioLossyContainerQuality.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLossyImplToJson(
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
const _$SpotubeMediaCompressionTypeEnumMap = {
|
||||
SpotubeMediaCompressionType.lossy: 'lossy',
|
||||
SpotubeMediaCompressionType.lossless: 'lossless',
|
||||
};
|
||||
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl
|
||||
_$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl(
|
||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
||||
name: json['name'] as String,
|
||||
qualities: (json['qualities'] as List<dynamic>)
|
||||
.map((e) => SpotubeAudioLosslessContainerQuality.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLosslessImplToJson(
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_$SpotubeAudioLossyContainerQualityImpl
|
||||
_$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) =>
|
||||
_$SpotubeAudioLossyContainerQualityImpl(
|
||||
bitrate: (json['bitrate'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson(
|
||||
_$SpotubeAudioLossyContainerQualityImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'bitrate': instance.bitrate,
|
||||
};
|
||||
|
||||
_$SpotubeAudioLosslessContainerQualityImpl
|
||||
_$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) =>
|
||||
_$SpotubeAudioLosslessContainerQualityImpl(
|
||||
bitDepth: (json['bitDepth'] as num).toInt(),
|
||||
sampleRate: (json['sampleRate'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson(
|
||||
_$SpotubeAudioLosslessContainerQualityImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'bitDepth': instance.bitDepth,
|
||||
'sampleRate': instance.sampleRate,
|
||||
};
|
||||
|
||||
_$SpotubeAudioSourceMatchObjectImpl
|
||||
_$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceMatchObjectImpl(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
artists: (json['artists'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
duration: Duration(microseconds: (json['duration'] as num).toInt()),
|
||||
thumbnail: json['thumbnail'] as String?,
|
||||
externalUri: json['externalUri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceMatchObjectImplToJson(
|
||||
_$SpotubeAudioSourceMatchObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'artists': instance.artists,
|
||||
'duration': instance.duration.inMicroseconds,
|
||||
'thumbnail': instance.thumbnail,
|
||||
'externalUri': instance.externalUri,
|
||||
};
|
||||
|
||||
_$SpotubeAudioSourceStreamObjectImpl
|
||||
_$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) =>
|
||||
_$SpotubeAudioSourceStreamObjectImpl(
|
||||
url: json['url'] as String,
|
||||
container: json['container'] as String,
|
||||
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
|
||||
codec: json['codec'] as String?,
|
||||
bitrate: (json['bitrate'] as num?)?.toDouble(),
|
||||
bitDepth: (json['bitDepth'] as num?)?.toInt(),
|
||||
sampleRate: (json['sampleRate'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeAudioSourceStreamObjectImplToJson(
|
||||
_$SpotubeAudioSourceStreamObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'url': instance.url,
|
||||
'container': instance.container,
|
||||
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
|
||||
'codec': instance.codec,
|
||||
'bitrate': instance.bitrate,
|
||||
'bitDepth': instance.bitDepth,
|
||||
'sampleRate': instance.sampleRate,
|
||||
};
|
||||
|
||||
_$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) =>
|
||||
_$SpotubeFullAlbumObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
artists: (json['artists'] as List<dynamic>)
|
||||
.map((e) => SpotubeSimpleArtistObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
releaseDate: json['releaseDate'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
totalTracks: (json['totalTracks'] as num).toInt(),
|
||||
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
|
||||
recordLabel: json['recordLabel'] as String?,
|
||||
genres:
|
||||
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeFullAlbumObjectImplToJson(
|
||||
_$SpotubeFullAlbumObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
'releaseDate': instance.releaseDate,
|
||||
'externalUri': instance.externalUri,
|
||||
'totalTracks': instance.totalTracks,
|
||||
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
|
||||
'recordLabel': instance.recordLabel,
|
||||
'genres': instance.genres,
|
||||
};
|
||||
|
||||
const _$SpotubeAlbumTypeEnumMap = {
|
||||
SpotubeAlbumType.album: 'album',
|
||||
SpotubeAlbumType.single: 'single',
|
||||
SpotubeAlbumType.compilation: 'compilation',
|
||||
};
|
||||
|
||||
_$SpotubeSimpleAlbumObjectImpl _$$SpotubeSimpleAlbumObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeSimpleAlbumObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
artists: (json['artists'] as List<dynamic>)
|
||||
.map((e) => SpotubeSimpleArtistObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
albumType: $enumDecode(_$SpotubeAlbumTypeEnumMap, json['albumType']),
|
||||
releaseDate: json['releaseDate'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeSimpleAlbumObjectImplToJson(
|
||||
_$SpotubeSimpleAlbumObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'externalUri': instance.externalUri,
|
||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
'albumType': _$SpotubeAlbumTypeEnumMap[instance.albumType]!,
|
||||
'releaseDate': instance.releaseDate,
|
||||
};
|
||||
|
||||
_$SpotubeFullArtistObjectImpl _$$SpotubeFullArtistObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeFullArtistObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
genres:
|
||||
(json['genres'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
followers: (json['followers'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeFullArtistObjectImplToJson(
|
||||
_$SpotubeFullArtistObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'externalUri': instance.externalUri,
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
'genres': instance.genres,
|
||||
'followers': instance.followers,
|
||||
};
|
||||
|
||||
_$SpotubeSimpleArtistObjectImpl _$$SpotubeSimpleArtistObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeSimpleArtistObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
SpotubeImageObject.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeSimpleArtistObjectImplToJson(
|
||||
_$SpotubeSimpleArtistObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'externalUri': instance.externalUri,
|
||||
'images': instance.images?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_$SpotubeBrowseSectionObjectImpl<T>
|
||||
_$$SpotubeBrowseSectionObjectImplFromJson<T>(
|
||||
Map json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
_$SpotubeBrowseSectionObjectImpl<T>(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
browseMore: json['browseMore'] as bool,
|
||||
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeBrowseSectionObjectImplToJson<T>(
|
||||
_$SpotubeBrowseSectionObjectImpl<T> instance,
|
||||
Object? Function(T value) toJsonT,
|
||||
) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'externalUri': instance.externalUri,
|
||||
'browseMore': instance.browseMore,
|
||||
'items': instance.items.map(toJsonT).toList(),
|
||||
};
|
||||
|
||||
_$MetadataFormFieldInputObjectImpl _$$MetadataFormFieldInputObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$MetadataFormFieldInputObjectImpl(
|
||||
@ -316,286 +52,6 @@ Map<String, dynamic> _$$MetadataFormFieldTextObjectImplToJson(
|
||||
'text': instance.text,
|
||||
};
|
||||
|
||||
_$SpotubeImageObjectImpl _$$SpotubeImageObjectImplFromJson(Map json) =>
|
||||
_$SpotubeImageObjectImpl(
|
||||
url: json['url'] as String,
|
||||
width: (json['width'] as num?)?.toInt(),
|
||||
height: (json['height'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeImageObjectImplToJson(
|
||||
_$SpotubeImageObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'url': instance.url,
|
||||
'width': instance.width,
|
||||
'height': instance.height,
|
||||
};
|
||||
|
||||
_$SpotubePaginationResponseObjectImpl<T>
|
||||
_$$SpotubePaginationResponseObjectImplFromJson<T>(
|
||||
Map json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
_$SpotubePaginationResponseObjectImpl<T>(
|
||||
limit: (json['limit'] as num).toInt(),
|
||||
nextOffset: (json['nextOffset'] as num?)?.toInt(),
|
||||
total: (json['total'] as num).toInt(),
|
||||
hasMore: json['hasMore'] as bool,
|
||||
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubePaginationResponseObjectImplToJson<T>(
|
||||
_$SpotubePaginationResponseObjectImpl<T> instance,
|
||||
Object? Function(T value) toJsonT,
|
||||
) =>
|
||||
<String, dynamic>{
|
||||
'limit': instance.limit,
|
||||
'nextOffset': instance.nextOffset,
|
||||
'total': instance.total,
|
||||
'hasMore': instance.hasMore,
|
||||
'items': instance.items.map(toJsonT).toList(),
|
||||
};
|
||||
|
||||
_$SpotubeFullPlaylistObjectImpl _$$SpotubeFullPlaylistObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeFullPlaylistObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
owner: SpotubeUserObject.fromJson(
|
||||
Map<String, dynamic>.from(json['owner'] as Map)),
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
collaborators: (json['collaborators'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeUserObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
collaborative: json['collaborative'] as bool? ?? false,
|
||||
public: json['public'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeFullPlaylistObjectImplToJson(
|
||||
_$SpotubeFullPlaylistObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'externalUri': instance.externalUri,
|
||||
'owner': instance.owner.toJson(),
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
'collaborators': instance.collaborators.map((e) => e.toJson()).toList(),
|
||||
'collaborative': instance.collaborative,
|
||||
'public': instance.public,
|
||||
};
|
||||
|
||||
_$SpotubeSimplePlaylistObjectImpl _$$SpotubeSimplePlaylistObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeSimplePlaylistObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
owner: SpotubeUserObject.fromJson(
|
||||
Map<String, dynamic>.from(json['owner'] as Map)),
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeSimplePlaylistObjectImplToJson(
|
||||
_$SpotubeSimplePlaylistObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'externalUri': instance.externalUri,
|
||||
'owner': instance.owner.toJson(),
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_$SpotubeSearchResponseObjectImpl _$$SpotubeSearchResponseObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeSearchResponseObjectImpl(
|
||||
albums: (json['albums'] as List<dynamic>)
|
||||
.map((e) => SpotubeSimpleAlbumObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
artists: (json['artists'] as List<dynamic>)
|
||||
.map((e) => SpotubeFullArtistObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
playlists: (json['playlists'] as List<dynamic>)
|
||||
.map((e) => SpotubeSimplePlaylistObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
tracks: (json['tracks'] as List<dynamic>)
|
||||
.map((e) => SpotubeFullTrackObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeSearchResponseObjectImplToJson(
|
||||
_$SpotubeSearchResponseObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'albums': instance.albums.map((e) => e.toJson()).toList(),
|
||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||
'playlists': instance.playlists.map((e) => e.toJson()).toList(),
|
||||
'tracks': instance.tracks.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_$SpotubeLocalTrackObjectImpl _$$SpotubeLocalTrackObjectImplFromJson(
|
||||
Map json) =>
|
||||
_$SpotubeLocalTrackObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
artists: (json['artists'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeSimpleArtistObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
album: SpotubeSimpleAlbumObject.fromJson(
|
||||
Map<String, dynamic>.from(json['album'] as Map)),
|
||||
durationMs: (json['durationMs'] as num).toInt(),
|
||||
path: json['path'] as String,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeLocalTrackObjectImplToJson(
|
||||
_$SpotubeLocalTrackObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'externalUri': instance.externalUri,
|
||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||
'album': instance.album.toJson(),
|
||||
'durationMs': instance.durationMs,
|
||||
'path': instance.path,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$SpotubeFullTrackObjectImpl _$$SpotubeFullTrackObjectImplFromJson(Map json) =>
|
||||
_$SpotubeFullTrackObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
externalUri: json['externalUri'] as String,
|
||||
artists: (json['artists'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeSimpleArtistObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
album: SpotubeSimpleAlbumObject.fromJson(
|
||||
Map<String, dynamic>.from(json['album'] as Map)),
|
||||
durationMs: (json['durationMs'] as num).toInt(),
|
||||
isrc: json['isrc'] as String,
|
||||
explicit: json['explicit'] as bool,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeFullTrackObjectImplToJson(
|
||||
_$SpotubeFullTrackObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'externalUri': instance.externalUri,
|
||||
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||
'album': instance.album.toJson(),
|
||||
'durationMs': instance.durationMs,
|
||||
'isrc': instance.isrc,
|
||||
'explicit': instance.explicit,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$SpotubeUserObjectImpl _$$SpotubeUserObjectImplFromJson(Map json) =>
|
||||
_$SpotubeUserObjectImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
images: (json['images'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeImageObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
externalUri: json['externalUri'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
|
||||
_$SpotubeUserObjectImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||
'externalUri': instance.externalUri,
|
||||
};
|
||||
|
||||
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
||||
_$PluginConfigurationImpl(
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
version: json['version'] as String,
|
||||
author: json['author'] as String,
|
||||
entryPoint: json['entryPoint'] as String,
|
||||
pluginApiVersion: json['pluginApiVersion'] as String,
|
||||
apis: (json['apis'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$PluginApisEnumMap, e))
|
||||
.toList() ??
|
||||
const [],
|
||||
abilities: (json['abilities'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$PluginAbilitiesEnumMap, e))
|
||||
.toList() ??
|
||||
const [],
|
||||
repository: json['repository'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$PluginConfigurationImplToJson(
|
||||
_$PluginConfigurationImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'version': instance.version,
|
||||
'author': instance.author,
|
||||
'entryPoint': instance.entryPoint,
|
||||
'pluginApiVersion': instance.pluginApiVersion,
|
||||
'apis': instance.apis.map((e) => _$PluginApisEnumMap[e]!).toList(),
|
||||
'abilities':
|
||||
instance.abilities.map((e) => _$PluginAbilitiesEnumMap[e]!).toList(),
|
||||
'repository': instance.repository,
|
||||
};
|
||||
|
||||
const _$PluginApisEnumMap = {
|
||||
PluginApis.webview: 'webview',
|
||||
PluginApis.localstorage: 'localstorage',
|
||||
PluginApis.timezone: 'timezone',
|
||||
};
|
||||
|
||||
const _$PluginAbilitiesEnumMap = {
|
||||
PluginAbilities.authentication: 'authentication',
|
||||
PluginAbilities.scrobbling: 'scrobbling',
|
||||
PluginAbilities.metadata: 'metadata',
|
||||
PluginAbilities.audioSource: 'audio-source',
|
||||
};
|
||||
|
||||
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
|
||||
_$PluginUpdateAvailableImpl(
|
||||
downloadUrl: json['downloadUrl'] as String,
|
||||
version: json['version'] as String,
|
||||
changelog: json['changelog'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$PluginUpdateAvailableImplToJson(
|
||||
_$PluginUpdateAvailableImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'downloadUrl': instance.downloadUrl,
|
||||
'version': instance.version,
|
||||
'changelog': instance.changelog,
|
||||
};
|
||||
|
||||
_$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
|
||||
Map json) =>
|
||||
_$MetadataPluginRepositoryImpl(
|
||||
|
||||
@ -1,22 +1,78 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
@Freezed(genericArgumentFactories: true)
|
||||
class SpotubePaginationResponseObject<T>
|
||||
with _$SpotubePaginationResponseObject<T> {
|
||||
factory SpotubePaginationResponseObject({
|
||||
required int limit,
|
||||
required int? nextOffset,
|
||||
required int total,
|
||||
required bool hasMore,
|
||||
required List<T> items,
|
||||
}) = _SpotubePaginationResponseObject<T>;
|
||||
class SpotubeFlattenedPaginationObject<T> {
|
||||
final int limit;
|
||||
final int? nextOffset;
|
||||
final int total;
|
||||
final bool hasMore;
|
||||
final List<T> items;
|
||||
|
||||
factory SpotubePaginationResponseObject.fromJson(
|
||||
Map<String, Object?> json,
|
||||
T Function(Map<String, dynamic> json) fromJsonT,
|
||||
) =>
|
||||
_$SpotubePaginationResponseObjectFromJson<T>(
|
||||
json,
|
||||
(json) => fromJsonT(json as Map<String, dynamic>),
|
||||
SpotubeFlattenedPaginationObject({
|
||||
required this.limit,
|
||||
required this.nextOffset,
|
||||
required this.total,
|
||||
required this.hasMore,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
static SpotubeFlattenedPaginationObject<T> from<T>(
|
||||
SpotubePaginationResponseObject response,
|
||||
T Function(SpotubePaginationResponseObjectItem item) parse,
|
||||
) {
|
||||
return SpotubeFlattenedPaginationObject<T>(
|
||||
limit: response.limit,
|
||||
nextOffset: response.nextOffset,
|
||||
total: response.total,
|
||||
hasMore: response.hasMore,
|
||||
items: response.items.map((item) => parse(item)).toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
SpotubeFlattenedPaginationObject<T> copyWith({
|
||||
int? limit,
|
||||
int? nextOffset,
|
||||
int? total,
|
||||
bool? hasMore,
|
||||
List<T>? items,
|
||||
}) {
|
||||
return SpotubeFlattenedPaginationObject<T>(
|
||||
limit: limit ?? this.limit,
|
||||
nextOffset: nextOffset ?? this.nextOffset,
|
||||
total: total ?? this.total,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
items: items ?? this.items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension SpotubePaginationResponseObjectExtension
|
||||
on SpotubePaginationResponseObject {
|
||||
SpotubeFlattenedPaginationObject<T> flatten<T>() {
|
||||
return SpotubeFlattenedPaginationObject.from<T>(
|
||||
this,
|
||||
(item) => switch (T) {
|
||||
SpotubeSimpleAlbumObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_AlbumSimple).field0 as T,
|
||||
SpotubeFullAlbumObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_AlbumFull).field0 as T,
|
||||
SpotubeSimpleArtistObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_ArtistSimple).field0
|
||||
as T,
|
||||
SpotubeFullArtistObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_ArtistFull).field0 as T,
|
||||
SpotubeTrackObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_Track).field0 as T,
|
||||
SpotubeSimplePlaylistObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_PlaylistSimple).field0
|
||||
as T,
|
||||
SpotubeFullPlaylistObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_PlaylistFull).field0
|
||||
as T,
|
||||
SpotubeBrowseSectionObject() =>
|
||||
(item as SpotubePaginationResponseObjectItem_BrowseSection).field0
|
||||
as T,
|
||||
_ => throw Exception("Unsupported type: $T"),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -1,94 +1,37 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
@freezed
|
||||
class SpotubeTrackObject with _$SpotubeTrackObject {
|
||||
factory SpotubeTrackObject.local({
|
||||
required String id,
|
||||
required String name,
|
||||
required String externalUri,
|
||||
@Default([]) List<SpotubeSimpleArtistObject> artists,
|
||||
required SpotubeSimpleAlbumObject album,
|
||||
required int durationMs,
|
||||
required String path,
|
||||
}) = SpotubeLocalTrackObject;
|
||||
|
||||
factory SpotubeTrackObject.full({
|
||||
required String id,
|
||||
required String name,
|
||||
required String externalUri,
|
||||
@Default([]) List<SpotubeSimpleArtistObject> artists,
|
||||
required SpotubeSimpleAlbumObject album,
|
||||
required int durationMs,
|
||||
required String isrc,
|
||||
required bool explicit,
|
||||
}) = SpotubeFullTrackObject;
|
||||
|
||||
factory SpotubeTrackObject.localTrackFromFile(
|
||||
File file, {
|
||||
Metadata? metadata,
|
||||
String? art,
|
||||
}) {
|
||||
return SpotubeLocalTrackObject(
|
||||
id: file.absolute.path,
|
||||
name: metadata?.title ?? basenameWithoutExtension(file.path),
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
artists: metadata?.artist?.split(",").map((a) {
|
||||
return SpotubeSimpleArtistObject(
|
||||
id: a.trim(),
|
||||
name: a.trim(),
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
);
|
||||
}).toList() ??
|
||||
[
|
||||
SpotubeSimpleArtistObject(
|
||||
id: "unknown",
|
||||
name: "Unknown Artist",
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
),
|
||||
],
|
||||
album: SpotubeSimpleAlbumObject(
|
||||
albumType: SpotubeAlbumType.album,
|
||||
id: metadata?.album ?? "unknown",
|
||||
name: metadata?.album ?? "Unknown Album",
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
artists: [
|
||||
SpotubeSimpleArtistObject(
|
||||
id: metadata?.albumArtist ?? "unknown",
|
||||
name: metadata?.albumArtist ?? "Unknown Artist",
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
),
|
||||
],
|
||||
releaseDate:
|
||||
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
|
||||
images: [
|
||||
if (art != null)
|
||||
SpotubeImageObject(
|
||||
url: art,
|
||||
width: 300,
|
||||
height: 300,
|
||||
),
|
||||
],
|
||||
),
|
||||
durationMs: metadata?.durationMs?.toInt() ?? 0,
|
||||
path: file.path,
|
||||
);
|
||||
}
|
||||
|
||||
factory SpotubeTrackObject.fromJson(Map<String, dynamic> json) =>
|
||||
_$SpotubeTrackObjectFromJson(
|
||||
json.containsKey("path")
|
||||
? {...json, "runtimeType": "local"}
|
||||
: {...json, "runtimeType": "full"},
|
||||
);
|
||||
}
|
||||
|
||||
extension AsMediaListSpotubeTrackObject on Iterable<SpotubeTrackObject> {
|
||||
List<SpotubeMedia> asMediaList() {
|
||||
return map((track) => SpotubeMedia(track)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
|
||||
extension FullAsPartialSpotubeTrackObject on Iterable<SpotubeFullTrackObject>? {
|
||||
List<SpotubeTrackObject>? union() {
|
||||
return this?.map((track) => SpotubeTrackObject.full(track)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension FullAsSpotubeTrackObject on Iterable<SpotubeFullTrackObject> {
|
||||
List<SpotubeTrackObject> union() {
|
||||
return map((track) => SpotubeTrackObject.full(track)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalAsPartialSpotubeTrackObject
|
||||
on Iterable<SpotubeLocalTrackObject>? {
|
||||
List<SpotubeTrackObject>? union() {
|
||||
return this?.map((track) => SpotubeTrackObject.local(track)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalAsSpotubeTrackObject on Iterable<SpotubeLocalTrackObject> {
|
||||
List<SpotubeTrackObject> union() {
|
||||
return map((track) => SpotubeTrackObject.local(track)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension ToMetadataSpotubeFullTrackObject on SpotubeTrackObject {
|
||||
Metadata toMetadata({
|
||||
required int fileLength,
|
||||
Uint8List? imageBytes,
|
||||
@ -117,3 +60,91 @@ extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension CommonTrackProperties on SpotubeTrackObject {
|
||||
String get id => when(
|
||||
full: (track) => track.id,
|
||||
local: (track) => track.id,
|
||||
);
|
||||
|
||||
String get name => when(
|
||||
full: (track) => track.name,
|
||||
local: (track) => track.name,
|
||||
);
|
||||
|
||||
String get externalUri => when(
|
||||
full: (track) => track.externalUri,
|
||||
local: (track) => track.externalUri,
|
||||
);
|
||||
|
||||
int get durationMs => when(
|
||||
full: (track) => track.durationMs,
|
||||
local: (track) => track.durationMs,
|
||||
);
|
||||
|
||||
SpotubeSimpleAlbumObject get album => when(
|
||||
full: (track) => track.album,
|
||||
local: (track) => track.album,
|
||||
);
|
||||
List<SpotubeSimpleArtistObject> get artists => when(
|
||||
full: (track) => track.artists,
|
||||
local: (track) => track.artists,
|
||||
);
|
||||
}
|
||||
|
||||
SpotubeLocalTrackObject localTrackFromFile(
|
||||
File file, {
|
||||
Metadata? metadata,
|
||||
String? art,
|
||||
}) {
|
||||
return SpotubeLocalTrackObject(
|
||||
typeName: "track_local",
|
||||
id: file.absolute.path,
|
||||
name: metadata?.title ?? basenameWithoutExtension(file.path),
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
artists: metadata?.artist?.split(",").map((a) {
|
||||
return SpotubeSimpleArtistObject(
|
||||
typeName: "artist_simple",
|
||||
id: a.trim(),
|
||||
name: a.trim(),
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
);
|
||||
}).toList() ??
|
||||
[
|
||||
SpotubeSimpleArtistObject(
|
||||
typeName: "artist_simple",
|
||||
id: "unknown",
|
||||
name: "Unknown Artist",
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
),
|
||||
],
|
||||
album: SpotubeSimpleAlbumObject(
|
||||
typeName: "album_simple",
|
||||
albumType: SpotubeAlbumType.album,
|
||||
id: metadata?.album ?? "unknown",
|
||||
name: metadata?.album ?? "Unknown Album",
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
artists: [
|
||||
SpotubeSimpleArtistObject(
|
||||
typeName: "artist_simple",
|
||||
id: metadata?.albumArtist ?? "unknown",
|
||||
name: metadata?.albumArtist ?? "Unknown Artist",
|
||||
externalUri: "file://${file.absolute.path}",
|
||||
),
|
||||
],
|
||||
releaseDate:
|
||||
metadata?.year != null ? "${metadata!.year}-01-01" : "1970-01-01",
|
||||
images: [
|
||||
if (art != null)
|
||||
SpotubeImageObject(
|
||||
typeName: "image",
|
||||
url: art,
|
||||
width: 300,
|
||||
height: 300,
|
||||
),
|
||||
],
|
||||
),
|
||||
durationMs: metadata?.durationMs?.toInt() ?? 0,
|
||||
path: file.path,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -88,12 +88,12 @@ class AlbumCard extends HookConsumerWidget {
|
||||
final remotePlayback = ref.read(connectProvider.notifier);
|
||||
await remotePlayback.load(
|
||||
WebSocketLoadEventData.album(
|
||||
tracks: fetchedTracks,
|
||||
tracks: fetchedTracks.union(),
|
||||
collection: album,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||
await playlistNotifier.load(fetchedTracks.union(), autoPlay: true);
|
||||
playlistNotifier.addCollection(album.id);
|
||||
historyNotifier.addAlbums([album]);
|
||||
}
|
||||
@ -123,7 +123,9 @@ class AlbumCard extends HookConsumerWidget {
|
||||
final fetchedTracks = await fetchAllTrack();
|
||||
|
||||
if (fetchedTracks.isEmpty) return;
|
||||
playlistNotifier.addTracks(fetchedTracks);
|
||||
playlistNotifier.addTracks(
|
||||
fetchedTracks.union(),
|
||||
);
|
||||
playlistNotifier.addCollection(album.id);
|
||||
historyNotifier.addAlbums([album]);
|
||||
if (context.mounted) {
|
||||
|
||||
@ -14,8 +14,8 @@ import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final validAbilities = {
|
||||
PluginAbilities.metadata: ("Metadata", SpotubeIcons.album),
|
||||
PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music),
|
||||
PluginAbility.metadata: ("Metadata", SpotubeIcons.album),
|
||||
PluginAbility.audioSource: ("Audio Source", SpotubeIcons.music),
|
||||
};
|
||||
|
||||
class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
@ -44,9 +44,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||
|
||||
final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
|
||||
plugin.abilities.contains(PluginAbilities.authentication);
|
||||
plugin.abilities.contains(PluginAbility.authentication);
|
||||
final supportsScrobbling = isDefaultMetadata &&
|
||||
plugin.abilities.contains(PluginAbilities.scrobbling);
|
||||
plugin.abilities.contains(PluginAbility.scrobbling);
|
||||
|
||||
final isMetadataAuthenticatedSnapshot =
|
||||
ref.watch(metadataPluginAuthenticatedProvider);
|
||||
@ -253,7 +253,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (plugin.abilities.contains(PluginAbilities.metadata))
|
||||
if (plugin.abilities.contains(PluginAbility.metadata))
|
||||
Button.secondary(
|
||||
enabled: !isDefaultMetadata,
|
||||
onPressed: () async {
|
||||
@ -265,7 +265,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
: context.l10n.set_default_metadata_source,
|
||||
),
|
||||
),
|
||||
if (plugin.abilities.contains(PluginAbilities.audioSource))
|
||||
if (plugin.abilities.contains(PluginAbility.audioSource))
|
||||
Button.secondary(
|
||||
enabled: !isDefaultAudioSource,
|
||||
onPressed: () async {
|
||||
@ -378,8 +378,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
!isAuthenticated)
|
||||
Button.primary(
|
||||
onPressed: () async {
|
||||
await pluginSnapshot?.asData?.value?.auth
|
||||
.authenticate();
|
||||
if ((pluginSnapshot?.hasValue ?? false) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginSnapshot!.value!.auth.authenticate(
|
||||
mpscTx: pluginSnapshot.value!.sender,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.login),
|
||||
child: Text(context.l10n.login),
|
||||
@ -389,7 +394,13 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
isAuthenticated)
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
await pluginSnapshot?.asData?.value?.auth.logout();
|
||||
if ((pluginSnapshot?.hasValue ?? false) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginSnapshot!.value!.auth.logout(
|
||||
mpscTx: pluginSnapshot.value!.sender,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.logout),
|
||||
child: Text(context.l10n.logout),
|
||||
|
||||
@ -123,7 +123,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TrackDetailsDialog(
|
||||
track: currentActiveTrack
|
||||
track: currentActiveTrack?.field0
|
||||
as SpotubeFullTrackObject,
|
||||
);
|
||||
});
|
||||
@ -180,7 +180,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
),
|
||||
if (isLocalTrack)
|
||||
Text(
|
||||
currentActiveTrack.artists.asString(),
|
||||
currentActiveTrack?.artists.asString() ?? "",
|
||||
style: theme.typography.normal
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
)
|
||||
|
||||
@ -38,11 +38,12 @@ class PlayerActions extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final isLocalTrack = playlist.activeTrack is SpotubeLocalTrackObject;
|
||||
final isLocalTrack =
|
||||
playlist.activeTrack?.field0 is SpotubeLocalTrackObject;
|
||||
ref.watch(downloadManagerProvider);
|
||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||
final isInQueue = useMemoized(() {
|
||||
if (playlist.activeTrack is! SpotubeFullTrackObject) return false;
|
||||
if (playlist.activeTrack is! SpotubeTrackObject) return false;
|
||||
final downloadTask =
|
||||
downloader.getTaskByTrackId(playlist.activeTrack!.id);
|
||||
return const [
|
||||
@ -172,16 +173,19 @@ class PlayerActions extends HookConsumerWidget {
|
||||
icon: Icon(
|
||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||
),
|
||||
onPressed: playlist.activeTrack != null
|
||||
onPressed: playlist.activeTrack != null && !isLocalTrack
|
||||
? () => downloader.addToQueue(
|
||||
playlist.activeTrack! as SpotubeFullTrackObject)
|
||||
playlist.activeTrack?.field0
|
||||
as SpotubeFullTrackObject,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (playlist.activeTrack != null &&
|
||||
!isLocalTrack &&
|
||||
authenticated.asData?.value == true)
|
||||
TrackHeartButton(track: playlist.activeTrack!),
|
||||
TrackHeartButton(
|
||||
track: playlist.activeTrack?.field0 as SpotubeFullTrackObject),
|
||||
AdaptivePopSheetList<Duration>(
|
||||
tooltip: context.l10n.sleep_timer,
|
||||
offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),
|
||||
|
||||
@ -26,14 +26,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
final activeTrack =
|
||||
ref.watch(audioPlayerProvider.select((e) => e.activeTrack));
|
||||
|
||||
if (activeTrack == null || activeTrack is! SpotubeFullTrackObject) {
|
||||
if (activeTrack is! SpotubeTrackObject_Full) {
|
||||
return const SafeArea(child: NotFound());
|
||||
}
|
||||
|
||||
return HookBuilder(builder: (context) {
|
||||
final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack));
|
||||
final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack.field0));
|
||||
final sourcedTrackNotifier =
|
||||
ref.watch(sourcedTrackProvider(activeTrack).notifier);
|
||||
ref.watch(sourcedTrackProvider(activeTrack.field0).notifier);
|
||||
|
||||
final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
|
||||
() => !sourcedTrack.isLoading
|
||||
@ -112,8 +112,10 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
width: 60,
|
||||
)
|
||||
: null,
|
||||
trailing:
|
||||
Text(sourceInfo.duration.toHumanReadableString()),
|
||||
trailing: Text(
|
||||
Duration(milliseconds: sourceInfo.duration)
|
||||
.toHumanReadableString(),
|
||||
),
|
||||
subtitle: Text(
|
||||
sourceInfo.artists.join(", "),
|
||||
maxLines: 1,
|
||||
|
||||
@ -98,19 +98,23 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
final allTracks = await fetchAllTracks();
|
||||
await remotePlayback.load(
|
||||
WebSocketLoadEventData.playlist(
|
||||
tracks: allTracks,
|
||||
tracks: allTracks.union(),
|
||||
collection: playlist,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
|
||||
await playlistNotifier.load(
|
||||
fetchedInitialTracks.union(),
|
||||
autoPlay: true,
|
||||
);
|
||||
playlistNotifier.addCollection(playlist.id);
|
||||
historyNotifier.addPlaylists([playlist]);
|
||||
|
||||
final allTracks = await fetchAllTracks();
|
||||
|
||||
await playlistNotifier
|
||||
.addTracks(allTracks.sublist(fetchedInitialTracks.length));
|
||||
await playlistNotifier.addTracks(
|
||||
allTracks.sublist(fetchedInitialTracks.length).union(),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
@ -142,7 +146,9 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
|
||||
if (fetchedInitialTracks.isEmpty) return;
|
||||
|
||||
playlistNotifier.addTracks(fetchedInitialTracks);
|
||||
playlistNotifier.addTracks(
|
||||
fetchedInitialTracks.union(),
|
||||
);
|
||||
playlistNotifier.addCollection(playlist.id);
|
||||
historyNotifier.addPlaylists([playlist]);
|
||||
if (context.mounted) {
|
||||
|
||||
@ -7,10 +7,12 @@ import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/connect/connect.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/pages/search/search.dart';
|
||||
import 'package:spotube/provider/connect/connect.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||
import 'package:spotube/src/rust/api/plugin/models/track.dart';
|
||||
|
||||
class SearchTracksSection extends HookConsumerWidget {
|
||||
const SearchTracksSection({
|
||||
@ -41,7 +43,8 @@ class SearchTracksSection extends HookConsumerWidget {
|
||||
if (search.isLoading)
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
...tracks.mapIndexed((i, track) {
|
||||
...tracks.mapIndexed((i, mehTrack) {
|
||||
final track = SpotubeTrackObject.full(mehTrack);
|
||||
return TrackTile(
|
||||
index: i,
|
||||
track: track,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart' as material;
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||
@ -31,6 +32,10 @@ class AlbumPage extends HookConsumerWidget {
|
||||
ref.watch(metadataPluginSavedAlbumsProvider.notifier);
|
||||
final isSavedAlbum =
|
||||
ref.watch(metadataPluginIsSavedAlbumProvider(album.id));
|
||||
final tracksUnion = useMemoized(
|
||||
() => tracks.asData?.value.items.union() ?? [],
|
||||
[tracks.asData?.value.items],
|
||||
);
|
||||
|
||||
return material.RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
@ -47,7 +52,7 @@ class AlbumPage extends HookConsumerWidget {
|
||||
title: album.name,
|
||||
description:
|
||||
"${context.l10n.released} • ${album.releaseDate} • ${album.artists.first.name}",
|
||||
tracks: tracks.asData?.value.items ?? [],
|
||||
tracks: tracksUnion,
|
||||
error: tracks.error,
|
||||
pagination: PaginationProps(
|
||||
hasNextPage: tracks.asData?.value.hasMore ?? false,
|
||||
@ -56,7 +61,9 @@ class AlbumPage extends HookConsumerWidget {
|
||||
await tracksNotifier.fetchMore();
|
||||
},
|
||||
onFetchAll: () async {
|
||||
return tracksNotifier.fetchAll();
|
||||
final res = await tracksNotifier.fetchAll();
|
||||
|
||||
return res.union();
|
||||
},
|
||||
onRefresh: () async {
|
||||
ref.invalidate(metadataPluginAlbumTracksProvider(album.id));
|
||||
|
||||
@ -44,7 +44,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
||||
List.generate(10, (index) => FakeData.track);
|
||||
|
||||
void playPlaylist(
|
||||
List<SpotubeFullTrackObject> tracks, {
|
||||
List<SpotubeTrackObject> tracks, {
|
||||
SpotubeTrackObject? currentTrack,
|
||||
}) async {
|
||||
isLoading.value = true;
|
||||
|
||||
@ -28,7 +28,7 @@ class HomeBrowseSectionItemsPage extends HookConsumerWidget {
|
||||
static const name = "home_browse_section_items";
|
||||
|
||||
final String sectionId;
|
||||
final SpotubeBrowseSectionObject<Object> section;
|
||||
final SpotubeBrowseSectionObject section;
|
||||
const HomeBrowseSectionItemsPage({
|
||||
super.key,
|
||||
@PathParam("sectionId") required this.sectionId,
|
||||
|
||||
@ -55,17 +55,17 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
final playlist = ref.read(audioPlayerProvider);
|
||||
final playback = ref.read(audioPlayerProvider.notifier);
|
||||
currentTrack ??= tracks.first;
|
||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
|
||||
if (!isPlaylistPlaying) {
|
||||
var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id);
|
||||
await playback.load(
|
||||
tracks,
|
||||
tracks.union(),
|
||||
initialIndex: indexWhere,
|
||||
autoPlay: true,
|
||||
);
|
||||
} else if (isPlaylistPlaying &&
|
||||
currentTrack.id != playlist.activeTrack?.id) {
|
||||
await playback.jumpToTrack(currentTrack);
|
||||
await playback.jumpToTrack(SpotubeTrackObject.local(currentTrack));
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,12 +75,12 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
) async {
|
||||
final playlist = ref.read(audioPlayerProvider);
|
||||
final playback = ref.read(audioPlayerProvider.notifier);
|
||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
|
||||
final shuffledTracks = tracks.shuffled();
|
||||
if (isPlaylistPlaying) return;
|
||||
|
||||
await playback.load(
|
||||
shuffledTracks,
|
||||
shuffledTracks.union(),
|
||||
initialIndex: 0,
|
||||
autoPlay: true,
|
||||
);
|
||||
@ -93,9 +93,9 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
) async {
|
||||
final playlist = ref.read(audioPlayerProvider);
|
||||
final playback = ref.read(audioPlayerProvider.notifier);
|
||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||
final isPlaylistPlaying = playlist.containsTracks(tracks.union());
|
||||
if (isPlaylistPlaying) return;
|
||||
await playback.addTracks(tracks);
|
||||
await playback.addTracks(tracks.union());
|
||||
if (!context.mounted) return;
|
||||
showToastForAction(context, "add-to-queue", tracks.length);
|
||||
}
|
||||
@ -109,7 +109,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
final trackSnapshot = ref.watch(localTracksProvider);
|
||||
final isPlaylistPlaying = useMemoized(
|
||||
() => playlist.containsTracks(
|
||||
trackSnapshot.asData?.value[location] ?? [],
|
||||
trackSnapshot.asData?.value[location]?.union() ?? [],
|
||||
),
|
||||
[playlist, trackSnapshot, location],
|
||||
);
|
||||
@ -382,7 +382,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
data: (tracks) {
|
||||
final sortedTracks = useMemoized(() {
|
||||
return ServiceUtils.sortTracks(
|
||||
tracks[location] ?? <SpotubeLocalTrackObject>[],
|
||||
tracks[location]?.union() ?? <SpotubeTrackObject>[],
|
||||
sortBy.value);
|
||||
}, [sortBy.value, tracks]);
|
||||
|
||||
@ -463,8 +463,12 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
onTap: () async {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
sortedTracks
|
||||
.map((e) => e.field0
|
||||
as SpotubeLocalTrackObject)
|
||||
.toList(),
|
||||
currentTrack: track.field0
|
||||
as SpotubeLocalTrackObject,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -43,6 +43,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
||||
() => me.asData?.value == null
|
||||
? null
|
||||
: SpotubeSimplePlaylistObject(
|
||||
typeName: "playlist_simple",
|
||||
id: "user-liked-tracks",
|
||||
name: context.l10n.liked_tracks,
|
||||
description: context.l10n.liked_tracks_description,
|
||||
@ -50,6 +51,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
||||
owner: me.asData!.value!,
|
||||
images: [
|
||||
SpotubeImageObject(
|
||||
typeName: "image",
|
||||
url: Assets.images.likedTracks.path,
|
||||
width: 300,
|
||||
height: 300,
|
||||
|
||||
@ -2,10 +2,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/player/player_controls.dart';
|
||||
import 'package:spotube/modules/player/player_queue.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart' as material;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||
@ -25,7 +26,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
||||
final likedTracks = ref.watch(metadataPluginSavedTracksProvider);
|
||||
final likedTracksNotifier =
|
||||
ref.watch(metadataPluginSavedTracksProvider.notifier);
|
||||
final tracks = likedTracks.asData?.value.items ?? [];
|
||||
final tracks = useMemoized(
|
||||
() => likedTracks.asData?.value.items.union() ?? [],
|
||||
[likedTracks.asData?.value]);
|
||||
|
||||
return material.RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
@ -42,7 +45,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
||||
await likedTracksNotifier.fetchMore();
|
||||
},
|
||||
onFetchAll: () async {
|
||||
return await likedTracksNotifier.fetchAll();
|
||||
final res = await likedTracksNotifier.fetchAll();
|
||||
|
||||
return res.union();
|
||||
},
|
||||
onRefresh: () async {
|
||||
ref.invalidate(metadataPluginSavedTracksProvider);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart' as material;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/track_presentation/presentation_props.dart';
|
||||
@ -50,6 +51,10 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
||||
|
||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id);
|
||||
final tracksMemoized = useMemoized(
|
||||
() => tracks.asData?.value.items.union() ?? [],
|
||||
[tracks.asData?.value],
|
||||
);
|
||||
|
||||
return material.RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
@ -71,14 +76,15 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlist.id));
|
||||
},
|
||||
onFetchAll: () async {
|
||||
return await tracksNotifier.fetchAll();
|
||||
final res = await tracksNotifier.fetchAll();
|
||||
return res.union();
|
||||
},
|
||||
),
|
||||
title: playlist.name,
|
||||
description: playlist.description,
|
||||
owner: playlist.owner.name,
|
||||
ownerImage: playlist.owner.images.lastOrNull?.url,
|
||||
tracks: tracks.asData?.value.items ?? [],
|
||||
tracks: tracksMemoized,
|
||||
error: tracks.error,
|
||||
routePath: '/playlist/${playlist.id}',
|
||||
isLiked: isFavoritePlaylist.asData?.value ?? false,
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/connect/connect.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/search/loading.dart';
|
||||
import 'package:spotube/pages/search/search.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
|
||||
@ -13,7 +13,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
@RoutePage()
|
||||
class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
||||
final String title;
|
||||
final List<MetadataFormFieldObject> fields;
|
||||
final List fields;
|
||||
const SettingsMetadataProviderFormPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
|
||||
@ -82,8 +82,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
return plugins.asData?.value.plugins.where((d) {
|
||||
return d.abilities.contains(
|
||||
tabState.value == 1
|
||||
? PluginAbilities.metadata
|
||||
: PluginAbilities.audioSource,
|
||||
? PluginAbility.metadata
|
||||
: PluginAbility.audioSource,
|
||||
);
|
||||
}).toList();
|
||||
}, [tabState.value, plugins.asData?.value]);
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
|
||||
@ -44,7 +44,8 @@ class TrackPage extends HookConsumerWidget {
|
||||
|
||||
final trackQuery = ref.watch(metadataPluginTrackProvider(trackId));
|
||||
|
||||
final track = trackQuery.asData?.value ?? FakeData.track;
|
||||
final track = SpotubeTrackObject.full(trackQuery.asData?.value ??
|
||||
FakeData.track.field0 as SpotubeFullTrackObject);
|
||||
|
||||
void onPlay() async {
|
||||
if (isActive) {
|
||||
@ -230,7 +231,10 @@ class TrackPage extends HookConsumerWidget {
|
||||
const Spacer()
|
||||
else
|
||||
const Gap(20),
|
||||
TrackHeartButton(track: track),
|
||||
TrackHeartButton(
|
||||
track: track.field0
|
||||
as SpotubeFullTrackObject,
|
||||
),
|
||||
TrackOptionsButton(
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
|
||||
@ -22,16 +22,16 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
assert(
|
||||
tracks.every(
|
||||
(track) =>
|
||||
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
|
||||
track is SpotubeTrackObject || track is SpotubeLocalTrackObject,
|
||||
),
|
||||
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
||||
'All tracks must be either SpotubeTrackObject or SpotubeLocalTrackObject',
|
||||
);
|
||||
}
|
||||
|
||||
void _assertAllowedTrack(SpotubeTrackObject tracks) {
|
||||
assert(
|
||||
tracks is SpotubeFullTrackObject || tracks is SpotubeLocalTrackObject,
|
||||
'Track must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
||||
tracks is SpotubeTrackObject || tracks is SpotubeLocalTrackObject,
|
||||
'Track must be either SpotubeTrackObject or SpotubeLocalTrackObject',
|
||||
);
|
||||
}
|
||||
|
||||
@ -345,9 +345,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
return false;
|
||||
}
|
||||
|
||||
return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
|
||||
? a.path == b.path
|
||||
: a.id == b.id;
|
||||
return switch ((a.field0, b.field0)) {
|
||||
(SpotubeLocalTrackObject(), SpotubeLocalTrackObject()) =>
|
||||
(a.field0 as SpotubeLocalTrackObject).path ==
|
||||
(b.field0 as SpotubeLocalTrackObject).path,
|
||||
_ => a.id == b.id,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> load(
|
||||
@ -366,12 +369,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
// Giving the initial track a boost so MediaKit won't skip
|
||||
// because of timeout
|
||||
final intendedActiveTrack = medias.elementAt(initialIndex);
|
||||
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
|
||||
ref.read(
|
||||
sourcedTrackProvider(
|
||||
intendedActiveTrack.track as SpotubeFullTrackObject,
|
||||
).future,
|
||||
);
|
||||
if (intendedActiveTrack.track is SpotubeTrackObject_Full) {
|
||||
ref.read(sourcedTrackProvider(
|
||||
intendedActiveTrack.track.field0 as SpotubeFullTrackObject)
|
||||
.future);
|
||||
}
|
||||
|
||||
if (medias.isEmpty) return;
|
||||
@ -398,7 +399,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
}
|
||||
|
||||
Future<void> swapActiveSource() async {
|
||||
if (state.tracks.isEmpty || state.activeTrack is! SpotubeFullTrackObject) {
|
||||
if (state.tracks.isEmpty || state.activeTrack is! SpotubeTrackObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -103,7 +103,8 @@ class AudioPlayerStreamListeners {
|
||||
return;
|
||||
}
|
||||
|
||||
scrobbler.scrobble(audioPlayerState.activeTrack!);
|
||||
scrobbler.scrobble(
|
||||
audioPlayerState.activeTrack!.field0 as SpotubeFullTrackObject);
|
||||
ref
|
||||
.read(metadataPluginScrobbleProvider.notifier)
|
||||
.scrobble(audioPlayerState.activeTrack!);
|
||||
@ -115,13 +116,28 @@ class AudioPlayerStreamListeners {
|
||||
if (activeTrack.artists.any((a) => a.images == null)) {
|
||||
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||
final artists = await Future.wait(
|
||||
activeTrack.artists
|
||||
.map((artist) => metadataPlugin!.artist.getArtist(artist.id)),
|
||||
activeTrack.artists.map(
|
||||
(artist) => metadataPlugin!.artist.getArtist(
|
||||
id: artist.id,
|
||||
mpscTx: metadataPlugin.sender,
|
||||
),
|
||||
),
|
||||
);
|
||||
activeTrack = activeTrack.copyWith(
|
||||
activeTrack = activeTrack.when(
|
||||
full: (field0) => SpotubeTrackObject.full(
|
||||
field0.copyWith(
|
||||
artists: artists
|
||||
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
local: (field0) => SpotubeTrackObject.local(
|
||||
field0.copyWith(
|
||||
artists: artists
|
||||
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -155,7 +171,8 @@ class AudioPlayerStreamListeners {
|
||||
|
||||
try {
|
||||
await ref.read(
|
||||
sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future,
|
||||
sourcedTrackProvider(nextTrack.field0 as SpotubeFullTrackObject)
|
||||
.future,
|
||||
);
|
||||
} finally {
|
||||
lastTrack = nextTrack.id;
|
||||
|
||||
@ -10,14 +10,14 @@ final queryingTrackInfoProvider = Provider<bool>((ref) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (audioPlayer.activeTrack is! SpotubeFullTrackObject) {
|
||||
if (audioPlayer.activeTrack is! SpotubeTrackObject_Full) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ref
|
||||
.watch(
|
||||
sourcedTrackProvider(
|
||||
audioPlayer.activeTrack! as SpotubeFullTrackObject),
|
||||
audioPlayer.activeTrack?.field0 as SpotubeFullTrackObject),
|
||||
)
|
||||
.isLoading;
|
||||
});
|
||||
|
||||
@ -27,9 +27,8 @@ class AudioPlayerState with _$AudioPlayerState {
|
||||
List<SpotubeTrackObject> tracks = const [],
|
||||
}) {
|
||||
assert(
|
||||
tracks.every((track) =>
|
||||
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject),
|
||||
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
||||
tracks.every((track) => track is SpotubeTrackObject_Local),
|
||||
'All tracks must be either SpotubeTrackObject or SpotubeLocalTrackObject',
|
||||
);
|
||||
|
||||
return AudioPlayerState._inner(
|
||||
@ -53,10 +52,12 @@ class AudioPlayerState with _$AudioPlayerState {
|
||||
bool containsTrack(SpotubeTrackObject track) {
|
||||
return tracks.isNotEmpty &&
|
||||
tracks.any(
|
||||
(t) =>
|
||||
t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject
|
||||
? t.path == track.path
|
||||
: t.id == track.id,
|
||||
(t) => switch ((t.field0, track.field0)) {
|
||||
(SpotubeLocalTrackObject(), SpotubeLocalTrackObject()) =>
|
||||
(t.field0 as SpotubeLocalTrackObject).path ==
|
||||
(track.field0 as SpotubeLocalTrackObject).path,
|
||||
_ => t.id == track.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -208,7 +208,7 @@ class ConnectNotifier extends AsyncNotifier<ConnectState?> {
|
||||
emit(WebSocketLoopEvent(value));
|
||||
}
|
||||
|
||||
Future<void> addTrack(SpotubeFullTrackObject data) async {
|
||||
Future<void> addTrack(SpotubeTrackObject data) async {
|
||||
emit(WebSocketAddTrackEvent(data));
|
||||
}
|
||||
|
||||
|
||||
@ -249,7 +249,7 @@ class DownloadManagerNotifier extends Notifier<List<DownloadTask>> {
|
||||
);
|
||||
await MetadataGod.writeMetadata(
|
||||
file: savePath,
|
||||
metadata: task.track.toMetadata(
|
||||
metadata: SpotubeTrackObject.full(task.track).toMetadata(
|
||||
fileLength: await savePathFile.length(),
|
||||
imageBytes: imageBytes,
|
||||
),
|
||||
|
||||
@ -69,7 +69,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
final items = getAlbumsWithCount(await albumsQuery.get());
|
||||
|
||||
return SpotubePaginationResponseObject(
|
||||
return SpotubeFlattenedPaginationObject(
|
||||
items: items,
|
||||
limit: limit,
|
||||
hasMore: items.length == limit,
|
||||
@ -110,7 +110,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
final historyTopAlbumsProvider = AsyncNotifierProviderFamily<
|
||||
HistoryTopAlbumsNotifier,
|
||||
SpotubePaginationResponseObject<PlaybackHistoryAlbum>,
|
||||
SpotubeFlattenedPaginationObject<PlaybackHistoryAlbum>,
|
||||
HistoryDuration>(
|
||||
() => HistoryTopAlbumsNotifier(),
|
||||
);
|
||||
|
||||
@ -36,7 +36,7 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
final items = getPlaylistsWithCount(await playlistsQuery.get());
|
||||
|
||||
return SpotubePaginationResponseObject(
|
||||
return SpotubeFlattenedPaginationObject(
|
||||
items: items,
|
||||
nextOffset: offset + limit,
|
||||
total: items.length,
|
||||
@ -80,7 +80,7 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
final historyTopPlaylistsProvider = AsyncNotifierProviderFamily<
|
||||
HistoryTopPlaylistsNotifier,
|
||||
SpotubePaginationResponseObject<PlaybackHistoryPlaylist>,
|
||||
SpotubeFlattenedPaginationObject<PlaybackHistoryPlaylist>,
|
||||
HistoryDuration>(
|
||||
() => HistoryTopPlaylistsNotifier(),
|
||||
);
|
||||
|
||||
@ -81,9 +81,12 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
.nonNulls
|
||||
.toList();
|
||||
|
||||
track = track.copyWith(artists: includedArtists);
|
||||
final updatedTrack = track.when(
|
||||
full: (field0) => field0.copyWith(artists: includedArtists).toJson(),
|
||||
local: (field0) => field0.copyWith(artists: includedArtists).toJson(),
|
||||
);
|
||||
|
||||
return e.copyWith(data: track.toJson());
|
||||
return e.copyWith(data: updatedTrack);
|
||||
});
|
||||
|
||||
assert(
|
||||
@ -109,7 +112,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
final items = getTracksWithCount(entries);
|
||||
|
||||
return SpotubePaginationResponseObject<PlaybackHistoryTrack>(
|
||||
return SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>(
|
||||
items: items,
|
||||
nextOffset: offset + limit,
|
||||
total: items.length,
|
||||
@ -190,7 +193,7 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
final historyTopTracksProvider = AsyncNotifierProviderFamily<
|
||||
HistoryTopTracksNotifier,
|
||||
SpotubePaginationResponseObject<PlaybackHistoryTrack>,
|
||||
SpotubeFlattenedPaginationObject<PlaybackHistoryTrack>,
|
||||
HistoryDuration>(
|
||||
() => HistoryTopTracksNotifier(),
|
||||
);
|
||||
|
||||
@ -130,11 +130,11 @@ final localTracksProvider =
|
||||
|
||||
final tracksFromMetadata = filesWithMetadata
|
||||
.map(
|
||||
(fileWithMetadata) => SpotubeTrackObject.localTrackFromFile(
|
||||
(fileWithMetadata) => localTrackFromFile(
|
||||
fileWithMetadata.file,
|
||||
metadata: fileWithMetadata.metadata,
|
||||
art: fileWithMetadata.art,
|
||||
) as SpotubeLocalTrackObject,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
|
||||
@ -15,6 +15,6 @@ final metadataPluginAlbumProvider =
|
||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.album.getAlbum(id);
|
||||
return metadataPlugin.album.getAlbum(id: id, mpscTx: metadataPlugin.sender);
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,13 +6,14 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
class MetadataPluginAlbumReleasesNotifier
|
||||
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin)
|
||||
.album
|
||||
.releases(limit: limit, offset: offset);
|
||||
.releases(mpscTx: await mpscTx, limit: limit, offset: offset)
|
||||
.then((a) => a.flatten());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -24,6 +25,6 @@ class MetadataPluginAlbumReleasesNotifier
|
||||
|
||||
final metadataPluginAlbumReleasesProvider = AsyncNotifierProvider<
|
||||
MetadataPluginAlbumReleasesNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
|
||||
() => MetadataPluginAlbumReleasesNotifier(),
|
||||
);
|
||||
|
||||
@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
class MetadataPluginArtistAlbumNotifier
|
||||
extends FamilyPaginatedAsyncNotifier<SpotubeSimpleAlbumObject, String> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin).artist.albums(
|
||||
arg,
|
||||
return await (await metadataPlugin)
|
||||
.artist
|
||||
.albums(
|
||||
mpscTx: await mpscTx,
|
||||
id: arg,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
)
|
||||
.then((a) => a.flatten());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -26,7 +30,7 @@ class MetadataPluginArtistAlbumNotifier
|
||||
|
||||
final metadataPluginArtistAlbumsProvider = AsyncNotifierProviderFamily<
|
||||
MetadataPluginArtistAlbumNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>,
|
||||
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>,
|
||||
String>(
|
||||
() => MetadataPluginArtistAlbumNotifier(),
|
||||
);
|
||||
|
||||
@ -15,6 +15,7 @@ final metadataPluginArtistProvider =
|
||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.artist.getArtist(artistId);
|
||||
return metadataPlugin.artist
|
||||
.getArtist(id: artistId, mpscTx: metadataPlugin.sender);
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
class MetadataPluginArtistRelatedArtistsNotifier
|
||||
extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin).artist.related(
|
||||
arg,
|
||||
return await (await metadataPlugin)
|
||||
.artist
|
||||
.related(
|
||||
id: arg,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
mpscTx: await mpscTx,
|
||||
)
|
||||
.then((a) => a.flatten());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -26,7 +30,7 @@ class MetadataPluginArtistRelatedArtistsNotifier
|
||||
|
||||
final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily<
|
||||
MetadataPluginArtistRelatedArtistsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>,
|
||||
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>,
|
||||
String>(
|
||||
() => MetadataPluginArtistRelatedArtistsNotifier(),
|
||||
);
|
||||
|
||||
@ -5,19 +5,20 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
|
||||
class MetadataPluginArtistTopTracksNotifier
|
||||
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject,
|
||||
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
|
||||
String> {
|
||||
MetadataPluginArtistTopTracksNotifier() : super();
|
||||
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
final tracks = await (await metadataPlugin).artist.topTracks(
|
||||
arg,
|
||||
id: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return tracks;
|
||||
return tracks.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -32,7 +33,7 @@ class MetadataPluginArtistTopTracksNotifier
|
||||
final metadataPluginArtistTopTracksProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<
|
||||
MetadataPluginArtistTopTracksNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>,
|
||||
SpotubeFlattenedPaginationObject<SpotubeTrackObject>,
|
||||
String>(
|
||||
() => MetadataPluginArtistTopTracksNotifier(),
|
||||
);
|
||||
|
||||
@ -41,10 +41,10 @@ class AudioSourceAvailableQualityPresetsNotifier
|
||||
listenSelf((previous, next) {
|
||||
final isNewLossless =
|
||||
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
|
||||
is SpotubeAudioSourceContainerPresetLossless;
|
||||
is SpotubeAudioSourceContainerPreset_Lossless;
|
||||
final isOldLossless = previous?.presets
|
||||
.elementAtOrNull(previous.selectedStreamingContainerIndex)
|
||||
is SpotubeAudioSourceContainerPresetLossless;
|
||||
is SpotubeAudioSourceContainerPreset_Lossless;
|
||||
if (!isOldLossless && isNewLossless) {
|
||||
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
|
||||
} else if (isOldLossless && !isNewLossless) {
|
||||
@ -72,11 +72,13 @@ class AudioSourceAvailableQualityPresetsNotifier
|
||||
state =
|
||||
AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr))
|
||||
.copyWith(
|
||||
presets: audioSource.audioSource.supportedPresets,
|
||||
presets: await audioSource.audioSource
|
||||
.supportedPresets(mpscTx: audioSource.sender),
|
||||
);
|
||||
} else {
|
||||
state = AudioSourcePresetsState(
|
||||
presets: audioSource.audioSource.supportedPresets,
|
||||
presets: await audioSource.audioSource
|
||||
.supportedPresets(mpscTx: audioSource.sender),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,15 +6,19 @@ import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
class MetadataPluginBrowseSectionItemsNotifier
|
||||
extends FamilyPaginatedAsyncNotifier<Object, String> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<Object>> fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<Object>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin).browse.sectionItems(
|
||||
arg,
|
||||
return await (await metadataPlugin)
|
||||
.browse
|
||||
.sectionItems(
|
||||
id: arg,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
mpscTx: await mpscTx,
|
||||
)
|
||||
.then((value) => value.flatten());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -26,7 +30,7 @@ class MetadataPluginBrowseSectionItemsNotifier
|
||||
|
||||
final metadataPluginBrowseSectionItemsProvider = AsyncNotifierProviderFamily<
|
||||
MetadataPluginBrowseSectionItemsNotifier,
|
||||
SpotubePaginationResponseObject<Object>,
|
||||
SpotubeFlattenedPaginationObject<Object>,
|
||||
String>(
|
||||
() => MetadataPluginBrowseSectionItemsNotifier(),
|
||||
);
|
||||
|
||||
@ -4,17 +4,20 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
|
||||
class MetadataPluginBrowseSectionsNotifier
|
||||
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject<Object>> {
|
||||
extends PaginatedAsyncNotifier<SpotubeBrowseSectionObject> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>
|
||||
fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin).browse.sections(
|
||||
return await (await metadataPlugin)
|
||||
.browse
|
||||
.sections(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
mpscTx: await mpscTx,
|
||||
)
|
||||
.then((value) => value.flatten());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -26,6 +29,6 @@ class MetadataPluginBrowseSectionsNotifier
|
||||
|
||||
final metadataPluginBrowseSectionsProvider = AsyncNotifierProvider<
|
||||
MetadataPluginBrowseSectionsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeBrowseSectionObject<Object>>>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeBrowseSectionObject>>(
|
||||
() => MetadataPluginBrowseSectionsNotifier(),
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
FutureOr<bool> build() async {
|
||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||
if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities
|
||||
.contains(PluginAbilities.authentication) !=
|
||||
.contains(PluginAbility.authentication) !=
|
||||
true) {
|
||||
return false;
|
||||
}
|
||||
@ -19,15 +19,19 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
return false;
|
||||
}
|
||||
|
||||
final sub = defaultPlugin.auth.authStateStream.listen((event) {
|
||||
state = AsyncData(defaultPlugin.auth.isAuthenticated());
|
||||
/// `authState` can be called once in the SpotubePlugin's lifetime.
|
||||
final sub = defaultPlugin.authState().listen((event) async {
|
||||
state = AsyncData(
|
||||
await defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender),
|
||||
);
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
sub.cancel();
|
||||
});
|
||||
|
||||
return defaultPlugin.auth.isAuthenticated();
|
||||
return await defaultPlugin.auth
|
||||
.isAuthenticated(mpscTx: defaultPlugin.sender);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +46,7 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||
if (defaultPluginConfig
|
||||
.asData?.value.defaultAudioSourcePluginConfig?.abilities
|
||||
.contains(PluginAbilities.authentication) !=
|
||||
.contains(PluginAbility.authentication) !=
|
||||
true) {
|
||||
return false;
|
||||
}
|
||||
@ -52,15 +56,16 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
return false;
|
||||
}
|
||||
|
||||
final sub = defaultPlugin.auth.authStateStream.listen((event) {
|
||||
state = AsyncData(defaultPlugin.auth.isAuthenticated());
|
||||
final sub = defaultPlugin.authState().listen((event) async {
|
||||
state = AsyncData(await defaultPlugin.auth
|
||||
.isAuthenticated(mpscTx: defaultPlugin.sender));
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
sub.cancel();
|
||||
});
|
||||
|
||||
return defaultPlugin.auth.isAuthenticated();
|
||||
return defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ class MetadataPluginRepositoriesNotifier
|
||||
return _hasMore[response.requestOptions.uri.host] ?? false;
|
||||
});
|
||||
|
||||
return SpotubePaginationResponseObject(
|
||||
return SpotubeFlattenedPaginationObject(
|
||||
items: repos,
|
||||
total: responses.fold<int>(
|
||||
0,
|
||||
@ -85,6 +85,6 @@ class MetadataPluginRepositoriesNotifier
|
||||
|
||||
final metadataPluginRepositoriesProvider = AsyncNotifierProvider<
|
||||
MetadataPluginRepositoriesNotifier,
|
||||
SpotubePaginationResponseObject<MetadataPluginRepository>>(
|
||||
SpotubeFlattenedPaginationObject<MetadataPluginRepository>>(
|
||||
() => MetadataPluginRepositoriesNotifier(),
|
||||
);
|
||||
|
||||
@ -17,7 +17,7 @@ class MetadataPluginScrobbleNotifier
|
||||
|
||||
if (metadataPlugin.valueOrNull == null ||
|
||||
pluginConfig == null ||
|
||||
!pluginConfig.abilities.contains(PluginAbilities.scrobbling)) {
|
||||
!pluginConfig.abilities.contains(PluginAbility.scrobbling)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -25,23 +25,46 @@ class MetadataPluginScrobbleNotifier
|
||||
|
||||
final subscription = controller.stream.listen((event) async {
|
||||
try {
|
||||
await metadataPlugin.valueOrNull?.core.scrobble({
|
||||
"id": event.id,
|
||||
"title": event.name,
|
||||
"artists": event.artists
|
||||
.map((artist) => {
|
||||
"id": artist.id,
|
||||
"name": artist.name,
|
||||
})
|
||||
final details = switch (event) {
|
||||
SpotubeTrackObject_Full(:final field0) => ScrobbleDetails(
|
||||
id: field0.id,
|
||||
title: field0.name,
|
||||
artists: field0.artists
|
||||
.map((artist) => ScrobbleArtist(
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
))
|
||||
.toList(),
|
||||
"album": {
|
||||
"id": event.album.id,
|
||||
"name": event.album.name,
|
||||
},
|
||||
"timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
"duration_ms": event.durationMs,
|
||||
"isrc": event is SpotubeFullTrackObject ? event.isrc : null,
|
||||
});
|
||||
album: ScrobbleAlbum(
|
||||
id: field0.album.id,
|
||||
name: field0.album.name,
|
||||
),
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
durationMs: field0.durationMs,
|
||||
isrc: field0.isrc,
|
||||
),
|
||||
SpotubeTrackObject_Local(:final field0) => ScrobbleDetails(
|
||||
id: field0.id,
|
||||
title: field0.name,
|
||||
artists: field0.artists
|
||||
.map((artist) => ScrobbleArtist(
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
))
|
||||
.toList(),
|
||||
album: ScrobbleAlbum(
|
||||
id: field0.album.id,
|
||||
name: field0.album.name,
|
||||
),
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
durationMs: field0.durationMs,
|
||||
),
|
||||
};
|
||||
|
||||
await metadataPlugin.valueOrNull?.core.scrobble(
|
||||
mpscTx: metadataPlugin.valueOrNull!.sender,
|
||||
details: details,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ final metadataPluginSupportTextProvider = FutureProvider<String>((ref) async {
|
||||
if (metadataPlugin == null) {
|
||||
throw 'No metadata plugin available';
|
||||
}
|
||||
return await metadataPlugin.core.support;
|
||||
return await metadataPlugin.core.support(mpscTx: metadataPlugin.sender);
|
||||
});
|
||||
|
||||
final audioSourcePluginSupportTextProvider =
|
||||
@ -17,5 +17,5 @@ final audioSourcePluginSupportTextProvider =
|
||||
if (audioSourcePlugin == null) {
|
||||
throw 'No metadata plugin available';
|
||||
}
|
||||
return await audioSourcePlugin.core.support;
|
||||
return await audioSourcePlugin.core.support(mpscTx: audioSourcePlugin.sender);
|
||||
});
|
||||
|
||||
@ -12,6 +12,6 @@ final metadataPluginUserProvider = FutureProvider<SpotubeUserObject?>(
|
||||
if (!authenticated || metadataPlugin == null) {
|
||||
return null;
|
||||
}
|
||||
return metadataPlugin.user.me();
|
||||
return metadataPlugin.user.me(mpscTx: metadataPlugin.sender);
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,14 +6,18 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
class MetadataPluginSavedAlbumNotifier
|
||||
extends PaginatedAsyncNotifier<SpotubeSimpleAlbumObject> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>> fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin).user.savedAlbums(
|
||||
return await (await metadataPlugin)
|
||||
.user
|
||||
.savedAlbums(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
mpscTx: await mpscTx,
|
||||
)
|
||||
.then((a) => a.flatten());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -35,7 +39,10 @@ class MetadataPluginSavedAlbumNotifier
|
||||
),
|
||||
);
|
||||
try {
|
||||
await (await metadataPlugin).album.save(albums.map((e) => e.id).toList());
|
||||
await (await metadataPlugin).album.save(
|
||||
ids: albums.map((e) => e.id).toList(),
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -58,7 +65,9 @@ class MetadataPluginSavedAlbumNotifier
|
||||
),
|
||||
);
|
||||
try {
|
||||
await (await metadataPlugin).album.unsave(albumIds);
|
||||
await (await metadataPlugin)
|
||||
.album
|
||||
.unsave(ids: albumIds, mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -68,7 +77,7 @@ class MetadataPluginSavedAlbumNotifier
|
||||
|
||||
final metadataPluginSavedAlbumsProvider = AsyncNotifierProvider<
|
||||
MetadataPluginSavedAlbumNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>>(
|
||||
() => MetadataPluginSavedAlbumNotifier(),
|
||||
);
|
||||
|
||||
|
||||
@ -6,16 +6,17 @@ import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
class MetadataPluginSavedArtistNotifier
|
||||
extends PaginatedAsyncNotifier<SpotubeFullArtistObject> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
|
||||
Future<SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
final artists = await (await metadataPlugin).user.savedArtists(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return artists;
|
||||
return artists.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -39,7 +40,7 @@ class MetadataPluginSavedArtistNotifier
|
||||
try {
|
||||
await (await metadataPlugin)
|
||||
.artist
|
||||
.save(artists.map((e) => e.id).toList());
|
||||
.save(ids: artists.map((e) => e.id).toList(), mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -63,7 +64,9 @@ class MetadataPluginSavedArtistNotifier
|
||||
);
|
||||
|
||||
try {
|
||||
await (await metadataPlugin).artist.unsave(artistIds);
|
||||
await (await metadataPlugin)
|
||||
.artist
|
||||
.unsave(ids: artistIds, mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -73,7 +76,7 @@ class MetadataPluginSavedArtistNotifier
|
||||
|
||||
final metadataPluginSavedArtistsProvider = AsyncNotifierProvider<
|
||||
MetadataPluginSavedArtistNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>>(
|
||||
() => MetadataPluginSavedArtistNotifier(),
|
||||
);
|
||||
|
||||
|
||||
@ -15,9 +15,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
fetch(int offset, int limit) async {
|
||||
final playlists = await (await metadataPlugin)
|
||||
.user
|
||||
.savedPlaylists(limit: limit, offset: offset);
|
||||
.savedPlaylists(limit: limit, offset: offset, mpscTx: await mpscTx);
|
||||
|
||||
return playlists;
|
||||
return playlists.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -58,7 +58,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
);
|
||||
|
||||
try {
|
||||
await (await metadataPlugin).playlist.save(playlist.id);
|
||||
await (await metadataPlugin)
|
||||
.playlist
|
||||
.save(playlistId: playlist.id, mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -76,7 +78,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
);
|
||||
|
||||
try {
|
||||
await (await metadataPlugin).playlist.unsave(playlist.id);
|
||||
await (await metadataPlugin)
|
||||
.playlist
|
||||
.unsave(playlistId: playlist.id, mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -88,7 +92,9 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
final oldState = state;
|
||||
try {
|
||||
state = const AsyncLoading();
|
||||
await (await metadataPlugin).playlist.deletePlaylist(playlistId);
|
||||
await (await metadataPlugin)
|
||||
.playlist
|
||||
.deletePlaylist(playlistId: playlistId, mpscTx: await mpscTx);
|
||||
ref.invalidateSelf();
|
||||
ref.invalidate(metadataPluginIsSavedPlaylistProvider(playlistId));
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||
@ -101,9 +107,8 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
Future<void> addTracks(String playlistId, List<String> trackIds) async {
|
||||
if (state.value == null) return;
|
||||
|
||||
await (await metadataPlugin)
|
||||
.playlist
|
||||
.addTracks(playlistId, trackIds: trackIds);
|
||||
await (await metadataPlugin).playlist.addTracks(
|
||||
playlistId: playlistId, trackIds: trackIds, mpscTx: await mpscTx);
|
||||
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||
}
|
||||
@ -111,9 +116,8 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
Future<void> removeTracks(String playlistId, List<String> trackIds) async {
|
||||
if (state.value == null) return;
|
||||
|
||||
await (await metadataPlugin)
|
||||
.playlist
|
||||
.removeTracks(playlistId, trackIds: trackIds);
|
||||
await (await metadataPlugin).playlist.removeTracks(
|
||||
playlistId: playlistId, trackIds: trackIds, mpscTx: await mpscTx);
|
||||
|
||||
ref.invalidate(metadataPluginPlaylistTracksProvider(playlistId));
|
||||
}
|
||||
@ -121,7 +125,7 @@ class MetadataPluginSavedPlaylistsNotifier
|
||||
|
||||
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
|
||||
MetadataPluginSavedPlaylistsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>>(
|
||||
() => MetadataPluginSavedPlaylistsNotifier(),
|
||||
);
|
||||
|
||||
|
||||
@ -13,9 +13,10 @@ class MetadataPluginSavedTracksNotifier
|
||||
final tracks = await (await metadataPlugin).user.savedTracks(
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return tracks;
|
||||
return tracks.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -26,7 +27,7 @@ class MetadataPluginSavedTracksNotifier
|
||||
return await fetch(0, 20);
|
||||
}
|
||||
|
||||
Future<void> addFavorite(List<SpotubeTrackObject> tracks) async {
|
||||
Future<void> addFavorite(List<SpotubeFullTrackObject> tracks) async {
|
||||
if (state.value == null) {
|
||||
return;
|
||||
}
|
||||
@ -34,22 +35,21 @@ class MetadataPluginSavedTracksNotifier
|
||||
final oldState = state.value;
|
||||
state = AsyncData(
|
||||
state.value!.copyWith(
|
||||
items: [
|
||||
...tracks.whereType<SpotubeFullTrackObject>(),
|
||||
...state.value!.items
|
||||
],
|
||||
items: [...tracks, ...state.value!.items],
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await (await metadataPlugin).track.save(tracks.map((e) => e.id).toList());
|
||||
await (await metadataPlugin)
|
||||
.track
|
||||
.save(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeFavorite(List<SpotubeTrackObject> tracks) async {
|
||||
Future<void> removeFavorite(List<SpotubeFullTrackObject> tracks) async {
|
||||
if (state.value == null) {
|
||||
return;
|
||||
}
|
||||
@ -68,7 +68,7 @@ class MetadataPluginSavedTracksNotifier
|
||||
try {
|
||||
await (await metadataPlugin)
|
||||
.track
|
||||
.unsave(tracks.map((e) => e.id).toList());
|
||||
.unsave(ids: tracks.map((e) => e.id).toList(), mpscTx: await mpscTx);
|
||||
} catch (e) {
|
||||
state = AsyncData(oldState!);
|
||||
rethrow;
|
||||
@ -78,7 +78,7 @@ class MetadataPluginSavedTracksNotifier
|
||||
|
||||
final metadataPluginSavedTracksProvider = AutoDisposeAsyncNotifierProvider<
|
||||
MetadataPluginSavedTracksNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>>(
|
||||
() => MetadataPluginSavedTracksNotifier(),
|
||||
);
|
||||
|
||||
|
||||
@ -11,11 +11,12 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/provider/server/server.dart';
|
||||
import 'package:spotube/services/dio/dio.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
@ -24,6 +25,8 @@ final allowedDomainsRegex = RegExp(
|
||||
r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+",
|
||||
);
|
||||
|
||||
final kPluginApiVersion = Version.parse("2.0.0");
|
||||
|
||||
class MetadataPluginState {
|
||||
final List<PluginConfiguration> plugins;
|
||||
final int defaultMetadataPlugin;
|
||||
@ -129,7 +132,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
repository: plugin.repository,
|
||||
apis: plugin.apis
|
||||
.map(
|
||||
(e) => PluginApis.values.firstWhereOrNull(
|
||||
(e) => PluginApi.values.firstWhereOrNull(
|
||||
(api) => api.name == e,
|
||||
),
|
||||
)
|
||||
@ -137,7 +140,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
.toList(),
|
||||
abilities: plugin.abilities
|
||||
.map(
|
||||
(e) => PluginAbilities.values.firstWhereOrNull(
|
||||
(e) => PluginAbility.values.firstWhereOrNull(
|
||||
(ability) => ability.name == e,
|
||||
),
|
||||
)
|
||||
@ -149,7 +152,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
final pluginJsonFile =
|
||||
File(join(pluginExtractionDir.path, "plugin.json"));
|
||||
final pluginBinaryFile =
|
||||
File(join(pluginExtractionDir.path, "plugin.out"));
|
||||
File(join(pluginExtractionDir.path, "plugin.js"));
|
||||
|
||||
if (!await pluginExtractionDir.exists() ||
|
||||
!await pluginJsonFile.exists() ||
|
||||
@ -374,13 +377,12 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
|
||||
bool validatePluginApiCompatibility(PluginConfiguration plugin) {
|
||||
final configPluginApiVersion = Version.parse(plugin.pluginApiVersion);
|
||||
final appPluginApiVersion = MetadataPlugin.pluginApiVersion;
|
||||
|
||||
// Plugin API's major version must match the app's major version
|
||||
if (configPluginApiVersion.major != appPluginApiVersion.major) {
|
||||
if (configPluginApiVersion.major != kPluginApiVersion.major) {
|
||||
return false;
|
||||
}
|
||||
return configPluginApiVersion >= appPluginApiVersion;
|
||||
return configPluginApiVersion >= kPluginApiVersion;
|
||||
}
|
||||
|
||||
void _assertPluginApiCompatibility(PluginConfiguration plugin) {
|
||||
@ -419,18 +421,18 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
selectedForMetadata: Value(
|
||||
(state.valueOrNull?.plugins
|
||||
.where(
|
||||
(d) => d.abilities.contains(PluginAbilities.metadata))
|
||||
(d) => d.abilities.contains(PluginAbility.metadata))
|
||||
.isEmpty ??
|
||||
true) &&
|
||||
plugin.abilities.contains(PluginAbilities.metadata),
|
||||
plugin.abilities.contains(PluginAbility.metadata),
|
||||
),
|
||||
selectedForAudioSource: Value(
|
||||
(state.valueOrNull?.plugins
|
||||
.where((d) =>
|
||||
d.abilities.contains(PluginAbilities.audioSource))
|
||||
d.abilities.contains(PluginAbility.audioSource))
|
||||
.isEmpty ??
|
||||
true) &&
|
||||
plugin.abilities.contains(PluginAbilities.audioSource),
|
||||
plugin.abilities.contains(PluginAbility.audioSource),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -450,8 +452,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
// only when there is 1 remaining plugin
|
||||
if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) {
|
||||
final remainingPlugins = state.valueOrNull?.plugins.where(
|
||||
(p) =>
|
||||
p != plugin && p.abilities.contains(PluginAbilities.metadata),
|
||||
(p) => p != plugin && p.abilities.contains(PluginAbility.metadata),
|
||||
) ??
|
||||
[];
|
||||
if (remainingPlugins.length == 1) {
|
||||
@ -462,8 +463,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) {
|
||||
final remainingPlugins = state.valueOrNull?.plugins.where(
|
||||
(p) =>
|
||||
p != plugin &&
|
||||
p.abilities.contains(PluginAbilities.audioSource),
|
||||
p != plugin && p.abilities.contains(PluginAbility.audioSource),
|
||||
) ??
|
||||
[];
|
||||
if (remainingPlugins.length == 1) {
|
||||
@ -523,7 +523,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
|
||||
Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async {
|
||||
assert(
|
||||
plugin.abilities.contains(PluginAbilities.metadata),
|
||||
plugin.abilities.contains(PluginAbility.metadata),
|
||||
"Must be a metadata plugin",
|
||||
);
|
||||
|
||||
@ -541,7 +541,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
|
||||
Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async {
|
||||
assert(
|
||||
plugin.abilities.contains(PluginAbilities.audioSource),
|
||||
plugin.abilities.contains(PluginAbility.audioSource),
|
||||
"Must be an audio-source plugin",
|
||||
);
|
||||
|
||||
@ -556,16 +556,16 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List> getPluginByteCode(PluginConfiguration plugin) async {
|
||||
Future<String> getPluginSourceCode(PluginConfiguration plugin) async {
|
||||
final pluginExtractionDirPath = await _getPluginExtractionDir(plugin);
|
||||
|
||||
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.out"));
|
||||
final libraryFile = File(join(pluginExtractionDirPath.path, "plugin.js"));
|
||||
|
||||
if (!libraryFile.existsSync()) {
|
||||
throw MetadataPluginException.pluginByteCodeFileNotFound();
|
||||
throw MetadataPluginException.pluginSourceCodeFileNotFound();
|
||||
}
|
||||
|
||||
return await libraryFile.readAsBytes();
|
||||
return await libraryFile.readAsString();
|
||||
}
|
||||
|
||||
Future<File?> getLogoPath(PluginConfiguration plugin) async {
|
||||
@ -586,27 +586,46 @@ final metadataPluginsProvider =
|
||||
MetadataPluginNotifier.new,
|
||||
);
|
||||
|
||||
final _pluginProvider =
|
||||
FutureProvider.family<MetadataPlugin?, PluginConfiguration?>(
|
||||
(ref, config) async {
|
||||
final (:server, :port) = await ref.watch(serverProvider.future);
|
||||
final serverSecret = ref.watch(serverRandomSecretProvider);
|
||||
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||
final pluginSourceCode = await pluginsNotifier.getPluginSourceCode(config);
|
||||
|
||||
final spotubePlugin = SpotubePlugin();
|
||||
final plugin = MetadataPlugin(
|
||||
plugin: spotubePlugin,
|
||||
sender: await spotubePlugin.createContext(
|
||||
pluginScript: pluginSourceCode,
|
||||
pluginConfig: config,
|
||||
serverEndpointUrl: "http://${server.address.host}:$port",
|
||||
serverSecret: serverSecret,
|
||||
localStorageDir: (await getApplicationSupportDirectory()).path,
|
||||
),
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
plugin.close();
|
||||
});
|
||||
|
||||
return plugin;
|
||||
},
|
||||
);
|
||||
|
||||
final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
(ref) async {
|
||||
final defaultPlugin = await ref.watch(
|
||||
metadataPluginsProvider
|
||||
.selectAsync((data) => data.defaultMetadataPluginConfig),
|
||||
);
|
||||
final youtubeEngine = ref.read(youtubeEngineProvider);
|
||||
|
||||
if (defaultPlugin == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||
final pluginByteCode =
|
||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
||||
|
||||
return await MetadataPlugin.create(
|
||||
youtubeEngine,
|
||||
defaultPlugin,
|
||||
pluginByteCode,
|
||||
);
|
||||
return await ref.watch(_pluginProvider(defaultPlugin).future);
|
||||
},
|
||||
);
|
||||
|
||||
@ -616,20 +635,6 @@ final audioSourcePluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
metadataPluginsProvider
|
||||
.selectAsync((data) => data.defaultAudioSourcePluginConfig),
|
||||
);
|
||||
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
||||
|
||||
if (defaultPlugin == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||
final pluginByteCode =
|
||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
||||
|
||||
return await MetadataPlugin.create(
|
||||
youtubeEngine,
|
||||
defaultPlugin,
|
||||
pluginByteCode,
|
||||
);
|
||||
return await ref.watch(_pluginProvider(defaultPlugin).future);
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||
|
||||
class MetadataPluginPlaylistNotifier
|
||||
extends AutoDisposeFamilyAsyncNotifier<SpotubeFullPlaylistObject, String> {
|
||||
@ -19,11 +20,17 @@ class MetadataPluginPlaylistNotifier
|
||||
return metadataPlugin;
|
||||
}
|
||||
|
||||
Future<OpaqueSender> get mpscTx async {
|
||||
return (await metadataPlugin).sender;
|
||||
}
|
||||
|
||||
@override
|
||||
build(playlistId) async {
|
||||
ref.cacheFor();
|
||||
|
||||
return (await metadataPlugin).playlist.getPlaylist(playlistId);
|
||||
return (await metadataPlugin)
|
||||
.playlist
|
||||
.getPlaylist(id: playlistId, mpscTx: await mpscTx);
|
||||
}
|
||||
|
||||
Future<void> create({
|
||||
@ -40,12 +47,13 @@ class MetadataPluginPlaylistNotifier
|
||||
}
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final playlist = await (await metadataPlugin).playlist.create(
|
||||
userId,
|
||||
final playlist = await (await metadataPlugin).playlist.createPlaylist(
|
||||
userId: userId,
|
||||
name: name,
|
||||
description: description,
|
||||
public: public,
|
||||
collaborative: collaborative,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
if (playlist != null) {
|
||||
state = AsyncValue.data(playlist);
|
||||
@ -71,12 +79,13 @@ class MetadataPluginPlaylistNotifier
|
||||
collaborative == null) {
|
||||
throw Exception('No modifications provided.');
|
||||
}
|
||||
await (await metadataPlugin).playlist.update(
|
||||
arg,
|
||||
await (await metadataPlugin).playlist.updatePlaylist(
|
||||
playlistId: arg,
|
||||
name: name,
|
||||
description: description,
|
||||
public: public,
|
||||
collaborative: collaborative,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
ref.invalidateSelf();
|
||||
} on Exception catch (e) {
|
||||
|
||||
@ -12,7 +12,7 @@ class MetadataPluginSearchAlbumsNotifier
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
if (arg.isEmpty) {
|
||||
return SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>(
|
||||
return SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>(
|
||||
limit: limit,
|
||||
nextOffset: null,
|
||||
total: 0,
|
||||
@ -22,12 +22,13 @@ class MetadataPluginSearchAlbumsNotifier
|
||||
}
|
||||
|
||||
final res = await (await metadataPlugin).search.albums(
|
||||
arg,
|
||||
query: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return res;
|
||||
return res.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -41,6 +42,6 @@ class MetadataPluginSearchAlbumsNotifier
|
||||
|
||||
final metadataPluginSearchAlbumsProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchAlbumsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>, String>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeSimpleAlbumObject>, String>(
|
||||
() => MetadataPluginSearchAlbumsNotifier(),
|
||||
);
|
||||
|
||||
@ -12,7 +12,8 @@ final metadataPluginSearchAllProvider =
|
||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.search.all(query);
|
||||
return metadataPlugin.search
|
||||
.all(query: query, mpscTx: metadataPlugin.sender);
|
||||
},
|
||||
);
|
||||
|
||||
@ -22,5 +23,5 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async {
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||
}
|
||||
return metadataPlugin.search.chips;
|
||||
return metadataPlugin.search.chips(mpscTx: metadataPlugin.sender);
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ class MetadataPluginSearchArtistsNotifier
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
if (arg.isEmpty) {
|
||||
return SpotubePaginationResponseObject<SpotubeFullArtistObject>(
|
||||
return SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>(
|
||||
limit: limit,
|
||||
nextOffset: null,
|
||||
total: 0,
|
||||
@ -22,12 +22,13 @@ class MetadataPluginSearchArtistsNotifier
|
||||
}
|
||||
|
||||
final res = await (await metadataPlugin).search.artists(
|
||||
arg,
|
||||
query: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return res;
|
||||
return res.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -41,6 +42,6 @@ class MetadataPluginSearchArtistsNotifier
|
||||
|
||||
final metadataPluginSearchArtistsProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchArtistsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>, String>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeFullArtistObject>, String>(
|
||||
() => MetadataPluginSearchArtistsNotifier(),
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ class MetadataPluginSearchPlaylistsNotifier
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
if (arg.isEmpty) {
|
||||
return SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>(
|
||||
return SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>(
|
||||
limit: limit,
|
||||
nextOffset: null,
|
||||
total: 0,
|
||||
@ -22,12 +22,13 @@ class MetadataPluginSearchPlaylistsNotifier
|
||||
}
|
||||
|
||||
final res = await (await metadataPlugin).search.playlists(
|
||||
arg,
|
||||
query: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return res;
|
||||
return res.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -42,7 +43,7 @@ class MetadataPluginSearchPlaylistsNotifier
|
||||
final metadataPluginSearchPlaylistsProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<
|
||||
MetadataPluginSearchPlaylistsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>,
|
||||
SpotubeFlattenedPaginationObject<SpotubeSimplePlaylistObject>,
|
||||
String>(
|
||||
() => MetadataPluginSearchPlaylistsNotifier(),
|
||||
);
|
||||
|
||||
@ -5,14 +5,14 @@ import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
|
||||
class MetadataPluginSearchTracksNotifier
|
||||
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeFullTrackObject,
|
||||
extends AutoDisposeFamilyPaginatedAsyncNotifier<SpotubeTrackObject,
|
||||
String> {
|
||||
MetadataPluginSearchTracksNotifier() : super();
|
||||
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
if (arg.isEmpty) {
|
||||
return SpotubePaginationResponseObject<SpotubeFullTrackObject>(
|
||||
return SpotubeFlattenedPaginationObject<SpotubeTrackObject>(
|
||||
limit: limit,
|
||||
nextOffset: null,
|
||||
total: 0,
|
||||
@ -22,12 +22,13 @@ class MetadataPluginSearchTracksNotifier
|
||||
}
|
||||
|
||||
final tracks = await (await metadataPlugin).search.tracks(
|
||||
arg,
|
||||
query: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return tracks;
|
||||
return tracks.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -41,6 +42,6 @@ class MetadataPluginSearchTracksNotifier
|
||||
|
||||
final metadataPluginSearchTracksProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginSearchTracksNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeTrackObject>, String>(
|
||||
() => MetadataPluginSearchTracksNotifier(),
|
||||
);
|
||||
|
||||
@ -12,12 +12,13 @@ class MetadataPluginAlbumTracksNotifier
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
final tracks = await (await metadataPlugin).album.tracks(
|
||||
arg,
|
||||
id: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return tracks;
|
||||
return tracks.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -31,6 +32,6 @@ class MetadataPluginAlbumTracksNotifier
|
||||
|
||||
final metadataPluginAlbumTracksProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginAlbumTracksNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>, String>(
|
||||
() => MetadataPluginAlbumTracksNotifier(),
|
||||
);
|
||||
|
||||
@ -12,12 +12,13 @@ class MetadataPluginPlaylistTracksNotifier
|
||||
@override
|
||||
fetch(offset, limit) async {
|
||||
final tracks = await (await metadataPlugin).playlist.tracks(
|
||||
arg,
|
||||
id: arg,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
mpscTx: await mpscTx,
|
||||
);
|
||||
|
||||
return tracks;
|
||||
return tracks.flatten();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -31,6 +32,6 @@ class MetadataPluginPlaylistTracksNotifier
|
||||
|
||||
final metadataPluginPlaylistTracksProvider =
|
||||
AutoDisposeAsyncNotifierProviderFamily<MetadataPluginPlaylistTracksNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullTrackObject>, String>(
|
||||
SpotubeFlattenedPaginationObject<SpotubeFullTrackObject>, String>(
|
||||
() => MetadataPluginPlaylistTracksNotifier(),
|
||||
);
|
||||
|
||||
@ -11,5 +11,7 @@ final metadataPluginTrackProvider =
|
||||
throw MetadataPluginException.noDefaultMetadataPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.track.getTrack(trackId);
|
||||
return await metadataPlugin.track
|
||||
.getTrack(id: trackId, mpscTx: metadataPlugin.sender);
|
||||
});
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
|
||||
final metadataPluginUpdateCheckerProvider =
|
||||
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
||||
final metadataPluginConfigs = await ref.watch(metadataPluginsProvider.future);
|
||||
try {
|
||||
final metadataPluginConfigs =
|
||||
await ref.watch(metadataPluginsProvider.future);
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null ||
|
||||
@ -12,12 +15,21 @@ final metadataPluginUpdateCheckerProvider =
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadataPlugin.core
|
||||
.checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!);
|
||||
final res = await metadataPlugin.core.checkUpdate(
|
||||
pluginConfig: metadataPluginConfigs.defaultMetadataPluginConfig!,
|
||||
mpscTx: metadataPlugin.sender,
|
||||
);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
debugPrint('Error checking metadata plugin update: $e');
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
|
||||
final audioSourcePluginUpdateCheckerProvider =
|
||||
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
||||
try {
|
||||
final audioSourcePluginConfigs =
|
||||
await ref.watch(metadataPluginsProvider.future);
|
||||
final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future);
|
||||
@ -27,6 +39,14 @@ final audioSourcePluginUpdateCheckerProvider =
|
||||
return null;
|
||||
}
|
||||
|
||||
return audioSourcePlugin.core
|
||||
.checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!);
|
||||
final res = await audioSourcePlugin.core.checkUpdate(
|
||||
pluginConfig: audioSourcePluginConfigs.defaultAudioSourcePluginConfig!,
|
||||
mpscTx: audioSourcePlugin.sender,
|
||||
);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
debugPrint('Error checking audio source plugin update: $e');
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||
|
||||
extension PaginationExtension<T> on AsyncValue<T> {
|
||||
bool get isLoadingNextPage => this is AsyncData && this is AsyncLoadingNext;
|
||||
@ -15,7 +16,7 @@ extension PaginationExtension<T> on AsyncValue<T> {
|
||||
|
||||
mixin MetadataPluginMixin<K>
|
||||
// ignore: invalid_use_of_internal_member
|
||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
||||
on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
|
||||
Future<MetadataPlugin> get metadataPlugin async {
|
||||
final plugin = await ref.read(metadataPluginProvider.future);
|
||||
|
||||
@ -25,6 +26,11 @@ mixin MetadataPluginMixin<K>
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
Future<OpaqueSender> get mpscTx async {
|
||||
final plugin = await metadataPlugin;
|
||||
return plugin.sender;
|
||||
}
|
||||
}
|
||||
|
||||
extension AutoDisposeAsyncNotifierCacheFor
|
||||
|
||||
@ -7,9 +7,9 @@ import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
abstract class FamilyPaginatedAsyncNotifier<K, A>
|
||||
extends FamilyAsyncNotifier<SpotubePaginationResponseObject<K>, A>
|
||||
extends FamilyAsyncNotifier<SpotubeFlattenedPaginationObject<K>, A>
|
||||
with MetadataPluginMixin<K> {
|
||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
||||
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
@ -74,9 +74,9 @@ abstract class FamilyPaginatedAsyncNotifier<K, A>
|
||||
}
|
||||
|
||||
abstract class AutoDisposeFamilyPaginatedAsyncNotifier<K, A>
|
||||
extends AutoDisposeFamilyAsyncNotifier<SpotubePaginationResponseObject<K>,
|
||||
extends AutoDisposeFamilyAsyncNotifier<SpotubeFlattenedPaginationObject<K>,
|
||||
A> with MetadataPluginMixin<K> {
|
||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
||||
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
|
||||
@ -10,8 +10,8 @@ import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
mixin PaginatedAsyncNotifierMixin<K>
|
||||
// ignore: invalid_use_of_internal_member
|
||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
||||
on AsyncNotifierBase<SpotubeFlattenedPaginationObject<K>> {
|
||||
Future<SpotubeFlattenedPaginationObject<K>> fetch(int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
@ -75,9 +75,9 @@ mixin PaginatedAsyncNotifierMixin<K>
|
||||
}
|
||||
|
||||
abstract class PaginatedAsyncNotifier<K>
|
||||
extends AsyncNotifier<SpotubePaginationResponseObject<K>>
|
||||
extends AsyncNotifier<SpotubeFlattenedPaginationObject<K>>
|
||||
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
||||
|
||||
abstract class AutoDisposePaginatedAsyncNotifier<K>
|
||||
extends AutoDisposeAsyncNotifier<SpotubePaginationResponseObject<K>>
|
||||
extends AutoDisposeAsyncNotifier<SpotubeFlattenedPaginationObject<K>>
|
||||
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
||||
|
||||
@ -10,8 +10,8 @@ import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
||||
final StreamController<SpotubeTrackObject> _scrobbleController =
|
||||
StreamController<SpotubeTrackObject>.broadcast();
|
||||
final _scrobbleController =
|
||||
StreamController<SpotubeFullTrackObject>.broadcast();
|
||||
@override
|
||||
build() async {
|
||||
final database = ref.watch(databaseProvider);
|
||||
@ -107,18 +107,18 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
||||
await database.delete(database.scrobblerTable).go();
|
||||
}
|
||||
|
||||
void scrobble(SpotubeTrackObject track) {
|
||||
void scrobble(SpotubeFullTrackObject track) {
|
||||
_scrobbleController.add(track);
|
||||
}
|
||||
|
||||
Future<void> love(SpotubeTrackObject track) async {
|
||||
Future<void> love(SpotubeFullTrackObject track) async {
|
||||
await state.asData?.value?.track.love(
|
||||
artist: track.artists.asString(),
|
||||
track: track.name,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> unlove(SpotubeTrackObject track) async {
|
||||
Future<void> unlove(SpotubeFullTrackObject track) async {
|
||||
await state.asData?.value?.track.unLove(
|
||||
artist: track.artists.asString(),
|
||||
track: track.name,
|
||||
|
||||
@ -16,7 +16,7 @@ final activeTrackSourcesProvider = FutureProvider<
|
||||
return null;
|
||||
}
|
||||
|
||||
if (audioPlayerState.activeTrack is SpotubeLocalTrackObject) {
|
||||
if (audioPlayerState.activeTrack is SpotubeTrackObject_Local) {
|
||||
return (
|
||||
source: null,
|
||||
notifier: null,
|
||||
@ -26,12 +26,12 @@ final activeTrackSourcesProvider = FutureProvider<
|
||||
|
||||
final sourcedTrack = await ref.watch(
|
||||
sourcedTrackProvider(
|
||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
||||
audioPlayerState.activeTrack?.field0 as SpotubeFullTrackObject,
|
||||
).future,
|
||||
);
|
||||
final sourcedTrackNotifier = ref.watch(
|
||||
sourcedTrackProvider(
|
||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
||||
audioPlayerState.activeTrack?.field0 as SpotubeFullTrackObject,
|
||||
).notifier,
|
||||
);
|
||||
|
||||
|
||||
201
lib/provider/server/libs/eventsource_publisher.dart
Normal file
201
lib/provider/server/libs/eventsource_publisher.dart
Normal file
@ -0,0 +1,201 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import "package:logging/logging.dart" as log;
|
||||
|
||||
/// Just a simple [Sink] implementation that proxies the [add] and [close]
|
||||
/// methods.
|
||||
class ProxySink<T> implements Sink<T> {
|
||||
void Function(T) onAdd;
|
||||
void Function() onClose;
|
||||
ProxySink({required this.onAdd, required this.onClose});
|
||||
@override
|
||||
void add(t) => onAdd(t);
|
||||
@override
|
||||
void close() => onClose();
|
||||
}
|
||||
|
||||
class EventCache {
|
||||
final int? cacheCapacity;
|
||||
final bool comparableIds;
|
||||
final Map<String, List<Event>> _caches = {};
|
||||
|
||||
EventCache({this.cacheCapacity, this.comparableIds = true});
|
||||
|
||||
void replay(Sink<Event> sink, String lastEventId, [String channel = ""]) {
|
||||
List<Event>? cache = _caches[channel];
|
||||
if (cache == null || cache.isEmpty) {
|
||||
// nothing to replay
|
||||
return;
|
||||
}
|
||||
// find the location of lastEventId in the queue
|
||||
int index;
|
||||
if (comparableIds) {
|
||||
// if comparableIds, we can use binary search
|
||||
index = binarySearch(cache, lastEventId);
|
||||
} else {
|
||||
// otherwise, we starts from the last one and look one by one
|
||||
index = cache.length - 1;
|
||||
while (index > 0 && cache[index].id != lastEventId) {
|
||||
index--;
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
// add them all to the sink
|
||||
cache.sublist(index).forEach(sink.add);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new [Event] to the cache(s) of the specified channel(s).
|
||||
/// Please note that we assume events are added with increasing values of
|
||||
/// [Event.id].
|
||||
void add(Event event, [Iterable<String> channels = const [""]]) {
|
||||
for (String channel in channels) {
|
||||
List<Event> cache = _caches.putIfAbsent(channel, () => []);
|
||||
if (cacheCapacity != null && cache.length >= cacheCapacity!) {
|
||||
cache.removeAt(0);
|
||||
}
|
||||
cache.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
void clear([Iterable<String> channels = const [""]]) {
|
||||
channels.forEach(_caches.remove);
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
_caches.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class Event implements Comparable<Event> {
|
||||
/// An identifier that can be used to allow a client to replay
|
||||
/// missed Events by returning the Last-Event-Id header.
|
||||
/// Return empty string if not required.
|
||||
String? id;
|
||||
|
||||
/// The name of the event. Return empty string if not required.
|
||||
String? event;
|
||||
|
||||
/// The payload of the event.
|
||||
String? data;
|
||||
|
||||
Event({this.id, this.event, this.data});
|
||||
|
||||
Event.message({this.id, this.data}) : event = "message";
|
||||
|
||||
@override
|
||||
int compareTo(Event other) => id!.compareTo(other.id!);
|
||||
}
|
||||
|
||||
/// An EventSource publisher. It can manage different channels of events.
|
||||
/// This class forms the backbone of an EventSource server. To actually serve
|
||||
/// a web server, use this together with [shelf_eventsource] or another server
|
||||
/// implementation.
|
||||
class EventSourcePublisher implements Sink<Event> {
|
||||
log.Logger? logger;
|
||||
EventCache? _cache;
|
||||
|
||||
/// Create a new EventSource server.
|
||||
///
|
||||
/// When using a cache, for efficient replaying, it is advisable to use a
|
||||
/// custom Event implementation that overrides the `Event.compareTo` method.
|
||||
/// F.e. if integer events are used, sorting should be done on integers and
|
||||
/// not on the string representations of them.
|
||||
/// If your Event's id properties are not incremental using
|
||||
/// [Comparable.compare], set [comparableIds] to false.
|
||||
EventSourcePublisher({
|
||||
int cacheCapacity = 0,
|
||||
bool comparableIds = false,
|
||||
bool enableLogging = true,
|
||||
}) {
|
||||
if (cacheCapacity > 0) {
|
||||
_cache = EventCache(cacheCapacity: cacheCapacity);
|
||||
}
|
||||
if (enableLogging) {
|
||||
logger = log.Logger("EventSourceServer");
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, List<ProxySink>> _subsByChannel = {};
|
||||
|
||||
/// Creates a Sink for the specified channel.
|
||||
/// The `add` and `remove` methods of this channel are equivalent to the
|
||||
/// respective methods of this class with the specific channel passed along.
|
||||
Sink<Event> channel(String channel) => ProxySink(
|
||||
onAdd: (e) => add(e, channels: [channel]),
|
||||
onClose: () => close(channels: [channel]));
|
||||
|
||||
/// Add a publication to the specified channels.
|
||||
/// By default, only adds to the default channel.
|
||||
@override
|
||||
void add(Event event, {Iterable<String> channels = const [""]}) {
|
||||
for (String channel in channels) {
|
||||
List<ProxySink>? subs = _subsByChannel[channel];
|
||||
if (subs == null) {
|
||||
continue;
|
||||
}
|
||||
_logFiner(
|
||||
"Sending event on channel $channel to ${subs.length} subscribers.");
|
||||
for (var sub in subs) {
|
||||
sub.add(event);
|
||||
}
|
||||
}
|
||||
_cache?.add(event, channels);
|
||||
}
|
||||
|
||||
/// Close the specified channels.
|
||||
/// All the connections with the subscribers to this channels will be closed.
|
||||
/// By default only closes the default channel.
|
||||
@override
|
||||
void close({Iterable<String> channels = const [""]}) {
|
||||
for (String channel in channels) {
|
||||
List<ProxySink>? subs = _subsByChannel[channel];
|
||||
if (subs == null) {
|
||||
continue;
|
||||
}
|
||||
_logInfo("Closing channel $channel with ${subs.length} subscribers.");
|
||||
for (var sub in subs) {
|
||||
sub.close();
|
||||
}
|
||||
}
|
||||
_cache?.clear(channels);
|
||||
}
|
||||
|
||||
/// Close all the open channels.
|
||||
void closeAllChannels() => close(channels: _subsByChannel.keys);
|
||||
|
||||
/// Initialize a new subscription and replay when possible.
|
||||
/// Should not be used by the user directly.
|
||||
void newSubscription({
|
||||
required void Function(Event) onEvent,
|
||||
required void Function() onClose,
|
||||
required String channel,
|
||||
String? lastEventId,
|
||||
}) {
|
||||
_logFine("New subscriber on channel $channel.");
|
||||
// create a sink for the subscription
|
||||
ProxySink<Event> sub = ProxySink(onAdd: onEvent, onClose: onClose);
|
||||
// save the subscription
|
||||
_subsByChannel.putIfAbsent(channel, () => []).add(sub);
|
||||
// replay past events
|
||||
if (_cache != null && lastEventId != null) {
|
||||
scheduleMicrotask(() {
|
||||
_logFine("Replaying events on channel $channel from id $lastEventId.");
|
||||
_cache!.replay(sub, lastEventId, channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _logInfo(message) {
|
||||
logger?.log(log.Level.INFO, message);
|
||||
}
|
||||
|
||||
void _logFine(message) {
|
||||
logger?.log(log.Level.FINE, message);
|
||||
}
|
||||
|
||||
void _logFiner(message) {
|
||||
logger?.log(log.Level.FINER, message);
|
||||
}
|
||||
}
|
||||
106
lib/provider/server/libs/shelf_eventsource.dart
Normal file
106
lib/provider/server/libs/shelf_eventsource.dart
Normal file
@ -0,0 +1,106 @@
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "package:shelf/shelf.dart";
|
||||
import "package:spotube/provider/server/libs/eventsource_publisher.dart";
|
||||
|
||||
class EventSourceEncoder extends Converter<Event, List<int>> {
|
||||
final bool compressed;
|
||||
|
||||
const EventSourceEncoder({this.compressed = false});
|
||||
|
||||
static final Map<String, Function> _fields = {
|
||||
"id: ": (e) => e.id,
|
||||
"event: ": (e) => e.event,
|
||||
"data: ": (e) => e.data,
|
||||
};
|
||||
|
||||
@override
|
||||
List<int> convert(Event event) {
|
||||
String payload = convertToString(event);
|
||||
List<int> bytes = utf8.encode(payload);
|
||||
if (compressed) {
|
||||
bytes = gzip.encode(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
String convertToString(Event event) {
|
||||
String payload = "";
|
||||
for (String prefix in _fields.keys) {
|
||||
String? value = _fields[prefix]?.call(event);
|
||||
if (value == null || value.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
// multi-line values need the field prefix on every line
|
||||
value = value.replaceAll("\n", "\n$prefix");
|
||||
payload += "$prefix$value\n";
|
||||
}
|
||||
payload += "\n";
|
||||
return payload;
|
||||
}
|
||||
|
||||
@override
|
||||
Sink<Event> startChunkedConversion(Sink<List<int>> sink) {
|
||||
Sink<dynamic> inputSink = sink;
|
||||
if (compressed) {
|
||||
inputSink =
|
||||
gzip.encoder.startChunkedConversion(inputSink as Sink<List<int>>);
|
||||
}
|
||||
inputSink =
|
||||
utf8.encoder.startChunkedConversion(inputSink as Sink<List<int>>);
|
||||
return new ProxySink(
|
||||
onAdd: (Event event) => inputSink.add(convertToString(event)),
|
||||
onClose: () => inputSink.close());
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a shelf handler for the specified channel.
|
||||
/// This handler can be passed to the [shelf.serve] method.
|
||||
Handler eventSourceHandler(
|
||||
EventSourcePublisher publisher, {
|
||||
String channel = "",
|
||||
bool gzip = false,
|
||||
}) {
|
||||
// define the handler
|
||||
Response shelfHandler(Request request) {
|
||||
if (request.method != "GET") {
|
||||
return Response.notFound(null);
|
||||
}
|
||||
|
||||
if (!request.canHijack) {
|
||||
throw ArgumentError("eventSourceHandler may only be used with a "
|
||||
"server that supports request hijacking.");
|
||||
}
|
||||
|
||||
// set content encoding to gzip if we allow it and the request supports it
|
||||
bool useGzip =
|
||||
gzip && (request.headers["Accept-Encoding"] ?? "").contains("gzip");
|
||||
|
||||
// hijack the raw underlying channel
|
||||
request.hijack((untypedChannel) {
|
||||
var socketChannel = (untypedChannel).cast<List<int>>();
|
||||
// create a regular UTF8 sink to write headers
|
||||
var sink = utf8.encoder.startChunkedConversion(socketChannel.sink);
|
||||
// write headers
|
||||
sink.add("HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/event-stream; charset=utf-8\r\n"
|
||||
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
|
||||
"Connection: keep-alive\r\n");
|
||||
if (useGzip) sink.add("Content-Encoding: gzip\r\n");
|
||||
sink.add("\r\n");
|
||||
|
||||
// create encoder for this connection
|
||||
var encodedSink = EventSourceEncoder(compressed: useGzip)
|
||||
.startChunkedConversion(socketChannel.sink);
|
||||
|
||||
// initialize the new subscription
|
||||
publisher.newSubscription(
|
||||
onEvent: encodedSink.add,
|
||||
onClose: encodedSink.close,
|
||||
channel: channel,
|
||||
lastEventId: request.headers["Last-Event-ID"]);
|
||||
});
|
||||
}
|
||||
|
||||
return shelfHandler;
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
import 'package:spotube/provider/server/libs/shelf_eventsource.dart';
|
||||
import 'package:spotube/provider/server/routes/connect.dart';
|
||||
import 'package:spotube/provider/server/routes/playback.dart';
|
||||
import 'package:spotube/provider/server/routes/plugin_apis/form.dart';
|
||||
import 'package:spotube/provider/server/routes/plugin_apis/path_provider.dart';
|
||||
import 'package:spotube/provider/server/routes/plugin_apis/webview.dart';
|
||||
import 'package:spotube/provider/server/routes/plugin_apis/yt_engine.dart';
|
||||
import 'package:spotube/provider/server/sse_publisher.dart';
|
||||
|
||||
Handler pluginApiAuthMiddleware(Handler handler) {
|
||||
return (Request request) {
|
||||
@ -25,6 +26,8 @@ final serverRouterProvider = Provider((ref) {
|
||||
final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
|
||||
final formRoutes = ref.watch(serverFormRoutesProvider);
|
||||
final ytEngineRoutes = ref.watch(serverYTEngineRoutesProvider);
|
||||
final publisher = ref.watch(ssePublisherProvider);
|
||||
final sseHandler = eventSourceHandler(publisher);
|
||||
|
||||
final router = Router();
|
||||
|
||||
@ -42,8 +45,8 @@ final serverRouterProvider = Provider((ref) {
|
||||
pluginApiAuthMiddleware(webviewRoutes.postCreateWebview),
|
||||
);
|
||||
router.get(
|
||||
"/plugin-api/webview/<uid>/on-url-request",
|
||||
pluginApiAuthMiddleware(webviewRoutes.getOnUrlRequestStream),
|
||||
"/plugin-api/webview/events",
|
||||
pluginApiAuthMiddleware(sseHandler),
|
||||
);
|
||||
router.post(
|
||||
"/plugin-api/webview/open",
|
||||
@ -61,10 +64,6 @@ final serverRouterProvider = Provider((ref) {
|
||||
"/plugin-api/form/show",
|
||||
pluginApiAuthMiddleware(formRoutes.showForm),
|
||||
);
|
||||
router.get(
|
||||
"/plugin/localstorage/directories",
|
||||
pluginApiAuthMiddleware(ServerPathProviderRoutes.getDirectories),
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/plugin-api/yt-engine/search",
|
||||
|
||||
@ -161,7 +161,7 @@ class ServerConnectRoutes {
|
||||
|
||||
event.onLoad((event) async {
|
||||
await audioPlayerNotifier.load(
|
||||
event.data.tracks.cast<SpotubeFullTrackObject>().toList(),
|
||||
event.data.tracks.cast<SpotubeTrackObject>().toList(),
|
||||
autoPlay: true,
|
||||
initialIndex: event.data.initialIndex ?? 0,
|
||||
);
|
||||
|
||||
@ -70,8 +70,9 @@ class ServerPlaybackRoutes {
|
||||
final sourcedTrack = activeSourcedTrack?.track.id == track.id
|
||||
? activeSourcedTrack?.source
|
||||
: await ref.read(
|
||||
sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject)
|
||||
.future,
|
||||
sourcedTrackProvider(
|
||||
spotubeMedia.track.field0 as SpotubeFullTrackObject,
|
||||
).future,
|
||||
);
|
||||
|
||||
return sourcedTrack;
|
||||
@ -258,7 +259,7 @@ class ServerPlaybackRoutes {
|
||||
|
||||
await MetadataGod.writeMetadata(
|
||||
file: trackCacheFile.path,
|
||||
metadata: track.query.toMetadata(
|
||||
metadata: SpotubeTrackObject.full(track.query).toMetadata(
|
||||
imageBytes: imageBytes,
|
||||
fileLength: fileLength,
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user