mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: move from Track to SpotubeTrackObject and use TrackSources object for providers
This commit is contained in:
parent
b979a6ede9
commit
4e6db8b9e1
@ -73,8 +73,8 @@ extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
|
|||||||
Future<List<Track>> asTracks(AlbumSimple album, ref) async {
|
Future<List<Track>> asTracks(AlbumSimple album, ref) async {
|
||||||
try {
|
try {
|
||||||
final spotify = ref.read(spotifyProvider);
|
final spotify = ref.read(spotifyProvider);
|
||||||
final tracks = await spotify.invoke(
|
final tracks = await spotify.invoke((api) =>
|
||||||
(api) => api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
|
api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
|
||||||
return tracks.toList();
|
return tracks.toList();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
// Ignore errors and create the track locally
|
// Ignore errors and create the track locally
|
||||||
@ -105,9 +105,3 @@ extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TracksToMediaExtension on Iterable<Track> {
|
|
||||||
List<SpotubeMedia> asMediaList() {
|
|
||||||
return map((track) => SpotubeMedia(track)).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@ -8,83 +9,85 @@ import 'package:spotube/provider/spotify/spotify.dart';
|
|||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
// TODO: Implement endless playback functionality
|
||||||
void useEndlessPlayback(WidgetRef ref) {
|
void useEndlessPlayback(WidgetRef ref) {
|
||||||
final auth = ref.watch(authenticationProvider);
|
// final auth = ref.watch(authenticationProvider);
|
||||||
final playback = ref.watch(audioPlayerProvider.notifier);
|
// final playback = ref.watch(audioPlayerProvider.notifier);
|
||||||
final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist));
|
// final audioPlayerState = ref.watch(audioPlayerProvider);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
// final spotify = ref.watch(spotifyProvider);
|
||||||
final endlessPlayback =
|
// final endlessPlayback =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
// ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
||||||
|
|
||||||
useEffect(
|
// useEffect(
|
||||||
() {
|
// () {
|
||||||
if (!endlessPlayback || auth.asData?.value == null) return null;
|
// if (!endlessPlayback || auth.asData?.value == null) return null;
|
||||||
|
|
||||||
void listener(int index) async {
|
// void listener(int index) async {
|
||||||
try {
|
// try {
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
// final playlist = ref.read(audioPlayerProvider);
|
||||||
if (index != playlist.tracks.length - 1) return;
|
// if (index != playlist.tracks.length - 1) return;
|
||||||
|
|
||||||
final track = playlist.tracks.last;
|
// final track = playlist.tracks.last;
|
||||||
|
|
||||||
final query = "${track.name} Radio";
|
// final query = "${track.name} Radio";
|
||||||
final pages = await spotify.invoke((api) =>
|
// final pages = await spotify.invoke((api) =>
|
||||||
api.search.get(query, types: [SearchType.playlist]).first());
|
// api.search.get(query, types: [SearchType.playlist]).first());
|
||||||
|
|
||||||
final radios = pages
|
// final radios = pages
|
||||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
// .expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||||
.toList()
|
// .toList()
|
||||||
.cast<PlaylistSimple>();
|
// .cast<PlaylistSimple>();
|
||||||
|
|
||||||
final artists = track.artists!.map((e) => e.name);
|
// final artists = track.artists.map((e) => e.name);
|
||||||
|
|
||||||
final radio = radios.firstWhere(
|
// final radio = radios.firstWhere(
|
||||||
(e) {
|
// (e) {
|
||||||
final validPlaylists =
|
// final validPlaylists =
|
||||||
artists.where((a) => e.description!.contains(a!));
|
// artists.where((a) => e.description!.contains(a));
|
||||||
return e.name == "${track.name} Radio" &&
|
// return e.name == "${track.name} Radio" &&
|
||||||
(validPlaylists.length >= 2 ||
|
// (validPlaylists.length >= 2 ||
|
||||||
validPlaylists.length == artists.length) &&
|
// validPlaylists.length == artists.length) &&
|
||||||
e.owner?.displayName != "Spotify";
|
// e.owner?.displayName != "Spotify";
|
||||||
},
|
// },
|
||||||
orElse: () => radios.first,
|
// orElse: () => radios.first,
|
||||||
);
|
// );
|
||||||
|
|
||||||
final tracks = await spotify.invoke(
|
// final tracks =
|
||||||
(api) => api.playlists.getTracksByPlaylistId(radio.id!).all());
|
// ref.read(metadataPluginPlaylistTracksProvider(radio.id!));
|
||||||
|
|
||||||
await playback.addTracks(
|
// await playback.addTracks(
|
||||||
tracks.toList()
|
// tracks.toList()
|
||||||
..removeWhere((e) {
|
// ..removeWhere((e) {
|
||||||
final playlist = ref.read(audioPlayerProvider);
|
// final playlist = ref.read(audioPlayerProvider);
|
||||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
// final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||||
return e.id == track.id || isDuplicate;
|
// return e.id == track.id || isDuplicate;
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
} catch (e, stack) {
|
// } catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
// AppLogger.reportError(e, stack);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Sometimes user can change settings for which the currentIndexChanged
|
// // Sometimes user can change settings for which the currentIndexChanged
|
||||||
// might not be called. So we need to check if the current track is the
|
// // might not be called. So we need to check if the current track is the
|
||||||
// last track and if it is then we need to call the listener manually.
|
// // last track and if it is then we need to call the listener manually.
|
||||||
if (playlist.index == playlist.medias.length - 1 &&
|
// if (audioPlayerState.currentIndex == audioPlayerState.tracks.length - 1 &&
|
||||||
audioPlayer.isPlaying) {
|
// audioPlayer.isPlaying) {
|
||||||
listener(playlist.index);
|
// listener(audioPlayerState.currentIndex);
|
||||||
}
|
// }
|
||||||
|
|
||||||
final subscription =
|
// final subscription =
|
||||||
audioPlayer.currentIndexChangedStream.listen(listener);
|
// audioPlayer.currentIndexChangedStream.listen(listener);
|
||||||
|
|
||||||
return subscription.cancel;
|
// return subscription.cancel;
|
||||||
},
|
// },
|
||||||
[
|
// [
|
||||||
spotify,
|
// spotify,
|
||||||
playback,
|
// playback,
|
||||||
playlist.medias,
|
// audioPlayerState.tracks,
|
||||||
endlessPlayback,
|
// audioPlayerState.currentIndex,
|
||||||
auth,
|
// endlessPlayback,
|
||||||
],
|
// auth,
|
||||||
);
|
// ],
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
|
|
||||||
part 'connect.freezed.dart';
|
part 'connect.freezed.dart';
|
||||||
|
@ -33,49 +33,36 @@ WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$WebSocketLoadEventData {
|
mixin _$WebSocketLoadEventData {
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
List<dynamic> get tracks => throw _privateConstructorUsedError;
|
||||||
List<Track> get tracks => throw _privateConstructorUsedError;
|
|
||||||
Object? get collection => throw _privateConstructorUsedError;
|
Object? get collection => throw _privateConstructorUsedError;
|
||||||
int? get initialIndex => throw _privateConstructorUsedError;
|
int? get initialIndex => throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(
|
required TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)
|
|
||||||
playlist,
|
playlist,
|
||||||
required TResult Function(
|
required TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)
|
|
||||||
album,
|
album,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(
|
TResult? Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)?
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
playlist,
|
playlist,
|
||||||
TResult? Function(
|
TResult? Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)?
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
album,
|
album,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(
|
TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)?
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
playlist,
|
playlist,
|
||||||
TResult Function(
|
TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)?
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
album,
|
album,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) =>
|
}) =>
|
||||||
@ -116,9 +103,7 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> {
|
|||||||
$Res Function(WebSocketLoadEventData) then) =
|
$Res Function(WebSocketLoadEventData) then) =
|
||||||
_$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>;
|
_$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call({List<dynamic> tracks, int? initialIndex});
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
|
||||||
int? initialIndex});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -144,7 +129,7 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
|||||||
tracks: null == tracks
|
tracks: null == tracks
|
||||||
? _value.tracks
|
? _value.tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<Track>,
|
as List<dynamic>,
|
||||||
initialIndex: freezed == initialIndex
|
initialIndex: freezed == initialIndex
|
||||||
? _value.initialIndex
|
? _value.initialIndex
|
||||||
: initialIndex // ignore: cast_nullable_to_non_nullable
|
: initialIndex // ignore: cast_nullable_to_non_nullable
|
||||||
@ -163,9 +148,11 @@ abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
{List<SpotubeFullTrackObject> tracks,
|
||||||
PlaylistSimple? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
|
|
||||||
|
$SpotubeSimplePlaylistObjectCopyWith<$Res>? get collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -191,17 +178,32 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
|
|||||||
tracks: null == tracks
|
tracks: null == tracks
|
||||||
? _value._tracks
|
? _value._tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<Track>,
|
as List<SpotubeFullTrackObject>,
|
||||||
collection: freezed == collection
|
collection: freezed == collection
|
||||||
? _value.collection
|
? _value.collection
|
||||||
: collection // ignore: cast_nullable_to_non_nullable
|
: collection // ignore: cast_nullable_to_non_nullable
|
||||||
as PlaylistSimple?,
|
as SpotubeSimplePlaylistObject?,
|
||||||
initialIndex: freezed == initialIndex
|
initialIndex: freezed == initialIndex
|
||||||
? _value.initialIndex
|
? _value.initialIndex
|
||||||
: initialIndex // ignore: cast_nullable_to_non_nullable
|
: initialIndex // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of WebSocketLoadEventData
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SpotubeSimplePlaylistObjectCopyWith<$Res>? get collection {
|
||||||
|
if (_value.collection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SpotubeSimplePlaylistObjectCopyWith<$Res>(_value.collection!,
|
||||||
|
(value) {
|
||||||
|
return _then(_value.copyWith(collection: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -209,8 +211,7 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
|
|||||||
class _$WebSocketLoadEventDataPlaylistImpl
|
class _$WebSocketLoadEventDataPlaylistImpl
|
||||||
extends WebSocketLoadEventDataPlaylist {
|
extends WebSocketLoadEventDataPlaylist {
|
||||||
_$WebSocketLoadEventDataPlaylistImpl(
|
_$WebSocketLoadEventDataPlaylistImpl(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
{required final List<SpotubeFullTrackObject> tracks,
|
||||||
required final List<Track> tracks,
|
|
||||||
this.collection,
|
this.collection,
|
||||||
this.initialIndex,
|
this.initialIndex,
|
||||||
final String? $type})
|
final String? $type})
|
||||||
@ -222,17 +223,16 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
Map<String, dynamic> json) =>
|
Map<String, dynamic> json) =>
|
||||||
_$$WebSocketLoadEventDataPlaylistImplFromJson(json);
|
_$$WebSocketLoadEventDataPlaylistImplFromJson(json);
|
||||||
|
|
||||||
final List<Track> _tracks;
|
final List<SpotubeFullTrackObject> _tracks;
|
||||||
@override
|
@override
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
List<SpotubeFullTrackObject> get tracks {
|
||||||
List<Track> get tracks {
|
|
||||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_tracks);
|
return EqualUnmodifiableListView(_tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final PlaylistSimple? collection;
|
final SpotubeSimplePlaylistObject? collection;
|
||||||
@override
|
@override
|
||||||
final int? initialIndex;
|
final int? initialIndex;
|
||||||
|
|
||||||
@ -274,15 +274,11 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(
|
required TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)
|
|
||||||
playlist,
|
playlist,
|
||||||
required TResult Function(
|
required TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)
|
|
||||||
album,
|
album,
|
||||||
}) {
|
}) {
|
||||||
return playlist(tracks, collection, initialIndex);
|
return playlist(tracks, collection, initialIndex);
|
||||||
@ -291,15 +287,11 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(
|
TResult? Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)?
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
playlist,
|
playlist,
|
||||||
TResult? Function(
|
TResult? Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)?
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
album,
|
album,
|
||||||
}) {
|
}) {
|
||||||
return playlist?.call(tracks, collection, initialIndex);
|
return playlist?.call(tracks, collection, initialIndex);
|
||||||
@ -308,15 +300,11 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(
|
TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)?
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
playlist,
|
playlist,
|
||||||
TResult Function(
|
TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)?
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
album,
|
album,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
@ -367,9 +355,8 @@ class _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
|
|
||||||
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
||||||
factory WebSocketLoadEventDataPlaylist(
|
factory WebSocketLoadEventDataPlaylist(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
{required final List<SpotubeFullTrackObject> tracks,
|
||||||
required final List<Track> tracks,
|
final SpotubeSimplePlaylistObject? collection,
|
||||||
final PlaylistSimple? collection,
|
|
||||||
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
|
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
|
||||||
WebSocketLoadEventDataPlaylist._() : super._();
|
WebSocketLoadEventDataPlaylist._() : super._();
|
||||||
|
|
||||||
@ -377,10 +364,9 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
|||||||
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
|
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
List<SpotubeFullTrackObject> get tracks;
|
||||||
List<Track> get tracks;
|
|
||||||
@override
|
@override
|
||||||
PlaylistSimple? get collection;
|
SpotubeSimplePlaylistObject? get collection;
|
||||||
@override
|
@override
|
||||||
int? get initialIndex;
|
int? get initialIndex;
|
||||||
|
|
||||||
@ -403,9 +389,11 @@ abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
{List<SpotubeFullTrackObject> tracks,
|
||||||
AlbumSimple? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
|
|
||||||
|
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -431,25 +419,38 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
|
|||||||
tracks: null == tracks
|
tracks: null == tracks
|
||||||
? _value._tracks
|
? _value._tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<Track>,
|
as List<SpotubeFullTrackObject>,
|
||||||
collection: freezed == collection
|
collection: freezed == collection
|
||||||
? _value.collection
|
? _value.collection
|
||||||
: collection // ignore: cast_nullable_to_non_nullable
|
: collection // ignore: cast_nullable_to_non_nullable
|
||||||
as AlbumSimple?,
|
as SpotubeSimpleAlbumObject?,
|
||||||
initialIndex: freezed == initialIndex
|
initialIndex: freezed == initialIndex
|
||||||
? _value.initialIndex
|
? _value.initialIndex
|
||||||
: initialIndex // ignore: cast_nullable_to_non_nullable
|
: initialIndex // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of WebSocketLoadEventData
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get collection {
|
||||||
|
if (_value.collection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.collection!, (value) {
|
||||||
|
return _then(_value.copyWith(collection: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||||
_$WebSocketLoadEventDataAlbumImpl(
|
_$WebSocketLoadEventDataAlbumImpl(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
{required final List<SpotubeFullTrackObject> tracks,
|
||||||
required final List<Track> tracks,
|
|
||||||
this.collection,
|
this.collection,
|
||||||
this.initialIndex,
|
this.initialIndex,
|
||||||
final String? $type})
|
final String? $type})
|
||||||
@ -461,17 +462,16 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
Map<String, dynamic> json) =>
|
Map<String, dynamic> json) =>
|
||||||
_$$WebSocketLoadEventDataAlbumImplFromJson(json);
|
_$$WebSocketLoadEventDataAlbumImplFromJson(json);
|
||||||
|
|
||||||
final List<Track> _tracks;
|
final List<SpotubeFullTrackObject> _tracks;
|
||||||
@override
|
@override
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
List<SpotubeFullTrackObject> get tracks {
|
||||||
List<Track> get tracks {
|
|
||||||
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_tracks);
|
return EqualUnmodifiableListView(_tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final AlbumSimple? collection;
|
final SpotubeSimpleAlbumObject? collection;
|
||||||
@override
|
@override
|
||||||
final int? initialIndex;
|
final int? initialIndex;
|
||||||
|
|
||||||
@ -512,15 +512,11 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(
|
required TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)
|
|
||||||
playlist,
|
playlist,
|
||||||
required TResult Function(
|
required TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)
|
|
||||||
album,
|
album,
|
||||||
}) {
|
}) {
|
||||||
return album(tracks, collection, initialIndex);
|
return album(tracks, collection, initialIndex);
|
||||||
@ -529,15 +525,11 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(
|
TResult? Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)?
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
playlist,
|
playlist,
|
||||||
TResult? Function(
|
TResult? Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)?
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
album,
|
album,
|
||||||
}) {
|
}) {
|
||||||
return album?.call(tracks, collection, initialIndex);
|
return album?.call(tracks, collection, initialIndex);
|
||||||
@ -546,15 +538,11 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(
|
TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimplePlaylistObject? collection, int? initialIndex)?
|
||||||
PlaylistSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
playlist,
|
playlist,
|
||||||
TResult Function(
|
TResult Function(List<SpotubeFullTrackObject> tracks,
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
SpotubeSimpleAlbumObject? collection, int? initialIndex)?
|
||||||
AlbumSimple? collection,
|
|
||||||
int? initialIndex)?
|
|
||||||
album,
|
album,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
@ -605,9 +593,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
|||||||
|
|
||||||
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
||||||
factory WebSocketLoadEventDataAlbum(
|
factory WebSocketLoadEventDataAlbum(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
{required final List<SpotubeFullTrackObject> tracks,
|
||||||
required final List<Track> tracks,
|
final SpotubeSimpleAlbumObject? collection,
|
||||||
final AlbumSimple? collection,
|
|
||||||
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
|
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
|
||||||
WebSocketLoadEventDataAlbum._() : super._();
|
WebSocketLoadEventDataAlbum._() : super._();
|
||||||
|
|
||||||
@ -615,10 +602,9 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
|||||||
_$WebSocketLoadEventDataAlbumImpl.fromJson;
|
_$WebSocketLoadEventDataAlbumImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
List<SpotubeFullTrackObject> get tracks;
|
||||||
List<Track> get tracks;
|
|
||||||
@override
|
@override
|
||||||
AlbumSimple? get collection;
|
SpotubeSimpleAlbumObject? get collection;
|
||||||
@override
|
@override
|
||||||
int? get initialIndex;
|
int? get initialIndex;
|
||||||
|
|
||||||
|
@ -10,11 +10,12 @@ _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
_$$WebSocketLoadEventDataPlaylistImplFromJson(Map json) =>
|
_$$WebSocketLoadEventDataPlaylistImplFromJson(Map json) =>
|
||||||
_$WebSocketLoadEventDataPlaylistImpl(
|
_$WebSocketLoadEventDataPlaylistImpl(
|
||||||
tracks: (json['tracks'] as List<dynamic>)
|
tracks: (json['tracks'] as List<dynamic>)
|
||||||
.map((e) => Track.fromJson(Map<String, dynamic>.from(e as Map)))
|
.map((e) => SpotubeFullTrackObject.fromJson(
|
||||||
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
collection: json['collection'] == null
|
collection: json['collection'] == null
|
||||||
? null
|
? null
|
||||||
: PlaylistSimple.fromJson(
|
: SpotubeSimplePlaylistObject.fromJson(
|
||||||
Map<String, dynamic>.from(json['collection'] as Map)),
|
Map<String, dynamic>.from(json['collection'] as Map)),
|
||||||
initialIndex: (json['initialIndex'] as num?)?.toInt(),
|
initialIndex: (json['initialIndex'] as num?)?.toInt(),
|
||||||
$type: json['runtimeType'] as String?,
|
$type: json['runtimeType'] as String?,
|
||||||
@ -23,7 +24,7 @@ _$WebSocketLoadEventDataPlaylistImpl
|
|||||||
Map<String, dynamic> _$$WebSocketLoadEventDataPlaylistImplToJson(
|
Map<String, dynamic> _$$WebSocketLoadEventDataPlaylistImplToJson(
|
||||||
_$WebSocketLoadEventDataPlaylistImpl instance) =>
|
_$WebSocketLoadEventDataPlaylistImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'tracks': _tracksJson(instance.tracks),
|
'tracks': instance.tracks.map((e) => e.toJson()).toList(),
|
||||||
'collection': instance.collection?.toJson(),
|
'collection': instance.collection?.toJson(),
|
||||||
'initialIndex': instance.initialIndex,
|
'initialIndex': instance.initialIndex,
|
||||||
'runtimeType': instance.$type,
|
'runtimeType': instance.$type,
|
||||||
@ -33,11 +34,12 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
|
|||||||
Map json) =>
|
Map json) =>
|
||||||
_$WebSocketLoadEventDataAlbumImpl(
|
_$WebSocketLoadEventDataAlbumImpl(
|
||||||
tracks: (json['tracks'] as List<dynamic>)
|
tracks: (json['tracks'] as List<dynamic>)
|
||||||
.map((e) => Track.fromJson(Map<String, dynamic>.from(e as Map)))
|
.map((e) => SpotubeFullTrackObject.fromJson(
|
||||||
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
collection: json['collection'] == null
|
collection: json['collection'] == null
|
||||||
? null
|
? null
|
||||||
: AlbumSimple.fromJson(
|
: SpotubeSimpleAlbumObject.fromJson(
|
||||||
Map<String, dynamic>.from(json['collection'] as Map)),
|
Map<String, dynamic>.from(json['collection'] as Map)),
|
||||||
initialIndex: (json['initialIndex'] as num?)?.toInt(),
|
initialIndex: (json['initialIndex'] as num?)?.toInt(),
|
||||||
$type: json['runtimeType'] as String?,
|
$type: json['runtimeType'] as String?,
|
||||||
@ -46,7 +48,7 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
|
|||||||
Map<String, dynamic> _$$WebSocketLoadEventDataAlbumImplToJson(
|
Map<String, dynamic> _$$WebSocketLoadEventDataAlbumImplToJson(
|
||||||
_$WebSocketLoadEventDataAlbumImpl instance) =>
|
_$WebSocketLoadEventDataAlbumImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'tracks': _tracksJson(instance.tracks),
|
'tracks': instance.tracks.map((e) => e.toJson()).toList(),
|
||||||
'collection': instance.collection?.toJson(),
|
'collection': instance.collection?.toJson(),
|
||||||
'initialIndex': instance.initialIndex,
|
'initialIndex': instance.initialIndex,
|
||||||
'runtimeType': instance.$type,
|
'runtimeType': instance.$type,
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
part of 'connect.dart';
|
part of 'connect.dart';
|
||||||
|
|
||||||
List<Map<String, dynamic>> _tracksJson(List<Track> tracks) {
|
|
||||||
return tracks.map((e) => e.toJson()).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
||||||
const WebSocketLoadEventData._();
|
const WebSocketLoadEventData._();
|
||||||
|
|
||||||
factory WebSocketLoadEventData.playlist({
|
factory WebSocketLoadEventData.playlist({
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) required List<Track> tracks,
|
required List<SpotubeFullTrackObject> tracks,
|
||||||
PlaylistSimple? collection,
|
SpotubeSimplePlaylistObject? collection,
|
||||||
int? initialIndex,
|
int? initialIndex,
|
||||||
}) = WebSocketLoadEventDataPlaylist;
|
}) = WebSocketLoadEventDataPlaylist;
|
||||||
|
|
||||||
factory WebSocketLoadEventData.album({
|
factory WebSocketLoadEventData.album({
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) required List<Track> tracks,
|
required List<SpotubeFullTrackObject> tracks,
|
||||||
AlbumSimple? collection,
|
SpotubeSimpleAlbumObject? collection,
|
||||||
int? initialIndex,
|
int? initialIndex,
|
||||||
}) = WebSocketLoadEventDataAlbum;
|
}) = WebSocketLoadEventDataAlbum;
|
||||||
|
|
||||||
|
@ -338,13 +338,16 @@ class WebSocketRemoveTrackEvent extends WebSocketEvent<String> {
|
|||||||
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
|
WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketAddTrackEvent extends WebSocketEvent<Track> {
|
class WebSocketAddTrackEvent extends WebSocketEvent<SpotubeFullTrackObject> {
|
||||||
WebSocketAddTrackEvent(Track data) : super(WsEvent.addTrack, data);
|
WebSocketAddTrackEvent(SpotubeFullTrackObject data)
|
||||||
|
: super(WsEvent.addTrack, data);
|
||||||
|
|
||||||
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
|
WebSocketAddTrackEvent.fromJson(Map<String, dynamic> json)
|
||||||
: super(
|
: super(
|
||||||
WsEvent.addTrack,
|
WsEvent.addTrack,
|
||||||
Track.fromJson(json["data"] as Map<String, dynamic>),
|
SpotubeFullTrackObject.fromJson(
|
||||||
|
json["data"] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
|
|
||||||
class CurrentPlaylist {
|
|
||||||
List<Track>? _tempTrack;
|
|
||||||
List<Track> tracks;
|
|
||||||
String id;
|
|
||||||
String name;
|
|
||||||
String thumbnail;
|
|
||||||
bool isLocal;
|
|
||||||
|
|
||||||
CurrentPlaylist({
|
|
||||||
required this.tracks,
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.thumbnail,
|
|
||||||
this.isLocal = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
static CurrentPlaylist fromJson(Map<String, dynamic> map, Ref ref) {
|
|
||||||
return CurrentPlaylist(
|
|
||||||
id: map["id"],
|
|
||||||
tracks: List.castFrom<dynamic, Track>(map["tracks"]
|
|
||||||
.map(
|
|
||||||
(track) => map["isLocal"] == true
|
|
||||||
? SourcedTrack.fromJson(track, ref: ref)
|
|
||||||
: Track.fromJson(track),
|
|
||||||
)
|
|
||||||
.toList()),
|
|
||||||
name: map["name"],
|
|
||||||
thumbnail: map["thumbnail"],
|
|
||||||
isLocal: map["isLocal"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> get trackIds => tracks.map((e) => e.id!).toList();
|
|
||||||
|
|
||||||
bool shuffle(Track? topTrack) {
|
|
||||||
// won't shuffle if already shuffled
|
|
||||||
if (_tempTrack == null) {
|
|
||||||
_tempTrack = [...tracks];
|
|
||||||
tracks = List.from(tracks)..shuffle();
|
|
||||||
if (topTrack != null) {
|
|
||||||
tracks.remove(topTrack);
|
|
||||||
tracks.insert(0, topTrack);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool unshuffle() {
|
|
||||||
// without _tempTracks unshuffling can't be done
|
|
||||||
if (_tempTrack != null) {
|
|
||||||
tracks = [..._tempTrack!];
|
|
||||||
_tempTrack = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
"id": id,
|
|
||||||
"name": name,
|
|
||||||
"tracks": tracks
|
|
||||||
.map((track) =>
|
|
||||||
track is SourcedTrack ? track.toJson() : track.toJson())
|
|
||||||
.toList(),
|
|
||||||
"thumbnail": thumbnail,
|
|
||||||
"isLocal": isLocal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors;
|
|||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
import 'package:spotube/models/database/database.steps.dart';
|
import 'package:spotube/models/database/database.steps.dart';
|
||||||
import 'package:spotube/models/lyrics.dart';
|
import 'package:spotube/models/lyrics.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
@ -43,6 +44,7 @@ part 'typeconverters/locale.dart';
|
|||||||
part 'typeconverters/string_list.dart';
|
part 'typeconverters/string_list.dart';
|
||||||
part 'typeconverters/encrypted_text.dart';
|
part 'typeconverters/encrypted_text.dart';
|
||||||
part 'typeconverters/map.dart';
|
part 'typeconverters/map.dart';
|
||||||
|
part 'typeconverters/map_list.dart';
|
||||||
part 'typeconverters/subtitle.dart';
|
part 'typeconverters/subtitle.dart';
|
||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
@ -54,8 +56,6 @@ part 'typeconverters/subtitle.dart';
|
|||||||
SkipSegmentTable,
|
SkipSegmentTable,
|
||||||
SourceMatchTable,
|
SourceMatchTable,
|
||||||
AudioPlayerStateTable,
|
AudioPlayerStateTable,
|
||||||
PlaylistTable,
|
|
||||||
PlaylistMediaTable,
|
|
||||||
HistoryTable,
|
HistoryTable,
|
||||||
LyricsTable,
|
LyricsTable,
|
||||||
MetadataPluginsTable,
|
MetadataPluginsTable,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6,22 +6,30 @@ class AudioPlayerStateTable extends Table {
|
|||||||
TextColumn get loopMode => textEnum<PlaylistMode>()();
|
TextColumn get loopMode => textEnum<PlaylistMode>()();
|
||||||
BoolColumn get shuffled => boolean()();
|
BoolColumn get shuffled => boolean()();
|
||||||
TextColumn get collections => text().map(const StringListConverter())();
|
TextColumn get collections => text().map(const StringListConverter())();
|
||||||
|
TextColumn get tracks =>
|
||||||
|
text().map(const SpotubeTrackObjectListConverter())();
|
||||||
|
IntColumn get currentIndex => integer()();
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaylistTable extends Table {
|
class SpotubeTrackObjectListConverter
|
||||||
IntColumn get id => integer().autoIncrement()();
|
extends TypeConverter<List<SpotubeTrackObject>, String> {
|
||||||
IntColumn get audioPlayerStateId =>
|
const SpotubeTrackObjectListConverter();
|
||||||
integer().references(AudioPlayerStateTable, #id)();
|
|
||||||
IntColumn get index => integer()();
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaylistMediaTable extends Table {
|
@override
|
||||||
IntColumn get id => integer().autoIncrement()();
|
List<SpotubeTrackObject> fromSql(String fromDb) {
|
||||||
IntColumn get playlistId => integer().references(PlaylistTable, #id)();
|
return fromDb
|
||||||
|
.split(",")
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.map(
|
||||||
|
(e) => SpotubeTrackObject.fromJson(
|
||||||
|
json.decode(e) as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
TextColumn get uri => text()();
|
@override
|
||||||
TextColumn get extras =>
|
String toSql(List<SpotubeTrackObject> value) {
|
||||||
text().nullable().map(const MapTypeConverter<String, dynamic>())();
|
return value.map((e) => json.encode(e)).join(",");
|
||||||
TextColumn get httpHeaders =>
|
}
|
||||||
text().nullable().map(const MapTypeConverter<String, String>())();
|
|
||||||
}
|
}
|
||||||
|
20
lib/models/database/typeconverters/map_list.dart
Normal file
20
lib/models/database/typeconverters/map_list.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
part of '../database.dart';
|
||||||
|
|
||||||
|
class MapListConverter
|
||||||
|
extends TypeConverter<List<Map<String, dynamic>>, String> {
|
||||||
|
const MapListConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Map<String, dynamic>> fromSql(String fromDb) {
|
||||||
|
return fromDb
|
||||||
|
.split(",")
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.map((e) => json.decode(e) as Map<String, dynamic>)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(List<Map<String, dynamic>> value) {
|
||||||
|
return value.map((e) => json.encode(e)).join(",");
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,13 @@
|
|||||||
library metadata_objects;
|
library metadata_objects;
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|
||||||
part 'metadata.g.dart';
|
part 'metadata.g.dart';
|
||||||
|
@ -2822,6 +2822,8 @@ abstract class _SpotubeSearchResponseObject
|
|||||||
|
|
||||||
SpotubeTrackObject _$SpotubeTrackObjectFromJson(Map<String, dynamic> json) {
|
SpotubeTrackObject _$SpotubeTrackObjectFromJson(Map<String, dynamic> json) {
|
||||||
switch (json['runtimeType']) {
|
switch (json['runtimeType']) {
|
||||||
|
case 'local':
|
||||||
|
return SpotubeLocalTrackObject.fromJson(json);
|
||||||
case 'full':
|
case 'full':
|
||||||
return SpotubeFullTrackObject.fromJson(json);
|
return SpotubeFullTrackObject.fromJson(json);
|
||||||
case 'simple':
|
case 'simple':
|
||||||
@ -2842,9 +2844,17 @@ mixin _$SpotubeTrackObject {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError;
|
SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError;
|
||||||
int get durationMs => throw _privateConstructorUsedError;
|
int get durationMs => throw _privateConstructorUsedError;
|
||||||
bool get explicit => throw _privateConstructorUsedError;
|
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)
|
||||||
|
local,
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -2868,6 +2878,15 @@ mixin _$SpotubeTrackObject {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -2891,6 +2910,15 @@ mixin _$SpotubeTrackObject {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
TResult Function(
|
TResult Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -2915,18 +2943,21 @@ mixin _$SpotubeTrackObject {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
@ -2955,8 +2986,7 @@ abstract class $SpotubeTrackObjectCopyWith<$Res> {
|
|||||||
String externalUri,
|
String externalUri,
|
||||||
List<SpotubeSimpleArtistObject> artists,
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
SpotubeSimpleAlbumObject album,
|
SpotubeSimpleAlbumObject album,
|
||||||
int durationMs,
|
int durationMs});
|
||||||
bool explicit});
|
|
||||||
|
|
||||||
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album;
|
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get album;
|
||||||
}
|
}
|
||||||
@ -2982,7 +3012,6 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
|
|||||||
Object? artists = null,
|
Object? artists = null,
|
||||||
Object? album = null,
|
Object? album = null,
|
||||||
Object? durationMs = null,
|
Object? durationMs = null,
|
||||||
Object? explicit = null,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -3009,10 +3038,6 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
|
|||||||
? _value.durationMs
|
? _value.durationMs
|
||||||
: durationMs // ignore: cast_nullable_to_non_nullable
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
explicit: null == explicit
|
|
||||||
? _value.explicit
|
|
||||||
: explicit // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3031,6 +3056,358 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SpotubeLocalTrackObjectImplCopyWith<$Res>
|
||||||
|
implements $SpotubeTrackObjectCopyWith<$Res> {
|
||||||
|
factory _$$SpotubeLocalTrackObjectImplCopyWith(
|
||||||
|
_$SpotubeLocalTrackObjectImpl value,
|
||||||
|
$Res Function(_$SpotubeLocalTrackObjectImpl) then) =
|
||||||
|
__$$SpotubeLocalTrackObjectImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SpotubeLocalTrackObjectImplCopyWithImpl<$Res>
|
||||||
|
extends _$SpotubeTrackObjectCopyWithImpl<$Res,
|
||||||
|
_$SpotubeLocalTrackObjectImpl>
|
||||||
|
implements _$$SpotubeLocalTrackObjectImplCopyWith<$Res> {
|
||||||
|
__$$SpotubeLocalTrackObjectImplCopyWithImpl(
|
||||||
|
_$SpotubeLocalTrackObjectImpl _value,
|
||||||
|
$Res Function(_$SpotubeLocalTrackObjectImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SpotubeTrackObject
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? externalUri = null,
|
||||||
|
Object? artists = null,
|
||||||
|
Object? album = null,
|
||||||
|
Object? durationMs = null,
|
||||||
|
Object? path = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$SpotubeLocalTrackObjectImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
externalUri: null == externalUri
|
||||||
|
? _value.externalUri
|
||||||
|
: externalUri // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
artists: null == artists
|
||||||
|
? _value._artists
|
||||||
|
: artists // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SpotubeSimpleArtistObject>,
|
||||||
|
album: null == album
|
||||||
|
? _value.album
|
||||||
|
: album // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SpotubeSimpleAlbumObject,
|
||||||
|
durationMs: null == durationMs
|
||||||
|
? _value.durationMs
|
||||||
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
path: null == path
|
||||||
|
? _value.path
|
||||||
|
: path // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SpotubeTrackObject
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SpotubeSimpleAlbumObjectCopyWith<$Res> get album {
|
||||||
|
return $SpotubeSimpleAlbumObjectCopyWith<$Res>(_value.album, (value) {
|
||||||
|
return _then(_value.copyWith(album: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject {
|
||||||
|
_$SpotubeLocalTrackObjectImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.externalUri,
|
||||||
|
final List<SpotubeSimpleArtistObject> artists = const [],
|
||||||
|
required this.album,
|
||||||
|
required this.durationMs,
|
||||||
|
required this.path,
|
||||||
|
final String? $type})
|
||||||
|
: _artists = artists,
|
||||||
|
$type = $type ?? 'local';
|
||||||
|
|
||||||
|
factory _$SpotubeLocalTrackObjectImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SpotubeLocalTrackObjectImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
@override
|
||||||
|
final String externalUri;
|
||||||
|
final List<SpotubeSimpleArtistObject> _artists;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
List<SpotubeSimpleArtistObject> get artists {
|
||||||
|
if (_artists is EqualUnmodifiableListView) return _artists;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final SpotubeSimpleAlbumObject album;
|
||||||
|
@override
|
||||||
|
final int durationMs;
|
||||||
|
@override
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SpotubeTrackObject.local(id: $id, name: $name, externalUri: $externalUri, artists: $artists, album: $album, durationMs: $durationMs, path: $path)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SpotubeLocalTrackObjectImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.name, name) || other.name == name) &&
|
||||||
|
(identical(other.externalUri, externalUri) ||
|
||||||
|
other.externalUri == externalUri) &&
|
||||||
|
const DeepCollectionEquality().equals(other._artists, _artists) &&
|
||||||
|
(identical(other.album, album) || other.album == album) &&
|
||||||
|
(identical(other.durationMs, durationMs) ||
|
||||||
|
other.durationMs == durationMs) &&
|
||||||
|
(identical(other.path, path) || other.path == path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, id, name, externalUri,
|
||||||
|
const DeepCollectionEquality().hash(_artists), album, durationMs, path);
|
||||||
|
|
||||||
|
/// Create a copy of SpotubeTrackObject
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SpotubeLocalTrackObjectImplCopyWith<_$SpotubeLocalTrackObjectImpl>
|
||||||
|
get copyWith => __$$SpotubeLocalTrackObjectImplCopyWithImpl<
|
||||||
|
_$SpotubeLocalTrackObjectImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)
|
||||||
|
local,
|
||||||
|
required TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String isrc,
|
||||||
|
bool explicit)
|
||||||
|
full,
|
||||||
|
required TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
int durationMs,
|
||||||
|
bool explicit,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject? album)
|
||||||
|
simple,
|
||||||
|
}) {
|
||||||
|
return local(id, name, externalUri, artists, album, durationMs, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
|
TResult? Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String isrc,
|
||||||
|
bool explicit)?
|
||||||
|
full,
|
||||||
|
TResult? Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
int durationMs,
|
||||||
|
bool explicit,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject? album)?
|
||||||
|
simple,
|
||||||
|
}) {
|
||||||
|
return local?.call(id, name, externalUri, artists, album, durationMs, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
|
TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String isrc,
|
||||||
|
bool explicit)?
|
||||||
|
full,
|
||||||
|
TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
int durationMs,
|
||||||
|
bool explicit,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject? album)?
|
||||||
|
simple,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (local != null) {
|
||||||
|
return local(id, name, externalUri, artists, album, durationMs, path);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
|
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
||||||
|
}) {
|
||||||
|
return local(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
|
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
|
}) {
|
||||||
|
return local?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
|
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (local != null) {
|
||||||
|
return local(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SpotubeLocalTrackObjectImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SpotubeLocalTrackObject implements SpotubeTrackObject {
|
||||||
|
factory SpotubeLocalTrackObject(
|
||||||
|
{required final String id,
|
||||||
|
required final String name,
|
||||||
|
required final String externalUri,
|
||||||
|
final List<SpotubeSimpleArtistObject> artists,
|
||||||
|
required final SpotubeSimpleAlbumObject album,
|
||||||
|
required final int durationMs,
|
||||||
|
required final String path}) = _$SpotubeLocalTrackObjectImpl;
|
||||||
|
|
||||||
|
factory SpotubeLocalTrackObject.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SpotubeLocalTrackObjectImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id;
|
||||||
|
@override
|
||||||
|
String get name;
|
||||||
|
@override
|
||||||
|
String get externalUri;
|
||||||
|
@override
|
||||||
|
List<SpotubeSimpleArtistObject> get artists;
|
||||||
|
@override
|
||||||
|
SpotubeSimpleAlbumObject get album;
|
||||||
|
@override
|
||||||
|
int get durationMs;
|
||||||
|
String get path;
|
||||||
|
|
||||||
|
/// Create a copy of SpotubeTrackObject
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SpotubeLocalTrackObjectImplCopyWith<_$SpotubeLocalTrackObjectImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract class _$$SpotubeFullTrackObjectImplCopyWith<$Res>
|
abstract class _$$SpotubeFullTrackObjectImplCopyWith<$Res>
|
||||||
implements $SpotubeTrackObjectCopyWith<$Res> {
|
implements $SpotubeTrackObjectCopyWith<$Res> {
|
||||||
@ -3218,6 +3595,15 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)
|
||||||
|
local,
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -3245,6 +3631,15 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -3272,6 +3667,15 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
TResult Function(
|
TResult Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -3303,6 +3707,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
||||||
}) {
|
}) {
|
||||||
@ -3312,6 +3717,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
}) {
|
}) {
|
||||||
@ -3321,6 +3727,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
@ -3366,7 +3773,6 @@ abstract class SpotubeFullTrackObject implements SpotubeTrackObject {
|
|||||||
@override
|
@override
|
||||||
int get durationMs;
|
int get durationMs;
|
||||||
String get isrc;
|
String get isrc;
|
||||||
@override
|
|
||||||
bool get explicit;
|
bool get explicit;
|
||||||
|
|
||||||
/// Create a copy of SpotubeTrackObject
|
/// Create a copy of SpotubeTrackObject
|
||||||
@ -3544,6 +3950,15 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)
|
||||||
|
local,
|
||||||
required TResult Function(
|
required TResult Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -3570,6 +3985,15 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
TResult? Function(
|
TResult? Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -3597,6 +4021,15 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String externalUri,
|
||||||
|
List<SpotubeSimpleArtistObject> artists,
|
||||||
|
SpotubeSimpleAlbumObject album,
|
||||||
|
int durationMs,
|
||||||
|
String path)?
|
||||||
|
local,
|
||||||
TResult Function(
|
TResult Function(
|
||||||
String id,
|
String id,
|
||||||
String name,
|
String name,
|
||||||
@ -3628,6 +4061,7 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult map<TResult extends Object?>({
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(SpotubeLocalTrackObject value) local,
|
||||||
required TResult Function(SpotubeFullTrackObject value) full,
|
required TResult Function(SpotubeFullTrackObject value) full,
|
||||||
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
required TResult Function(SpotubeSimpleTrackObject value) simple,
|
||||||
}) {
|
}) {
|
||||||
@ -3637,6 +4071,7 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult? Function(SpotubeFullTrackObject value)? full,
|
TResult? Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
TResult? Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
}) {
|
}) {
|
||||||
@ -3646,6 +4081,7 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeMap<TResult extends Object?>({
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(SpotubeLocalTrackObject value)? local,
|
||||||
TResult Function(SpotubeFullTrackObject value)? full,
|
TResult Function(SpotubeFullTrackObject value)? full,
|
||||||
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
TResult Function(SpotubeSimpleTrackObject value)? simple,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
@ -3685,7 +4121,6 @@ abstract class SpotubeSimpleTrackObject implements SpotubeTrackObject {
|
|||||||
String get externalUri;
|
String get externalUri;
|
||||||
@override
|
@override
|
||||||
int get durationMs;
|
int get durationMs;
|
||||||
@override
|
|
||||||
bool get explicit;
|
bool get explicit;
|
||||||
@override
|
@override
|
||||||
List<SpotubeSimpleArtistObject> get artists;
|
List<SpotubeSimpleArtistObject> get artists;
|
||||||
|
@ -287,6 +287,37 @@ Map<String, dynamic> _$$SpotubeSearchResponseObjectImplToJson(
|
|||||||
'tracks': instance.tracks.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 _$$SpotubeFullTrackObjectImplFromJson(Map json) =>
|
||||||
_$SpotubeFullTrackObjectImpl(
|
_$SpotubeFullTrackObjectImpl(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
|
@ -2,6 +2,16 @@ part of 'metadata.dart';
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SpotubeTrackObject with _$SpotubeTrackObject {
|
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({
|
factory SpotubeTrackObject.full({
|
||||||
required String id,
|
required String id,
|
||||||
required String name,
|
required String name,
|
||||||
@ -27,6 +37,40 @@ class SpotubeTrackObject with _$SpotubeTrackObject {
|
|||||||
_$SpotubeTrackObjectFromJson(
|
_$SpotubeTrackObjectFromJson(
|
||||||
json.containsKey("isrc")
|
json.containsKey("isrc")
|
||||||
? {...json, "runtimeType": "full"}
|
? {...json, "runtimeType": "full"}
|
||||||
: {...json, "runtimeType": "simple"},
|
: json.containsKey("path")
|
||||||
|
? {...json, "runtimeType": "local"}
|
||||||
|
: {...json, "runtimeType": "simple"},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AsMediaListSpotubeTrackObject on Iterable<SpotubeTrackObject> {
|
||||||
|
List<SpotubeMedia> asMediaList() {
|
||||||
|
return map((track) => SpotubeMedia(track)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ToMetadataSpotubeFullTrackObject on SpotubeFullTrackObject {
|
||||||
|
Metadata toMetadata({
|
||||||
|
required int fileLength,
|
||||||
|
Uint8List? imageBytes,
|
||||||
|
}) {
|
||||||
|
return Metadata(
|
||||||
|
title: name,
|
||||||
|
artist: artists.map((a) => a.name).join(", "),
|
||||||
|
album: album.name,
|
||||||
|
albumArtist: artists.map((a) => a.name).join(", "),
|
||||||
|
year: album.releaseDate == null
|
||||||
|
? 1970
|
||||||
|
: DateTime.parse(album.releaseDate!).year,
|
||||||
|
durationMs: durationMs.toDouble(),
|
||||||
|
fileSize: BigInt.from(fileLength),
|
||||||
|
picture: imageBytes != null
|
||||||
|
? Picture(
|
||||||
|
data: imageBytes,
|
||||||
|
// Spotify images are always JPEGs
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
94
lib/models/playback/track_sources.dart
Normal file
94
lib/models/playback/track_sources.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
|
|
||||||
|
part 'track_sources.freezed.dart';
|
||||||
|
part 'track_sources.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TrackSourceQuery with _$TrackSourceQuery {
|
||||||
|
factory TrackSourceQuery({
|
||||||
|
required String id,
|
||||||
|
required String title,
|
||||||
|
required List<String> artists,
|
||||||
|
required String album,
|
||||||
|
required int durationMs,
|
||||||
|
required String isrc,
|
||||||
|
required bool explicit,
|
||||||
|
}) = _TrackSourceQuery;
|
||||||
|
|
||||||
|
factory TrackSourceQuery.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TrackSourceQueryFromJson(json);
|
||||||
|
|
||||||
|
factory TrackSourceQuery.fromTrack(SpotubeFullTrackObject track) {
|
||||||
|
return TrackSourceQuery(
|
||||||
|
id: track.id,
|
||||||
|
title: track.name,
|
||||||
|
artists: track.artists.map((e) => e.name).toList(),
|
||||||
|
album: track.album.name,
|
||||||
|
durationMs: track.durationMs,
|
||||||
|
isrc: track.isrc,
|
||||||
|
explicit: track.explicit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery].
|
||||||
|
factory TrackSourceQuery.parseUri(String url) {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
return TrackSourceQuery.fromJson({
|
||||||
|
"id": uri.pathSegments.last,
|
||||||
|
...uri.queryParameters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TrackSourceInfo with _$TrackSourceInfo {
|
||||||
|
factory TrackSourceInfo({
|
||||||
|
required String id,
|
||||||
|
required String title,
|
||||||
|
required String artists,
|
||||||
|
required String thumbnail,
|
||||||
|
required String pageUrl,
|
||||||
|
required int durationMs,
|
||||||
|
}) = _TrackSourceInfo;
|
||||||
|
|
||||||
|
factory TrackSourceInfo.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TrackSourceInfoFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TrackSource with _$TrackSource {
|
||||||
|
factory TrackSource({
|
||||||
|
required String url,
|
||||||
|
required SourceQualities quality,
|
||||||
|
required SourceCodecs codec,
|
||||||
|
required String bitrate,
|
||||||
|
}) = _TrackSource;
|
||||||
|
|
||||||
|
factory TrackSource.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TrackSourceFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
abstract class BasicSourcedTrack {
|
||||||
|
final TrackSourceQuery query;
|
||||||
|
final AudioSource source;
|
||||||
|
final TrackSourceInfo info;
|
||||||
|
final List<TrackSource> sources;
|
||||||
|
@JsonKey(defaultValue: [])
|
||||||
|
final List<TrackSourceInfo> siblings;
|
||||||
|
BasicSourcedTrack({
|
||||||
|
required this.query,
|
||||||
|
required this.source,
|
||||||
|
required this.info,
|
||||||
|
required this.sources,
|
||||||
|
this.siblings = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BasicSourcedTrack.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$BasicSourcedTrackFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$BasicSourcedTrackToJson(this);
|
||||||
|
}
|
776
lib/models/playback/track_sources.freezed.dart
Normal file
776
lib/models/playback/track_sources.freezed.dart
Normal file
@ -0,0 +1,776 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'track_sources.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
TrackSourceQuery _$TrackSourceQueryFromJson(Map<String, dynamic> json) {
|
||||||
|
return _TrackSourceQuery.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$TrackSourceQuery {
|
||||||
|
String get id => throw _privateConstructorUsedError;
|
||||||
|
String get title => throw _privateConstructorUsedError;
|
||||||
|
List<String> get artists => throw _privateConstructorUsedError;
|
||||||
|
String get album => throw _privateConstructorUsedError;
|
||||||
|
int get durationMs => throw _privateConstructorUsedError;
|
||||||
|
String get isrc => throw _privateConstructorUsedError;
|
||||||
|
bool get explicit => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this TrackSourceQuery to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceQuery
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$TrackSourceQueryCopyWith<TrackSourceQuery> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $TrackSourceQueryCopyWith<$Res> {
|
||||||
|
factory $TrackSourceQueryCopyWith(
|
||||||
|
TrackSourceQuery value, $Res Function(TrackSourceQuery) then) =
|
||||||
|
_$TrackSourceQueryCopyWithImpl<$Res, TrackSourceQuery>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String title,
|
||||||
|
List<String> artists,
|
||||||
|
String album,
|
||||||
|
int durationMs,
|
||||||
|
String isrc,
|
||||||
|
bool explicit});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery>
|
||||||
|
implements $TrackSourceQueryCopyWith<$Res> {
|
||||||
|
_$TrackSourceQueryCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceQuery
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? title = null,
|
||||||
|
Object? artists = null,
|
||||||
|
Object? album = null,
|
||||||
|
Object? durationMs = null,
|
||||||
|
Object? isrc = null,
|
||||||
|
Object? explicit = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: null == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
artists: null == artists
|
||||||
|
? _value.artists
|
||||||
|
: artists // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
|
album: null == album
|
||||||
|
? _value.album
|
||||||
|
: album // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
durationMs: null == durationMs
|
||||||
|
? _value.durationMs
|
||||||
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
isrc: null == isrc
|
||||||
|
? _value.isrc
|
||||||
|
: isrc // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
explicit: null == explicit
|
||||||
|
? _value.explicit
|
||||||
|
: explicit // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$TrackSourceQueryImplCopyWith<$Res>
|
||||||
|
implements $TrackSourceQueryCopyWith<$Res> {
|
||||||
|
factory _$$TrackSourceQueryImplCopyWith(_$TrackSourceQueryImpl value,
|
||||||
|
$Res Function(_$TrackSourceQueryImpl) then) =
|
||||||
|
__$$TrackSourceQueryImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String title,
|
||||||
|
List<String> artists,
|
||||||
|
String album,
|
||||||
|
int durationMs,
|
||||||
|
String isrc,
|
||||||
|
bool explicit});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$TrackSourceQueryImplCopyWithImpl<$Res>
|
||||||
|
extends _$TrackSourceQueryCopyWithImpl<$Res, _$TrackSourceQueryImpl>
|
||||||
|
implements _$$TrackSourceQueryImplCopyWith<$Res> {
|
||||||
|
__$$TrackSourceQueryImplCopyWithImpl(_$TrackSourceQueryImpl _value,
|
||||||
|
$Res Function(_$TrackSourceQueryImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceQuery
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? title = null,
|
||||||
|
Object? artists = null,
|
||||||
|
Object? album = null,
|
||||||
|
Object? durationMs = null,
|
||||||
|
Object? isrc = null,
|
||||||
|
Object? explicit = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$TrackSourceQueryImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: null == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
artists: null == artists
|
||||||
|
? _value._artists
|
||||||
|
: artists // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
|
album: null == album
|
||||||
|
? _value.album
|
||||||
|
: album // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
durationMs: null == durationMs
|
||||||
|
? _value.durationMs
|
||||||
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
isrc: null == isrc
|
||||||
|
? _value.isrc
|
||||||
|
: isrc // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
explicit: null == explicit
|
||||||
|
? _value.explicit
|
||||||
|
: explicit // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$TrackSourceQueryImpl implements _TrackSourceQuery {
|
||||||
|
_$TrackSourceQueryImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.title,
|
||||||
|
required final List<String> artists,
|
||||||
|
required this.album,
|
||||||
|
required this.durationMs,
|
||||||
|
required this.isrc,
|
||||||
|
required this.explicit})
|
||||||
|
: _artists = artists;
|
||||||
|
|
||||||
|
factory _$TrackSourceQueryImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$TrackSourceQueryImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
@override
|
||||||
|
final String title;
|
||||||
|
final List<String> _artists;
|
||||||
|
@override
|
||||||
|
List<String> get artists {
|
||||||
|
if (_artists is EqualUnmodifiableListView) return _artists;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String album;
|
||||||
|
@override
|
||||||
|
final int durationMs;
|
||||||
|
@override
|
||||||
|
final String isrc;
|
||||||
|
@override
|
||||||
|
final bool explicit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'TrackSourceQuery(id: $id, title: $title, artists: $artists, album: $album, durationMs: $durationMs, isrc: $isrc, explicit: $explicit)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$TrackSourceQueryImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.title, title) || other.title == title) &&
|
||||||
|
const DeepCollectionEquality().equals(other._artists, _artists) &&
|
||||||
|
(identical(other.album, album) || other.album == album) &&
|
||||||
|
(identical(other.durationMs, durationMs) ||
|
||||||
|
other.durationMs == durationMs) &&
|
||||||
|
(identical(other.isrc, isrc) || other.isrc == isrc) &&
|
||||||
|
(identical(other.explicit, explicit) ||
|
||||||
|
other.explicit == explicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
const DeepCollectionEquality().hash(_artists),
|
||||||
|
album,
|
||||||
|
durationMs,
|
||||||
|
isrc,
|
||||||
|
explicit);
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceQuery
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
|
||||||
|
__$$TrackSourceQueryImplCopyWithImpl<_$TrackSourceQueryImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$TrackSourceQueryImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _TrackSourceQuery implements TrackSourceQuery {
|
||||||
|
factory _TrackSourceQuery(
|
||||||
|
{required final String id,
|
||||||
|
required final String title,
|
||||||
|
required final List<String> artists,
|
||||||
|
required final String album,
|
||||||
|
required final int durationMs,
|
||||||
|
required final String isrc,
|
||||||
|
required final bool explicit}) = _$TrackSourceQueryImpl;
|
||||||
|
|
||||||
|
factory _TrackSourceQuery.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$TrackSourceQueryImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id;
|
||||||
|
@override
|
||||||
|
String get title;
|
||||||
|
@override
|
||||||
|
List<String> get artists;
|
||||||
|
@override
|
||||||
|
String get album;
|
||||||
|
@override
|
||||||
|
int get durationMs;
|
||||||
|
@override
|
||||||
|
String get isrc;
|
||||||
|
@override
|
||||||
|
bool get explicit;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceQuery
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSourceInfo _$TrackSourceInfoFromJson(Map<String, dynamic> json) {
|
||||||
|
return _TrackSourceInfo.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$TrackSourceInfo {
|
||||||
|
String get id => throw _privateConstructorUsedError;
|
||||||
|
String get title => throw _privateConstructorUsedError;
|
||||||
|
String get artists => throw _privateConstructorUsedError;
|
||||||
|
String get thumbnail => throw _privateConstructorUsedError;
|
||||||
|
String get pageUrl => throw _privateConstructorUsedError;
|
||||||
|
int get durationMs => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this TrackSourceInfo to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceInfo
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$TrackSourceInfoCopyWith<TrackSourceInfo> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $TrackSourceInfoCopyWith<$Res> {
|
||||||
|
factory $TrackSourceInfoCopyWith(
|
||||||
|
TrackSourceInfo value, $Res Function(TrackSourceInfo) then) =
|
||||||
|
_$TrackSourceInfoCopyWithImpl<$Res, TrackSourceInfo>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String title,
|
||||||
|
String artists,
|
||||||
|
String thumbnail,
|
||||||
|
String pageUrl,
|
||||||
|
int durationMs});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo>
|
||||||
|
implements $TrackSourceInfoCopyWith<$Res> {
|
||||||
|
_$TrackSourceInfoCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceInfo
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? title = null,
|
||||||
|
Object? artists = null,
|
||||||
|
Object? thumbnail = null,
|
||||||
|
Object? pageUrl = null,
|
||||||
|
Object? durationMs = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: null == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
artists: null == artists
|
||||||
|
? _value.artists
|
||||||
|
: artists // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
thumbnail: null == thumbnail
|
||||||
|
? _value.thumbnail
|
||||||
|
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
pageUrl: null == pageUrl
|
||||||
|
? _value.pageUrl
|
||||||
|
: pageUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
durationMs: null == durationMs
|
||||||
|
? _value.durationMs
|
||||||
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$TrackSourceInfoImplCopyWith<$Res>
|
||||||
|
implements $TrackSourceInfoCopyWith<$Res> {
|
||||||
|
factory _$$TrackSourceInfoImplCopyWith(_$TrackSourceInfoImpl value,
|
||||||
|
$Res Function(_$TrackSourceInfoImpl) then) =
|
||||||
|
__$$TrackSourceInfoImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String title,
|
||||||
|
String artists,
|
||||||
|
String thumbnail,
|
||||||
|
String pageUrl,
|
||||||
|
int durationMs});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$TrackSourceInfoImplCopyWithImpl<$Res>
|
||||||
|
extends _$TrackSourceInfoCopyWithImpl<$Res, _$TrackSourceInfoImpl>
|
||||||
|
implements _$$TrackSourceInfoImplCopyWith<$Res> {
|
||||||
|
__$$TrackSourceInfoImplCopyWithImpl(
|
||||||
|
_$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceInfo
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? title = null,
|
||||||
|
Object? artists = null,
|
||||||
|
Object? thumbnail = null,
|
||||||
|
Object? pageUrl = null,
|
||||||
|
Object? durationMs = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$TrackSourceInfoImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
title: null == title
|
||||||
|
? _value.title
|
||||||
|
: title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
artists: null == artists
|
||||||
|
? _value.artists
|
||||||
|
: artists // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
thumbnail: null == thumbnail
|
||||||
|
? _value.thumbnail
|
||||||
|
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
pageUrl: null == pageUrl
|
||||||
|
? _value.pageUrl
|
||||||
|
: pageUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
durationMs: null == durationMs
|
||||||
|
? _value.durationMs
|
||||||
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$TrackSourceInfoImpl implements _TrackSourceInfo {
|
||||||
|
_$TrackSourceInfoImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.artists,
|
||||||
|
required this.thumbnail,
|
||||||
|
required this.pageUrl,
|
||||||
|
required this.durationMs});
|
||||||
|
|
||||||
|
factory _$TrackSourceInfoImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$TrackSourceInfoImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
@override
|
||||||
|
final String title;
|
||||||
|
@override
|
||||||
|
final String artists;
|
||||||
|
@override
|
||||||
|
final String thumbnail;
|
||||||
|
@override
|
||||||
|
final String pageUrl;
|
||||||
|
@override
|
||||||
|
final int durationMs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'TrackSourceInfo(id: $id, title: $title, artists: $artists, thumbnail: $thumbnail, pageUrl: $pageUrl, durationMs: $durationMs)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$TrackSourceInfoImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.title, title) || other.title == title) &&
|
||||||
|
(identical(other.artists, artists) || other.artists == artists) &&
|
||||||
|
(identical(other.thumbnail, thumbnail) ||
|
||||||
|
other.thumbnail == thumbnail) &&
|
||||||
|
(identical(other.pageUrl, pageUrl) || other.pageUrl == pageUrl) &&
|
||||||
|
(identical(other.durationMs, durationMs) ||
|
||||||
|
other.durationMs == durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType, id, title, artists, thumbnail, pageUrl, durationMs);
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceInfo
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
|
||||||
|
__$$TrackSourceInfoImplCopyWithImpl<_$TrackSourceInfoImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$TrackSourceInfoImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _TrackSourceInfo implements TrackSourceInfo {
|
||||||
|
factory _TrackSourceInfo(
|
||||||
|
{required final String id,
|
||||||
|
required final String title,
|
||||||
|
required final String artists,
|
||||||
|
required final String thumbnail,
|
||||||
|
required final String pageUrl,
|
||||||
|
required final int durationMs}) = _$TrackSourceInfoImpl;
|
||||||
|
|
||||||
|
factory _TrackSourceInfo.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$TrackSourceInfoImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id;
|
||||||
|
@override
|
||||||
|
String get title;
|
||||||
|
@override
|
||||||
|
String get artists;
|
||||||
|
@override
|
||||||
|
String get thumbnail;
|
||||||
|
@override
|
||||||
|
String get pageUrl;
|
||||||
|
@override
|
||||||
|
int get durationMs;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSourceInfo
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSource _$TrackSourceFromJson(Map<String, dynamic> json) {
|
||||||
|
return _TrackSource.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$TrackSource {
|
||||||
|
String get url => throw _privateConstructorUsedError;
|
||||||
|
SourceQualities get quality => throw _privateConstructorUsedError;
|
||||||
|
SourceCodecs get codec => throw _privateConstructorUsedError;
|
||||||
|
String get bitrate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this TrackSource to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$TrackSourceCopyWith<TrackSource> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $TrackSourceCopyWith<$Res> {
|
||||||
|
factory $TrackSourceCopyWith(
|
||||||
|
TrackSource value, $Res Function(TrackSource) then) =
|
||||||
|
_$TrackSourceCopyWithImpl<$Res, TrackSource>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String url,
|
||||||
|
SourceQualities quality,
|
||||||
|
SourceCodecs codec,
|
||||||
|
String bitrate});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource>
|
||||||
|
implements $TrackSourceCopyWith<$Res> {
|
||||||
|
_$TrackSourceCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? url = null,
|
||||||
|
Object? quality = null,
|
||||||
|
Object? codec = null,
|
||||||
|
Object? bitrate = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
url: null == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
quality: null == quality
|
||||||
|
? _value.quality
|
||||||
|
: quality // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SourceQualities,
|
||||||
|
codec: null == codec
|
||||||
|
? _value.codec
|
||||||
|
: codec // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SourceCodecs,
|
||||||
|
bitrate: null == bitrate
|
||||||
|
? _value.bitrate
|
||||||
|
: bitrate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$TrackSourceImplCopyWith<$Res>
|
||||||
|
implements $TrackSourceCopyWith<$Res> {
|
||||||
|
factory _$$TrackSourceImplCopyWith(
|
||||||
|
_$TrackSourceImpl value, $Res Function(_$TrackSourceImpl) then) =
|
||||||
|
__$$TrackSourceImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String url,
|
||||||
|
SourceQualities quality,
|
||||||
|
SourceCodecs codec,
|
||||||
|
String bitrate});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$TrackSourceImplCopyWithImpl<$Res>
|
||||||
|
extends _$TrackSourceCopyWithImpl<$Res, _$TrackSourceImpl>
|
||||||
|
implements _$$TrackSourceImplCopyWith<$Res> {
|
||||||
|
__$$TrackSourceImplCopyWithImpl(
|
||||||
|
_$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of TrackSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? url = null,
|
||||||
|
Object? quality = null,
|
||||||
|
Object? codec = null,
|
||||||
|
Object? bitrate = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$TrackSourceImpl(
|
||||||
|
url: null == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
quality: null == quality
|
||||||
|
? _value.quality
|
||||||
|
: quality // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SourceQualities,
|
||||||
|
codec: null == codec
|
||||||
|
? _value.codec
|
||||||
|
: codec // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SourceCodecs,
|
||||||
|
bitrate: null == bitrate
|
||||||
|
? _value.bitrate
|
||||||
|
: bitrate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$TrackSourceImpl implements _TrackSource {
|
||||||
|
_$TrackSourceImpl(
|
||||||
|
{required this.url,
|
||||||
|
required this.quality,
|
||||||
|
required this.codec,
|
||||||
|
required this.bitrate});
|
||||||
|
|
||||||
|
factory _$TrackSourceImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$TrackSourceImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String url;
|
||||||
|
@override
|
||||||
|
final SourceQualities quality;
|
||||||
|
@override
|
||||||
|
final SourceCodecs codec;
|
||||||
|
@override
|
||||||
|
final String bitrate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$TrackSourceImpl &&
|
||||||
|
(identical(other.url, url) || other.url == url) &&
|
||||||
|
(identical(other.quality, quality) || other.quality == quality) &&
|
||||||
|
(identical(other.codec, codec) || other.codec == codec) &&
|
||||||
|
(identical(other.bitrate, bitrate) || other.bitrate == bitrate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, url, quality, codec, bitrate);
|
||||||
|
|
||||||
|
/// Create a copy of TrackSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
|
||||||
|
__$$TrackSourceImplCopyWithImpl<_$TrackSourceImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$TrackSourceImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _TrackSource implements TrackSource {
|
||||||
|
factory _TrackSource(
|
||||||
|
{required final String url,
|
||||||
|
required final SourceQualities quality,
|
||||||
|
required final SourceCodecs codec,
|
||||||
|
required final String bitrate}) = _$TrackSourceImpl;
|
||||||
|
|
||||||
|
factory _TrackSource.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$TrackSourceImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get url;
|
||||||
|
@override
|
||||||
|
SourceQualities get quality;
|
||||||
|
@override
|
||||||
|
SourceCodecs get codec;
|
||||||
|
@override
|
||||||
|
String get bitrate;
|
||||||
|
|
||||||
|
/// Create a copy of TrackSource
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
110
lib/models/playback/track_sources.g.dart
Normal file
110
lib/models/playback/track_sources.g.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'track_sources.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
||||||
|
query: TrackSourceQuery.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['query'] as Map)),
|
||||||
|
source: $enumDecode(_$AudioSourceEnumMap, json['source']),
|
||||||
|
info: TrackSourceInfo.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['info'] as Map)),
|
||||||
|
sources: (json['sources'] as List<dynamic>)
|
||||||
|
.map((e) => TrackSource.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList(),
|
||||||
|
siblings: (json['siblings'] as List<dynamic>?)
|
||||||
|
?.map((e) =>
|
||||||
|
TrackSourceInfo.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$BasicSourcedTrackToJson(BasicSourcedTrack instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'query': instance.query.toJson(),
|
||||||
|
'source': _$AudioSourceEnumMap[instance.source]!,
|
||||||
|
'info': instance.info.toJson(),
|
||||||
|
'sources': instance.sources.map((e) => e.toJson()).toList(),
|
||||||
|
'siblings': instance.siblings.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$AudioSourceEnumMap = {
|
||||||
|
AudioSource.youtube: 'youtube',
|
||||||
|
AudioSource.piped: 'piped',
|
||||||
|
AudioSource.jiosaavn: 'jiosaavn',
|
||||||
|
AudioSource.invidious: 'invidious',
|
||||||
|
};
|
||||||
|
|
||||||
|
_$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) =>
|
||||||
|
_$TrackSourceQueryImpl(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
artists:
|
||||||
|
(json['artists'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
|
album: json['album'] as String,
|
||||||
|
durationMs: (json['durationMs'] as num).toInt(),
|
||||||
|
isrc: json['isrc'] as String,
|
||||||
|
explicit: json['explicit'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$TrackSourceQueryImplToJson(
|
||||||
|
_$TrackSourceQueryImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'artists': instance.artists,
|
||||||
|
'album': instance.album,
|
||||||
|
'durationMs': instance.durationMs,
|
||||||
|
'isrc': instance.isrc,
|
||||||
|
'explicit': instance.explicit,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$TrackSourceInfoImpl _$$TrackSourceInfoImplFromJson(Map json) =>
|
||||||
|
_$TrackSourceInfoImpl(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
artists: json['artists'] as String,
|
||||||
|
thumbnail: json['thumbnail'] as String,
|
||||||
|
pageUrl: json['pageUrl'] as String,
|
||||||
|
durationMs: (json['durationMs'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$TrackSourceInfoImplToJson(
|
||||||
|
_$TrackSourceInfoImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'artists': instance.artists,
|
||||||
|
'thumbnail': instance.thumbnail,
|
||||||
|
'pageUrl': instance.pageUrl,
|
||||||
|
'durationMs': instance.durationMs,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl(
|
||||||
|
url: json['url'] as String,
|
||||||
|
quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']),
|
||||||
|
codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']),
|
||||||
|
bitrate: json['bitrate'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'url': instance.url,
|
||||||
|
'quality': _$SourceQualitiesEnumMap[instance.quality]!,
|
||||||
|
'codec': _$SourceCodecsEnumMap[instance.codec]!,
|
||||||
|
'bitrate': instance.bitrate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SourceQualitiesEnumMap = {
|
||||||
|
SourceQualities.high: 'high',
|
||||||
|
SourceQualities.medium: 'medium',
|
||||||
|
SourceQualities.low: 'low',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SourceCodecsEnumMap = {
|
||||||
|
SourceCodecs.m4a: 'm4a',
|
||||||
|
SourceCodecs.weba: 'weba',
|
||||||
|
};
|
@ -24,7 +24,7 @@ import 'package:spotube/models/local_track.dart';
|
|||||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -44,7 +44,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider);
|
final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider);
|
||||||
final currentActiveTrack =
|
final currentActiveTrack =
|
||||||
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
||||||
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
||||||
|
@ -19,7 +19,7 @@ import 'package:spotube/hooks/utils/use_debounce.dart';
|
|||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
@ -75,9 +75,9 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
final searchMode = useState(preferences.searchMode);
|
final searchMode = useState(preferences.searchMode);
|
||||||
final activeTrackNotifier = ref.watch(activeSourcedTrackProvider.notifier);
|
final activeTrackNotifier = ref.watch(activeTrackSourcesProvider.notifier);
|
||||||
final activeTrack =
|
final activeTrack =
|
||||||
ref.watch(activeSourcedTrackProvider) ?? playlist.activeTrack;
|
ref.watch(activeTrackSourcesProvider) ?? playlist.activeTrack;
|
||||||
|
|
||||||
final title = ServiceUtils.getTitle(
|
final title = ServiceUtils.getTitle(
|
||||||
activeTrack?.name ?? "",
|
activeTrack?.name ?? "",
|
||||||
|
@ -3,22 +3,38 @@ import 'dart:math';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
|
||||||
import 'package:spotube/extensions/list.dart';
|
import 'package:spotube/extensions/list.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/discord_provider.dart';
|
import 'package:spotube/provider/discord_provider.dart';
|
||||||
import 'package:spotube/provider/server/sourced_track.dart';
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||||
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
|
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
|
||||||
|
|
||||||
|
void _assertAllowedTracks(Iterable<SpotubeTrackObject> tracks) {
|
||||||
|
assert(
|
||||||
|
tracks.every(
|
||||||
|
(track) =>
|
||||||
|
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
|
||||||
|
),
|
||||||
|
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _assertAllowedTrack(SpotubeTrackObject tracks) {
|
||||||
|
assert(
|
||||||
|
tracks is SpotubeFullTrackObject || tracks is SpotubeLocalTrackObject,
|
||||||
|
'Track must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _syncSavedState() async {
|
Future<void> _syncSavedState() async {
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
|
|
||||||
@ -32,6 +48,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
loopMode: audioPlayer.loopMode,
|
loopMode: audioPlayer.loopMode,
|
||||||
shuffled: audioPlayer.isShuffled,
|
shuffled: audioPlayer.isShuffled,
|
||||||
collections: <String>[],
|
collections: <String>[],
|
||||||
|
tracks: <SpotubeTrackObject>[],
|
||||||
|
currentIndex: 0,
|
||||||
id: const Value(0),
|
id: const Value(0),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -43,51 +61,20 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
await audioPlayer.setShuffle(playerState.shuffled);
|
await audioPlayer.setShuffle(playerState.shuffled);
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlist =
|
final tracks = playerState.tracks;
|
||||||
await database.select(database.playlistTable).getSingleOrNull();
|
final currentIndex = playerState.currentIndex;
|
||||||
final medias = await database.select(database.playlistMediaTable).get();
|
|
||||||
|
|
||||||
if (playlist == null) {
|
if (tracks.isEmpty && state.tracks.isNotEmpty) {
|
||||||
await database.into(database.playlistTable).insert(
|
await _updatePlayerState(
|
||||||
PlaylistTableCompanion.insert(
|
AudioPlayerStateTableCompanion(
|
||||||
audioPlayerStateId: 0,
|
tracks: Value(state.tracks),
|
||||||
index: audioPlayer.playlist.index,
|
currentIndex: Value(currentIndex),
|
||||||
id: const Value(0),
|
),
|
||||||
),
|
);
|
||||||
);
|
} else if (tracks.isNotEmpty) {
|
||||||
|
|
||||||
playlist = await database.select(database.playlistTable).getSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (medias.isEmpty && audioPlayer.playlist.medias.isNotEmpty) {
|
|
||||||
await database.batch((batch) {
|
|
||||||
batch.insertAll(
|
|
||||||
database.playlistMediaTable,
|
|
||||||
[
|
|
||||||
for (final media in audioPlayer.playlist.medias)
|
|
||||||
PlaylistMediaTableCompanion.insert(
|
|
||||||
playlistId: playlist!.id,
|
|
||||||
uri: media.uri,
|
|
||||||
extras: Value(media.extras),
|
|
||||||
httpHeaders: Value(media.httpHeaders),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else if (medias.isNotEmpty) {
|
|
||||||
await audioPlayer.openPlaylist(
|
await audioPlayer.openPlaylist(
|
||||||
medias
|
tracks.asMediaList(),
|
||||||
.map(
|
initialIndex: currentIndex,
|
||||||
(media) => SpotubeMedia.fromMedia(
|
|
||||||
Media(
|
|
||||||
media.uri,
|
|
||||||
extras: media.extras,
|
|
||||||
httpHeaders: media.httpHeaders,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
initialIndex: playlist.index,
|
|
||||||
autoPlay: false,
|
autoPlay: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,36 +96,6 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
.write(companion);
|
.write(companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updatePlaylist(
|
|
||||||
Playlist playlist,
|
|
||||||
) async {
|
|
||||||
final database = ref.read(databaseProvider);
|
|
||||||
|
|
||||||
await database.batch((batch) {
|
|
||||||
batch.update(
|
|
||||||
database.playlistTable,
|
|
||||||
PlaylistTableCompanion(index: Value(playlist.index)),
|
|
||||||
where: (tb) => tb.id.equals(0),
|
|
||||||
);
|
|
||||||
|
|
||||||
batch.deleteAll(database.playlistMediaTable);
|
|
||||||
|
|
||||||
if (playlist.medias.isEmpty) return;
|
|
||||||
batch.insertAll(
|
|
||||||
database.playlistMediaTable,
|
|
||||||
[
|
|
||||||
for (final media in playlist.medias)
|
|
||||||
PlaylistMediaTableCompanion.insert(
|
|
||||||
playlistId: 0,
|
|
||||||
uri: media.uri,
|
|
||||||
extras: Value(media.extras),
|
|
||||||
httpHeaders: Value(media.httpHeaders),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build() {
|
build() {
|
||||||
final subscriptions = [
|
final subscriptions = [
|
||||||
@ -183,9 +140,25 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
}),
|
}),
|
||||||
audioPlayer.playlistStream.listen((playlist) async {
|
audioPlayer.playlistStream.listen((playlist) async {
|
||||||
try {
|
try {
|
||||||
state = state.copyWith(playlist: playlist);
|
final queries = playlist.medias
|
||||||
|
.map((media) => TrackSourceQuery.parseUri(media.uri))
|
||||||
|
.toList();
|
||||||
|
final tracks = queries
|
||||||
|
.map((query) => state.tracks.firstWhere(
|
||||||
|
(element) => element.id == query.id,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
state = state.copyWith(
|
||||||
|
tracks: tracks,
|
||||||
|
currentIndex: playlist.index,
|
||||||
|
);
|
||||||
|
|
||||||
await _updatePlaylist(playlist);
|
await _updatePlayerState(
|
||||||
|
AudioPlayerStateTableCompanion(
|
||||||
|
currentIndex: Value(state.currentIndex),
|
||||||
|
tracks: Value(state.tracks),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
}
|
}
|
||||||
@ -203,8 +176,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
return AudioPlayerState(
|
return AudioPlayerState(
|
||||||
loopMode: audioPlayer.loopMode,
|
loopMode: audioPlayer.loopMode,
|
||||||
playing: audioPlayer.isPlaying,
|
playing: audioPlayer.isPlaying,
|
||||||
playlist: audioPlayer.playlist,
|
|
||||||
shuffled: audioPlayer.isShuffled,
|
shuffled: audioPlayer.isShuffled,
|
||||||
|
tracks: [],
|
||||||
collections: [],
|
collections: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -245,17 +218,16 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
await removeCollections([collectionId]);
|
await removeCollections([collectionId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracks related methods
|
|
||||||
|
|
||||||
Future<void> addTracksAtFirst(
|
Future<void> addTracksAtFirst(
|
||||||
Iterable<Track> tracks, {
|
Iterable<SpotubeTrackObject> tracks, {
|
||||||
bool allowDuplicates = false,
|
bool allowDuplicates = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
_assertAllowedTracks(tracks);
|
||||||
if (state.tracks.length == 1) {
|
if (state.tracks.length == 1) {
|
||||||
return addTracks(tracks);
|
return addTracks(tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks = _blacklist.filter(tracks).toList() as List<Track>;
|
tracks = _blacklist.filter(tracks).toList();
|
||||||
|
|
||||||
for (int i = 0; i < tracks.length; i++) {
|
for (int i = 0; i < tracks.length; i++) {
|
||||||
final track = tracks.elementAt(i);
|
final track = tracks.elementAt(i);
|
||||||
@ -267,19 +239,23 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
|
|
||||||
await audioPlayer.addTrackAt(
|
await audioPlayer.addTrackAt(
|
||||||
SpotubeMedia(track),
|
SpotubeMedia(track),
|
||||||
max(state.playlist.index, 0) + i + 1,
|
max(state.currentIndex, 0) + i + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(Track track) async {
|
Future<void> addTrack(SpotubeTrackObject track) async {
|
||||||
|
_assertAllowedTrack(track);
|
||||||
|
|
||||||
if (_blacklist.contains(track)) return;
|
if (_blacklist.contains(track)) return;
|
||||||
if (state.tracks.any((element) => _compareTracks(element, track))) return;
|
if (state.tracks.any((element) => _compareTracks(element, track))) return;
|
||||||
await audioPlayer.addTrack(SpotubeMedia(track));
|
await audioPlayer.addTrack(SpotubeMedia(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTracks(Iterable<Track> tracks) async {
|
Future<void> addTracks(Iterable<SpotubeTrackObject> tracks) async {
|
||||||
tracks = _blacklist.filter(tracks).toList() as List<Track>;
|
_assertAllowedTracks(tracks);
|
||||||
|
|
||||||
|
tracks = _blacklist.filter(tracks).toList();
|
||||||
for (final track in tracks) {
|
for (final track in tracks) {
|
||||||
await audioPlayer.addTrack(SpotubeMedia(track));
|
await audioPlayer.addTrack(SpotubeMedia(track));
|
||||||
}
|
}
|
||||||
@ -299,31 +275,40 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _compareTracks(Track a, Track b) {
|
bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) {
|
||||||
if ((a is LocalTrack && b is! LocalTrack) ||
|
if ((a is SpotubeLocalTrackObject && b is! SpotubeLocalTrackObject) ||
|
||||||
(a is! LocalTrack && b is LocalTrack)) {
|
(a is! SpotubeLocalTrackObject && b is SpotubeLocalTrackObject)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return a is LocalTrack && b is LocalTrack
|
return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
|
||||||
? (a).path == (b).path
|
? (a).path == (b).path
|
||||||
: a.id == b.id;
|
: a.id == b.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load(
|
Future<void> load(
|
||||||
List<Track> tracks, {
|
List<SpotubeTrackObject> tracks, {
|
||||||
int initialIndex = 0,
|
int initialIndex = 0,
|
||||||
bool autoPlay = false,
|
bool autoPlay = false,
|
||||||
}) async {
|
}) async {
|
||||||
final medias = (_blacklist.filter(tracks).toList() as List<Track>)
|
_assertAllowedTracks(tracks);
|
||||||
|
|
||||||
|
final medias = _blacklist
|
||||||
|
.filter(tracks)
|
||||||
|
.toList()
|
||||||
.asMediaList()
|
.asMediaList()
|
||||||
.unique((a, b) => _compareTracks(a.track, b.track));
|
.unique((a, b) => a.uri == b.uri);
|
||||||
|
|
||||||
// Giving the initial track a boost so MediaKit won't skip
|
// Giving the initial track a boost so MediaKit won't skip
|
||||||
// because of timeout
|
// because of timeout
|
||||||
final intendedActiveTrack = medias.elementAt(initialIndex);
|
final intendedActiveTrack = medias.elementAt(initialIndex);
|
||||||
if (intendedActiveTrack.track is! LocalTrack) {
|
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
|
||||||
await ref.read(sourcedTrackProvider(intendedActiveTrack).future);
|
await ref.read(
|
||||||
|
trackSourcesProvider(
|
||||||
|
TrackSourceQuery.fromTrack(
|
||||||
|
intendedActiveTrack.track as SpotubeFullTrackObject),
|
||||||
|
).future,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (medias.isEmpty) return;
|
if (medias.isEmpty) return;
|
||||||
@ -337,7 +322,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> jumpToTrack(Track track) async {
|
Future<void> jumpToTrack(SpotubeTrackObject track) async {
|
||||||
final index =
|
final index =
|
||||||
state.tracks.toList().indexWhere((element) => element.id == track.id);
|
state.tracks.toList().indexWhere((element) => element.id == track.id);
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
|
@ -2,16 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/provider/discord_provider.dart';
|
import 'package:spotube/provider/discord_provider.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
import 'package:spotube/provider/skip_segments/skip_segments.dart';
|
import 'package:spotube/provider/skip_segments/skip_segments.dart';
|
||||||
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||||
import 'package:spotube/provider/server/sourced_track.dart';
|
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||||
@ -101,16 +102,18 @@ class AudioPlayerStreamListeners {
|
|||||||
|
|
||||||
/// The [Track] from Playlist.getTracks doesn't contain artist images
|
/// The [Track] from Playlist.getTracks doesn't contain artist images
|
||||||
/// so we need to fetch them from the API
|
/// so we need to fetch them from the API
|
||||||
final activeTrack =
|
var activeTrack = audioPlayerState.activeTrack!;
|
||||||
Track.fromJson(audioPlayerState.activeTrack!.toJson());
|
if (activeTrack.artists.any((a) => a.images == null)) {
|
||||||
if (audioPlayerState.activeTrack!.artists
|
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||||
?.any((a) => a.images == null) ??
|
final artists = await Future.wait(
|
||||||
false) {
|
activeTrack.artists
|
||||||
activeTrack.artists =
|
.map((artist) => metadataPlugin!.artist.getArtist(artist.id)),
|
||||||
await ref.read(spotifyProvider).api.artists.list([
|
);
|
||||||
for (final artist in audioPlayerState.activeTrack!.artists!)
|
activeTrack = activeTrack.copyWith(
|
||||||
artist.id!,
|
artists: artists
|
||||||
]).then((value) => value.toList());
|
.map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson()))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await history.addTrack(activeTrack);
|
await history.addTrack(activeTrack);
|
||||||
@ -127,24 +130,26 @@ class AudioPlayerStreamListeners {
|
|||||||
(event.inSeconds / max(audioPlayer.duration.inSeconds, 1)) * 100;
|
(event.inSeconds / max(audioPlayer.duration.inSeconds, 1)) * 100;
|
||||||
try {
|
try {
|
||||||
if (percentProgress < 80 ||
|
if (percentProgress < 80 ||
|
||||||
audioPlayerState.playlist.index == -1 ||
|
audioPlayerState.currentIndex == -1 ||
|
||||||
audioPlayerState.playlist.index ==
|
audioPlayerState.currentIndex ==
|
||||||
audioPlayerState.tracks.length - 1) {
|
audioPlayerState.tracks.length - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final nextTrack = SpotubeMedia.fromMedia(
|
final nextTrack = audioPlayerState.tracks
|
||||||
audioPlayerState.playlist.medias
|
.elementAt(audioPlayerState.currentIndex + 1);
|
||||||
.elementAt(audioPlayerState.playlist.index + 1),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
|
if (lastTrack == nextTrack.id || nextTrack is SpotubeLocalTrackObject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref.read(sourcedTrackProvider(nextTrack).future);
|
await ref.read(
|
||||||
|
trackSourcesProvider(
|
||||||
|
TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject),
|
||||||
|
).future,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
lastTrack = nextTrack.track.id!;
|
lastTrack = nextTrack.id;
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/sourced_track.dart';
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
|
||||||
|
|
||||||
final queryingTrackInfoProvider = Provider<bool>((ref) {
|
final queryingTrackInfoProvider = Provider<bool>((ref) {
|
||||||
final media = audioPlayer.playlist.index == -1 ||
|
final audioPlayer = ref.watch(audioPlayerProvider);
|
||||||
audioPlayer.playlist.medias.isEmpty
|
|
||||||
? null
|
|
||||||
: audioPlayer.playlist.medias.elementAtOrNull(audioPlayer.playlist.index);
|
|
||||||
final audioPlayerActiveTrack =
|
|
||||||
media == null ? null : SpotubeMedia.fromMedia(media);
|
|
||||||
|
|
||||||
final activeMedia = ref.watch(audioPlayerProvider.select(
|
if (audioPlayer.activeTrack == null) {
|
||||||
(s) => s.activeMedia == null
|
return false;
|
||||||
? null
|
}
|
||||||
: SpotubeMedia.fromMedia(s.activeMedia!),
|
|
||||||
)) ??
|
|
||||||
audioPlayerActiveTrack;
|
|
||||||
|
|
||||||
if (activeMedia == null) return false;
|
return ref
|
||||||
|
.watch(trackSourcesProvider(
|
||||||
return ref.watch(sourcedTrackProvider(activeMedia)).isLoading;
|
TrackSourceQuery.fromTrack(
|
||||||
|
audioPlayer.activeTrack! as SpotubeFullTrackObject),
|
||||||
|
))
|
||||||
|
.isLoading;
|
||||||
});
|
});
|
||||||
|
@ -1,104 +1,60 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
|
||||||
|
|
||||||
class AudioPlayerState {
|
part 'state.freezed.dart';
|
||||||
final bool playing;
|
part 'state.g.dart';
|
||||||
final PlaylistMode loopMode;
|
|
||||||
final bool shuffled;
|
|
||||||
final Playlist playlist;
|
|
||||||
|
|
||||||
final List<Track> tracks;
|
@freezed
|
||||||
final List<String> collections;
|
class AudioPlayerState with _$AudioPlayerState {
|
||||||
|
const AudioPlayerState._();
|
||||||
|
|
||||||
AudioPlayerState({
|
factory AudioPlayerState._inner({
|
||||||
required this.playing,
|
required bool playing,
|
||||||
required this.loopMode,
|
required PlaylistMode loopMode,
|
||||||
required this.shuffled,
|
required bool shuffled,
|
||||||
required this.playlist,
|
required List<String> collections,
|
||||||
required this.collections,
|
@Default(0) int currentIndex,
|
||||||
List<Track>? tracks,
|
@Default([]) List<SpotubeTrackObject> tracks,
|
||||||
}) : tracks = tracks ??
|
}) = _AudioPlayerState;
|
||||||
playlist.medias
|
|
||||||
.map((media) => SpotubeMedia.fromMedia(media).track)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
factory AudioPlayerState.fromJson(Map<String, dynamic> json) {
|
factory AudioPlayerState({
|
||||||
return AudioPlayerState(
|
required bool playing,
|
||||||
playing: json['playing'],
|
required PlaylistMode loopMode,
|
||||||
loopMode: PlaylistMode.values.firstWhere(
|
required bool shuffled,
|
||||||
(e) => e.name == json['loopMode'],
|
required List<String> collections,
|
||||||
orElse: () => audioPlayer.loopMode,
|
int currentIndex = 0,
|
||||||
),
|
List<SpotubeTrackObject> tracks = const [],
|
||||||
shuffled: json['shuffled'],
|
|
||||||
playlist: Playlist(
|
|
||||||
json['playlist']['medias']
|
|
||||||
.map(
|
|
||||||
(media) => SpotubeMedia.fromMedia(Media(
|
|
||||||
media['uri'],
|
|
||||||
extras: media['extras'],
|
|
||||||
httpHeaders: media['httpHeaders'],
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.cast<Media>()
|
|
||||||
.toList(),
|
|
||||||
index: json['playlist']['index'],
|
|
||||||
),
|
|
||||||
collections: List<String>.from(json['collections']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'playing': playing,
|
|
||||||
'loopMode': loopMode.name,
|
|
||||||
'shuffled': shuffled,
|
|
||||||
'playlist': {
|
|
||||||
'medias': playlist.medias
|
|
||||||
.map((media) => {
|
|
||||||
'uri': media.uri,
|
|
||||||
'extras': media.extras,
|
|
||||||
'httpHeaders': media.httpHeaders,
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
'index': playlist.index,
|
|
||||||
},
|
|
||||||
'collections': collections,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioPlayerState copyWith({
|
|
||||||
bool? playing,
|
|
||||||
PlaylistMode? loopMode,
|
|
||||||
bool? shuffled,
|
|
||||||
Playlist? playlist,
|
|
||||||
List<String>? collections,
|
|
||||||
}) {
|
}) {
|
||||||
return AudioPlayerState(
|
assert(
|
||||||
playing: playing ?? this.playing,
|
tracks.every((track) =>
|
||||||
loopMode: loopMode ?? this.loopMode,
|
track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject),
|
||||||
shuffled: shuffled ?? this.shuffled,
|
'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
|
||||||
playlist: playlist ?? this.playlist,
|
);
|
||||||
collections: collections ?? this.collections,
|
|
||||||
tracks: playlist == null ? tracks : null,
|
return AudioPlayerState._inner(
|
||||||
|
playing: playing,
|
||||||
|
loopMode: loopMode,
|
||||||
|
shuffled: shuffled,
|
||||||
|
currentIndex: currentIndex,
|
||||||
|
tracks: tracks,
|
||||||
|
collections: collections,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Track? get activeTrack {
|
factory AudioPlayerState.fromJson(Map<String, dynamic> json) =>
|
||||||
if (playlist.index == -1) return null;
|
_$AudioPlayerStateFromJson(json);
|
||||||
return tracks.elementAtOrNull(playlist.index);
|
|
||||||
|
SpotubeTrackObject? get activeTrack {
|
||||||
|
if (currentIndex < 0 || currentIndex >= tracks.length) return null;
|
||||||
|
return tracks[currentIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
Media? get activeMedia {
|
bool containsTrack(SpotubeTrackObject track) {
|
||||||
if (playlist.index == -1 || playlist.medias.isEmpty) return null;
|
|
||||||
return playlist.medias.elementAt(playlist.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool containsTrack(Track track) {
|
|
||||||
return tracks.any((t) => t.id == track.id);
|
return tracks.any((t) => t.id == track.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool containsTracks(List<Track> tracks) {
|
bool containsTracks(List<SpotubeTrackObject> tracks) {
|
||||||
return tracks.every(containsTrack);
|
return tracks.every(containsTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
297
lib/provider/audio_player/state.freezed.dart
Normal file
297
lib/provider/audio_player/state.freezed.dart
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
AudioPlayerState _$AudioPlayerStateFromJson(Map<String, dynamic> json) {
|
||||||
|
return _AudioPlayerState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$AudioPlayerState {
|
||||||
|
bool get playing => throw _privateConstructorUsedError;
|
||||||
|
PlaylistMode get loopMode => throw _privateConstructorUsedError;
|
||||||
|
bool get shuffled => throw _privateConstructorUsedError;
|
||||||
|
List<String> get collections => throw _privateConstructorUsedError;
|
||||||
|
int get currentIndex => throw _privateConstructorUsedError;
|
||||||
|
List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this AudioPlayerState to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of AudioPlayerState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$AudioPlayerStateCopyWith<AudioPlayerState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $AudioPlayerStateCopyWith<$Res> {
|
||||||
|
factory $AudioPlayerStateCopyWith(
|
||||||
|
AudioPlayerState value, $Res Function(AudioPlayerState) then) =
|
||||||
|
_$AudioPlayerStateCopyWithImpl<$Res, AudioPlayerState>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{bool playing,
|
||||||
|
PlaylistMode loopMode,
|
||||||
|
bool shuffled,
|
||||||
|
List<String> collections,
|
||||||
|
int currentIndex,
|
||||||
|
List<SpotubeTrackObject> tracks});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$AudioPlayerStateCopyWithImpl<$Res, $Val extends AudioPlayerState>
|
||||||
|
implements $AudioPlayerStateCopyWith<$Res> {
|
||||||
|
_$AudioPlayerStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of AudioPlayerState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? playing = null,
|
||||||
|
Object? loopMode = null,
|
||||||
|
Object? shuffled = null,
|
||||||
|
Object? collections = null,
|
||||||
|
Object? currentIndex = null,
|
||||||
|
Object? tracks = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
playing: null == playing
|
||||||
|
? _value.playing
|
||||||
|
: playing // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
loopMode: null == loopMode
|
||||||
|
? _value.loopMode
|
||||||
|
: loopMode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PlaylistMode,
|
||||||
|
shuffled: null == shuffled
|
||||||
|
? _value.shuffled
|
||||||
|
: shuffled // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
collections: null == collections
|
||||||
|
? _value.collections
|
||||||
|
: collections // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
|
currentIndex: null == currentIndex
|
||||||
|
? _value.currentIndex
|
||||||
|
: currentIndex // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
tracks: null == tracks
|
||||||
|
? _value.tracks
|
||||||
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SpotubeTrackObject>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AudioPlayerStateImplCopyWith<$Res>
|
||||||
|
implements $AudioPlayerStateCopyWith<$Res> {
|
||||||
|
factory _$$AudioPlayerStateImplCopyWith(_$AudioPlayerStateImpl value,
|
||||||
|
$Res Function(_$AudioPlayerStateImpl) then) =
|
||||||
|
__$$AudioPlayerStateImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{bool playing,
|
||||||
|
PlaylistMode loopMode,
|
||||||
|
bool shuffled,
|
||||||
|
List<String> collections,
|
||||||
|
int currentIndex,
|
||||||
|
List<SpotubeTrackObject> tracks});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AudioPlayerStateImplCopyWithImpl<$Res>
|
||||||
|
extends _$AudioPlayerStateCopyWithImpl<$Res, _$AudioPlayerStateImpl>
|
||||||
|
implements _$$AudioPlayerStateImplCopyWith<$Res> {
|
||||||
|
__$$AudioPlayerStateImplCopyWithImpl(_$AudioPlayerStateImpl _value,
|
||||||
|
$Res Function(_$AudioPlayerStateImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of AudioPlayerState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? playing = null,
|
||||||
|
Object? loopMode = null,
|
||||||
|
Object? shuffled = null,
|
||||||
|
Object? collections = null,
|
||||||
|
Object? currentIndex = null,
|
||||||
|
Object? tracks = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$AudioPlayerStateImpl(
|
||||||
|
playing: null == playing
|
||||||
|
? _value.playing
|
||||||
|
: playing // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
loopMode: null == loopMode
|
||||||
|
? _value.loopMode
|
||||||
|
: loopMode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PlaylistMode,
|
||||||
|
shuffled: null == shuffled
|
||||||
|
? _value.shuffled
|
||||||
|
: shuffled // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
collections: null == collections
|
||||||
|
? _value._collections
|
||||||
|
: collections // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
|
currentIndex: null == currentIndex
|
||||||
|
? _value.currentIndex
|
||||||
|
: currentIndex // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
tracks: null == tracks
|
||||||
|
? _value._tracks
|
||||||
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SpotubeTrackObject>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$AudioPlayerStateImpl extends _AudioPlayerState {
|
||||||
|
_$AudioPlayerStateImpl(
|
||||||
|
{required this.playing,
|
||||||
|
required this.loopMode,
|
||||||
|
required this.shuffled,
|
||||||
|
required final List<String> collections,
|
||||||
|
this.currentIndex = 0,
|
||||||
|
final List<SpotubeTrackObject> tracks = const []})
|
||||||
|
: _collections = collections,
|
||||||
|
_tracks = tracks,
|
||||||
|
super._();
|
||||||
|
|
||||||
|
factory _$AudioPlayerStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$AudioPlayerStateImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool playing;
|
||||||
|
@override
|
||||||
|
final PlaylistMode loopMode;
|
||||||
|
@override
|
||||||
|
final bool shuffled;
|
||||||
|
final List<String> _collections;
|
||||||
|
@override
|
||||||
|
List<String> get collections {
|
||||||
|
if (_collections is EqualUnmodifiableListView) return _collections;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_collections);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int currentIndex;
|
||||||
|
final List<SpotubeTrackObject> _tracks;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
List<SpotubeTrackObject> get tracks {
|
||||||
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AudioPlayerState._inner(playing: $playing, loopMode: $loopMode, shuffled: $shuffled, collections: $collections, currentIndex: $currentIndex, tracks: $tracks)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AudioPlayerStateImpl &&
|
||||||
|
(identical(other.playing, playing) || other.playing == playing) &&
|
||||||
|
(identical(other.loopMode, loopMode) ||
|
||||||
|
other.loopMode == loopMode) &&
|
||||||
|
(identical(other.shuffled, shuffled) ||
|
||||||
|
other.shuffled == shuffled) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._collections, _collections) &&
|
||||||
|
(identical(other.currentIndex, currentIndex) ||
|
||||||
|
other.currentIndex == currentIndex) &&
|
||||||
|
const DeepCollectionEquality().equals(other._tracks, _tracks));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
playing,
|
||||||
|
loopMode,
|
||||||
|
shuffled,
|
||||||
|
const DeepCollectionEquality().hash(_collections),
|
||||||
|
currentIndex,
|
||||||
|
const DeepCollectionEquality().hash(_tracks));
|
||||||
|
|
||||||
|
/// Create a copy of AudioPlayerState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith =>
|
||||||
|
__$$AudioPlayerStateImplCopyWithImpl<_$AudioPlayerStateImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$AudioPlayerStateImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _AudioPlayerState extends AudioPlayerState {
|
||||||
|
factory _AudioPlayerState(
|
||||||
|
{required final bool playing,
|
||||||
|
required final PlaylistMode loopMode,
|
||||||
|
required final bool shuffled,
|
||||||
|
required final List<String> collections,
|
||||||
|
final int currentIndex,
|
||||||
|
final List<SpotubeTrackObject> tracks}) = _$AudioPlayerStateImpl;
|
||||||
|
_AudioPlayerState._() : super._();
|
||||||
|
|
||||||
|
factory _AudioPlayerState.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$AudioPlayerStateImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get playing;
|
||||||
|
@override
|
||||||
|
PlaylistMode get loopMode;
|
||||||
|
@override
|
||||||
|
bool get shuffled;
|
||||||
|
@override
|
||||||
|
List<String> get collections;
|
||||||
|
@override
|
||||||
|
int get currentIndex;
|
||||||
|
@override
|
||||||
|
List<SpotubeTrackObject> get tracks;
|
||||||
|
|
||||||
|
/// Create a copy of AudioPlayerState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
40
lib/provider/audio_player/state.g.dart
Normal file
40
lib/provider/audio_player/state.g.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$AudioPlayerStateImpl _$$AudioPlayerStateImplFromJson(Map json) =>
|
||||||
|
_$AudioPlayerStateImpl(
|
||||||
|
playing: json['playing'] as bool,
|
||||||
|
loopMode: $enumDecode(_$PlaylistModeEnumMap, json['loopMode']),
|
||||||
|
shuffled: json['shuffled'] as bool,
|
||||||
|
collections: (json['collections'] as List<dynamic>)
|
||||||
|
.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
currentIndex: (json['currentIndex'] as num?)?.toInt() ?? 0,
|
||||||
|
tracks: (json['tracks'] as List<dynamic>?)
|
||||||
|
?.map((e) => SpotubeTrackObject.fromJson(
|
||||||
|
Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$AudioPlayerStateImplToJson(
|
||||||
|
_$AudioPlayerStateImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'playing': instance.playing,
|
||||||
|
'loopMode': _$PlaylistModeEnumMap[instance.loopMode]!,
|
||||||
|
'shuffled': instance.shuffled,
|
||||||
|
'collections': instance.collections,
|
||||||
|
'currentIndex': instance.currentIndex,
|
||||||
|
'tracks': instance.tracks.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$PlaylistModeEnumMap = {
|
||||||
|
PlaylistMode.none: 'none',
|
||||||
|
PlaylistMode.single: 'single',
|
||||||
|
PlaylistMode.loop: 'loop',
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/current_playlist.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
|
||||||
class BlackListNotifier extends AsyncNotifier<List<BlacklistTableData>> {
|
class BlackListNotifier extends AsyncNotifier<List<BlacklistTableData>> {
|
||||||
@ -34,17 +34,15 @@ class BlackListNotifier extends AsyncNotifier<List<BlacklistTableData>> {
|
|||||||
.go();
|
.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool contains(TrackSimple track) {
|
bool contains(SpotubeTrackObject track) {
|
||||||
final containsTrack =
|
final containsTrack =
|
||||||
state.asData?.value.any((element) => element.elementId == track.id) ??
|
state.asData?.value.any((element) => element.elementId == track.id) ??
|
||||||
false;
|
false;
|
||||||
|
|
||||||
final containsTrackArtists = track.artists?.any(
|
final containsTrackArtists = track.artists.any(
|
||||||
(artist) =>
|
(artist) =>
|
||||||
state.asData?.value.any((el) => el.elementId == artist.id) ??
|
state.asData?.value.any((el) => el.elementId == artist.id) ?? false,
|
||||||
false,
|
);
|
||||||
) ??
|
|
||||||
false;
|
|
||||||
|
|
||||||
return containsTrack || containsTrackArtists;
|
return containsTrack || containsTrackArtists;
|
||||||
}
|
}
|
||||||
@ -56,18 +54,9 @@ class BlackListNotifier extends AsyncNotifier<List<BlacklistTableData>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Filters the non blacklisted tracks from the given [tracks]
|
/// Filters the non blacklisted tracks from the given [tracks]
|
||||||
Iterable<TrackSimple> filter(Iterable<TrackSimple> tracks) {
|
Iterable<SpotubeTrackObject> filter(Iterable<SpotubeTrackObject> tracks) {
|
||||||
return tracks.whereNot(contains).toList();
|
return tracks.whereNot(contains).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentPlaylist filterPlaylist(CurrentPlaylist playlist) {
|
|
||||||
return CurrentPlaylist(
|
|
||||||
id: playlist.id,
|
|
||||||
name: playlist.name,
|
|
||||||
thumbnail: playlist.thumbnail,
|
|
||||||
tracks: playlist.tracks.where((track) => !contains(track)).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final blacklistProvider =
|
final blacklistProvider =
|
||||||
|
@ -5,11 +5,11 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
@ -41,7 +41,8 @@ final queueProvider = StateProvider<AudioPlayerState>(
|
|||||||
playing: audioPlayer.isPlaying,
|
playing: audioPlayer.isPlaying,
|
||||||
loopMode: audioPlayer.loopMode,
|
loopMode: audioPlayer.loopMode,
|
||||||
shuffled: audioPlayer.isShuffled,
|
shuffled: audioPlayer.isShuffled,
|
||||||
playlist: audioPlayer.playlist,
|
tracks: [],
|
||||||
|
currentIndex: 0,
|
||||||
collections: [],
|
collections: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -207,7 +208,7 @@ class ConnectNotifier extends AsyncNotifier<ConnectState?> {
|
|||||||
emit(WebSocketLoopEvent(value));
|
emit(WebSocketLoopEvent(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(Track data) async {
|
Future<void> addTrack(SpotubeFullTrackObject data) async {
|
||||||
emit(WebSocketAddTrackEvent(data));
|
emit(WebSocketAddTrackEvent(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
|
import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -74,20 +73,20 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updatePresence(Track track) async {
|
Future<void> updatePresence(SpotubeTrackObject track) async {
|
||||||
if (!kIsDesktop) return;
|
if (!kIsDesktop) return;
|
||||||
if (FlutterDiscordRPC.instance.isConnected == false) return;
|
if (FlutterDiscordRPC.instance.isConnected == false) return;
|
||||||
final artistNames = track.artists?.asString();
|
final artistNames = track.artists.asString();
|
||||||
final isPlaying = audioPlayer.isPlaying;
|
final isPlaying = audioPlayer.isPlaying;
|
||||||
final position = audioPlayer.position;
|
final position = audioPlayer.position;
|
||||||
|
|
||||||
await FlutterDiscordRPC.instance.setActivity(
|
await FlutterDiscordRPC.instance.setActivity(
|
||||||
activity: RPCActivity(
|
activity: RPCActivity(
|
||||||
details: track.name,
|
details: track.name,
|
||||||
state: artistNames != null ? "by $artistNames" : null,
|
state: artistNames,
|
||||||
assets: RPCAssets(
|
assets: RPCAssets(
|
||||||
largeImage:
|
largeImage:
|
||||||
track.album?.images?.first.url ?? "spotube-logo-foreground",
|
track.album?.images.first.url ?? "spotube-logo-foreground",
|
||||||
largeText: track.album?.name ?? "Unknown album",
|
largeText: track.album?.name ?? "Unknown album",
|
||||||
smallImage: "spotube-logo-foreground",
|
smallImage: "spotube-logo-foreground",
|
||||||
smallText: "Spotube",
|
smallText: "Spotube",
|
||||||
@ -95,8 +94,7 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
|||||||
buttons: [
|
buttons: [
|
||||||
RPCButton(
|
RPCButton(
|
||||||
label: "Listen on Spotify",
|
label: "Listen on Spotify",
|
||||||
url: track.externalUrls?.spotify ??
|
url: track.externalUri,
|
||||||
"https://open.spotify.com/tracks/${track.id}",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
timestamps: RPCTimestamps(
|
timestamps: RPCTimestamps(
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/download_manager/download_manager.dart';
|
import 'package:spotube/services/download_manager/download_manager.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
@ -21,18 +20,22 @@ import 'package:spotube/utils/service_utils.dart';
|
|||||||
class DownloadManagerProvider extends ChangeNotifier {
|
class DownloadManagerProvider extends ChangeNotifier {
|
||||||
DownloadManagerProvider({required this.ref})
|
DownloadManagerProvider({required this.ref})
|
||||||
: $history = <SourcedTrack>{},
|
: $history = <SourcedTrack>{},
|
||||||
$backHistory = <Track>{},
|
$backHistory = <SpotubeFullTrackObject>{},
|
||||||
dl = DownloadManager() {
|
dl = DownloadManager() {
|
||||||
dl.statusStream.listen((event) async {
|
dl.statusStream.listen((event) async {
|
||||||
try {
|
try {
|
||||||
final (:request, :status) = event;
|
final (:request, :status) = event;
|
||||||
|
|
||||||
final track = $history.firstWhereOrNull(
|
final sourcedTrack = $history.firstWhereOrNull(
|
||||||
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
||||||
);
|
);
|
||||||
|
if (sourcedTrack == null) return;
|
||||||
|
final track = $backHistory.firstWhereOrNull(
|
||||||
|
(element) => element.id == sourcedTrack.query.id,
|
||||||
|
);
|
||||||
if (track == null) return;
|
if (track == null) return;
|
||||||
|
|
||||||
final savePath = getTrackFileUrl(track);
|
final savePath = getTrackFileUrl(sourcedTrack);
|
||||||
// related to onFileExists
|
// related to onFileExists
|
||||||
final oldFile = File("$savePath.old");
|
final oldFile = File("$savePath.old");
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final imageBytes = await ServiceUtils.downloadImage(
|
final imageBytes = await ServiceUtils.downloadImage(
|
||||||
(track.album?.images).asUrlString(
|
(track.album.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
index: 1,
|
index: 1,
|
||||||
),
|
),
|
||||||
@ -78,7 +81,8 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> Function(Track track) onFileExists = (Track track) async => true;
|
Future<bool> Function(SpotubeFullTrackObject track) onFileExists =
|
||||||
|
(SpotubeFullTrackObject track) async => true;
|
||||||
|
|
||||||
final Ref<DownloadManagerProvider> ref;
|
final Ref<DownloadManagerProvider> ref;
|
||||||
|
|
||||||
@ -99,21 +103,19 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
final Set<SourcedTrack> $history;
|
final Set<SourcedTrack> $history;
|
||||||
// these are the tracks which metadata hasn't been fetched yet
|
// these are the tracks which metadata hasn't been fetched yet
|
||||||
final Set<Track> $backHistory;
|
final Set<SpotubeFullTrackObject> $backHistory;
|
||||||
final DownloadManager dl;
|
final DownloadManager dl;
|
||||||
|
|
||||||
String getTrackFileUrl(Track track) {
|
String getTrackFileUrl(SourcedTrack track) {
|
||||||
final name =
|
final name =
|
||||||
"${track.name} - ${track.artists?.asString() ?? ""}.${downloadCodec.name}";
|
"${track.query.title} - ${track.query.artists.join(", ")}.${downloadCodec.name}";
|
||||||
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
|
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isActive(Track track) {
|
Future<bool> isActive(SpotubeFullTrackObject track) async {
|
||||||
if ($backHistory.contains(track)) return true;
|
if ($backHistory.contains(track)) return true;
|
||||||
|
|
||||||
final sourcedTrack = mapToSourcedTrack(track);
|
final sourcedTrack = await mapToSourcedTrack(track);
|
||||||
|
|
||||||
if (sourcedTrack == null) return false;
|
|
||||||
|
|
||||||
return dl
|
return dl
|
||||||
.getAllDownloads()
|
.getAllDownloads()
|
||||||
@ -128,8 +130,12 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// For singular downloads
|
/// For singular downloads
|
||||||
Future<void> addToQueue(Track track) async {
|
Future<void> addToQueue(SpotubeFullTrackObject track) async {
|
||||||
final savePath = getTrackFileUrl(track);
|
final sourcedTrack = await ref.read(
|
||||||
|
trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future,
|
||||||
|
);
|
||||||
|
|
||||||
|
final savePath = getTrackFileUrl(sourcedTrack);
|
||||||
|
|
||||||
final oldFile = File(savePath);
|
final oldFile = File(savePath);
|
||||||
if (await oldFile.exists() && !await onFileExists(track)) {
|
if (await oldFile.exists() && !await onFileExists(track)) {
|
||||||
@ -140,18 +146,21 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
await oldFile.rename("$savePath.old");
|
await oldFile.rename("$savePath.old");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track is SourcedTrack && track.codec == downloadCodec) {
|
if (sourcedTrack.codec == downloadCodec) {
|
||||||
final downloadTask =
|
final downloadTask = await dl.addDownload(
|
||||||
await dl.addDownload(track.getUrlOfCodec(downloadCodec), savePath);
|
sourcedTrack.getUrlOfCodec(downloadCodec), savePath);
|
||||||
if (downloadTask != null) {
|
if (downloadTask != null) {
|
||||||
$history.add(track);
|
$history.add(sourcedTrack);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$backHistory.add(track);
|
$backHistory.add(track);
|
||||||
final sourcedTrack = await SourcedTrack.fetchFromTrack(
|
final sourcedTrack = await ref
|
||||||
ref: ref,
|
.read(
|
||||||
track: track,
|
trackSourcesProvider(
|
||||||
).then((d) {
|
TrackSourceQuery.fromTrack(track),
|
||||||
|
).future,
|
||||||
|
)
|
||||||
|
.then((d) {
|
||||||
$backHistory.remove(track);
|
$backHistory.remove(track);
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
@ -167,10 +176,8 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> batchAddToQueue(List<Track> tracks) async {
|
Future<void> batchAddToQueue(List<SpotubeFullTrackObject> tracks) async {
|
||||||
$backHistory.addAll(
|
$backHistory.addAll(tracks);
|
||||||
tracks.where((element) => element is! SourcedTrack),
|
|
||||||
);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
for (final track in tracks) {
|
for (final track in tracks) {
|
||||||
try {
|
try {
|
||||||
@ -194,20 +201,23 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
$history.remove(track);
|
$history.remove(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pause(SourcedTrack track) {
|
Future<void> pause(SpotubeFullTrackObject track) async {
|
||||||
return dl.pauseDownload(track.getUrlOfCodec(downloadCodec));
|
final sourcedTrack = await mapToSourcedTrack(track);
|
||||||
|
return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resume(SourcedTrack track) {
|
Future<void> resume(SpotubeFullTrackObject track) async {
|
||||||
return dl.resumeDownload(track.getUrlOfCodec(downloadCodec));
|
final sourcedTrack = await mapToSourcedTrack(track);
|
||||||
|
return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> retry(SourcedTrack track) {
|
Future<void> retry(SpotubeFullTrackObject track) {
|
||||||
return addToQueue(track);
|
return addToQueue(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancel(SourcedTrack track) {
|
void cancel(SpotubeFullTrackObject track) async {
|
||||||
dl.cancelDownload(track.getUrlOfCodec(downloadCodec));
|
final sourcedTrack = await mapToSourcedTrack(track);
|
||||||
|
return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec));
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelAll() {
|
void cancelAll() {
|
||||||
@ -217,12 +227,19 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SourcedTrack? mapToSourcedTrack(Track track) {
|
Future<SourcedTrack> mapToSourcedTrack(SpotubeFullTrackObject track) async {
|
||||||
if (track is SourcedTrack) {
|
final historicTrack =
|
||||||
return track;
|
$history.firstWhereOrNull((element) => element.query.id == track.id);
|
||||||
} else {
|
|
||||||
return $history.firstWhereOrNull((element) => element.id == track.id);
|
if (historicTrack != null) {
|
||||||
|
return historicTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final sourcedTrack = await ref.read(
|
||||||
|
trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future,
|
||||||
|
);
|
||||||
|
|
||||||
|
return sourcedTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
ValueNotifier<DownloadStatus>? getStatusNotifier(SourcedTrack track) {
|
ValueNotifier<DownloadStatus>? getStatusNotifier(SourcedTrack track) {
|
||||||
|
@ -5,7 +5,7 @@ import 'package:home_widget/home_widget.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -71,7 +71,7 @@ Future<void> _updateWidget() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendActiveTrack(Track? track) async {
|
Future<void> _sendActiveTrack(SpotubeTrackObject? track) async {
|
||||||
if (track == null) {
|
if (track == null) {
|
||||||
await _saveWidgetData("activeTrack", null);
|
await _saveWidgetData("activeTrack", null);
|
||||||
await _updateWidget();
|
await _updateWidget();
|
||||||
@ -80,8 +80,8 @@ Future<void> _sendActiveTrack(Track? track) async {
|
|||||||
|
|
||||||
final jsonTrack = track.toJson();
|
final jsonTrack = track.toJson();
|
||||||
|
|
||||||
final image = track.album?.images?.first;
|
final image = track.album?.images.first;
|
||||||
final cachedImage = await DefaultCacheManager().getSingleFile(image!.url!);
|
final cachedImage = await DefaultCacheManager().getSingleFile(image!.url);
|
||||||
final data = {
|
final data = {
|
||||||
...jsonTrack,
|
...jsonTrack,
|
||||||
"album": {
|
"album": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
|
||||||
class PlaybackHistoryActions {
|
class PlaybackHistoryActions {
|
||||||
@ -16,31 +16,31 @@ class PlaybackHistoryActions {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addPlaylists(List<PlaylistSimple> playlists) async {
|
Future<void> addPlaylists(List<SpotubeSimplePlaylistObject> playlists) async {
|
||||||
await _batchInsertHistoryEntries([
|
await _batchInsertHistoryEntries([
|
||||||
for (final playlist in playlists)
|
for (final playlist in playlists)
|
||||||
HistoryTableCompanion.insert(
|
HistoryTableCompanion.insert(
|
||||||
type: HistoryEntryType.playlist,
|
type: HistoryEntryType.playlist,
|
||||||
itemId: playlist.id!,
|
itemId: playlist.id,
|
||||||
data: playlist.toJson(),
|
data: playlist.toJson(),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAlbums(List<AlbumSimple> albums) async {
|
Future<void> addAlbums(List<SpotubeSimpleAlbumObject> albums) async {
|
||||||
await _batchInsertHistoryEntries([
|
await _batchInsertHistoryEntries([
|
||||||
for (final albums in albums)
|
for (final albums in albums)
|
||||||
HistoryTableCompanion.insert(
|
HistoryTableCompanion.insert(
|
||||||
type: HistoryEntryType.album,
|
type: HistoryEntryType.album,
|
||||||
itemId: albums.id!,
|
itemId: albums.id,
|
||||||
data: albums.toJson(),
|
data: albums.toJson(),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTracks(List<Track> tracks) async {
|
Future<void> addTracks(List<SpotubeTrackObject> tracks) async {
|
||||||
assert(
|
assert(
|
||||||
tracks.every((t) => t.artists?.every((a) => a.images != null) ?? false),
|
tracks.every((t) => t.artists.every((a) => a.images != null)),
|
||||||
'Track artists must have images',
|
'Track artists must have images',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -48,22 +48,22 @@ class PlaybackHistoryActions {
|
|||||||
for (final track in tracks)
|
for (final track in tracks)
|
||||||
HistoryTableCompanion.insert(
|
HistoryTableCompanion.insert(
|
||||||
type: HistoryEntryType.track,
|
type: HistoryEntryType.track,
|
||||||
itemId: track.id!,
|
itemId: track.id,
|
||||||
data: track.toJson(),
|
data: track.toJson(),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(Track track) async {
|
Future<void> addTrack(SpotubeTrackObject track) async {
|
||||||
assert(
|
assert(
|
||||||
track.artists?.every((a) => a.images != null) ?? false,
|
track.artists.every((a) => a.images != null),
|
||||||
'Track artists must have images',
|
'Track artists must have images',
|
||||||
);
|
);
|
||||||
|
|
||||||
await _db.into(_db.historyTable).insert(
|
await _db.into(_db.historyTable).insert(
|
||||||
HistoryTableCompanion.insert(
|
HistoryTableCompanion.insert(
|
||||||
type: HistoryEntryType.track,
|
type: HistoryEntryType.track,
|
||||||
itemId: track.id!,
|
itemId: track.id,
|
||||||
data: track.toJson(),
|
data: track.toJson(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -3,16 +3,15 @@ import 'dart:async';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/env.dart';
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
||||||
final StreamController<Track> _scrobbleController =
|
final StreamController<SpotubeTrackObject> _scrobbleController =
|
||||||
StreamController<Track>.broadcast();
|
StreamController<SpotubeTrackObject>.broadcast();
|
||||||
@override
|
@override
|
||||||
build() async {
|
build() async {
|
||||||
final database = ref.watch(databaseProvider);
|
final database = ref.watch(databaseProvider);
|
||||||
@ -47,13 +46,12 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
|||||||
_scrobbleController.stream.listen((track) async {
|
_scrobbleController.stream.listen((track) async {
|
||||||
try {
|
try {
|
||||||
await state.asData?.value?.track.scrobble(
|
await state.asData?.value?.track.scrobble(
|
||||||
artist: track.artists!.first.name!,
|
artist: track.artists.first.name,
|
||||||
track: track.name!,
|
track: track.name,
|
||||||
album: track.album!.name!,
|
album: track.album!.name,
|
||||||
chosenByUser: true,
|
chosenByUser: true,
|
||||||
duration: track.duration,
|
duration: Duration(milliseconds: track.durationMs),
|
||||||
timestamp: DateTime.now().toUtc(),
|
timestamp: DateTime.now().toUtc(),
|
||||||
trackNumber: track.trackNumber,
|
|
||||||
);
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
AppLogger.reportError(e, stackTrace);
|
AppLogger.reportError(e, stackTrace);
|
||||||
@ -109,21 +107,21 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
|||||||
await database.delete(database.scrobblerTable).go();
|
await database.delete(database.scrobblerTable).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
void scrobble(Track track) {
|
void scrobble(SpotubeTrackObject track) {
|
||||||
_scrobbleController.add(track);
|
_scrobbleController.add(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> love(Track track) async {
|
Future<void> love(SpotubeTrackObject track) async {
|
||||||
await state.asData?.value?.track.love(
|
await state.asData?.value?.track.love(
|
||||||
artist: track.artists!.asString(),
|
artist: track.artists.asString(),
|
||||||
track: track.name!,
|
track: track.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> unlove(Track track) async {
|
Future<void> unlove(SpotubeTrackObject track) async {
|
||||||
await state.asData?.value?.track.unLove(
|
await state.asData?.value?.track.unLove(
|
||||||
artist: track.artists!.asString(),
|
artist: track.artists.asString(),
|
||||||
track: track.name!,
|
track: track.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
|
|
||||||
class ActiveSourcedTrackNotifier extends Notifier<SourcedTrack?> {
|
|
||||||
@override
|
|
||||||
build() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void update(SourcedTrack? sourcedTrack) {
|
|
||||||
state = sourcedTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> populateSibling() async {
|
|
||||||
if (state == null) return;
|
|
||||||
state = await state!.copyWithSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> swapSibling(SourceInfo sibling) async {
|
|
||||||
if (state == null) return;
|
|
||||||
await populateSibling();
|
|
||||||
final newTrack = await state!.swapWithSibling(sibling);
|
|
||||||
if (newTrack == null) return;
|
|
||||||
|
|
||||||
state = newTrack;
|
|
||||||
await audioPlayer.pause();
|
|
||||||
|
|
||||||
final playbackNotifier = ref.read(audioPlayerProvider.notifier);
|
|
||||||
final oldActiveIndex = audioPlayer.currentIndex;
|
|
||||||
|
|
||||||
await playbackNotifier.addTracksAtFirst([newTrack], allowDuplicates: true);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
|
||||||
await playbackNotifier.jumpToTrack(newTrack);
|
|
||||||
|
|
||||||
await audioPlayer.removeTrack(oldActiveIndex);
|
|
||||||
|
|
||||||
await audioPlayer.resume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final activeSourcedTrackProvider =
|
|
||||||
NotifierProvider<ActiveSourcedTrackNotifier, SourcedTrack?>(
|
|
||||||
() => ActiveSourcedTrackNotifier(),
|
|
||||||
);
|
|
42
lib/provider/server/active_track_sources.dart
Normal file
42
lib/provider/server/active_track_sources.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
|
final activeTrackSourcesProvider = FutureProvider<
|
||||||
|
({
|
||||||
|
SourcedTrack? source,
|
||||||
|
TrackSourcesNotifier? notifier,
|
||||||
|
SpotubeTrackObject track,
|
||||||
|
})?>((ref) async {
|
||||||
|
final audioPlayerState = ref.watch(audioPlayerProvider);
|
||||||
|
|
||||||
|
if (audioPlayerState.activeTrack == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioPlayerState.activeTrack is SpotubeLocalTrackObject) {
|
||||||
|
return (
|
||||||
|
source: null,
|
||||||
|
notifier: null,
|
||||||
|
track: audioPlayerState.activeTrack!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final trackQuery = TrackSourceQuery.fromTrack(
|
||||||
|
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
||||||
|
);
|
||||||
|
|
||||||
|
final sourcedTrack = await ref.watch(trackSourcesProvider(trackQuery).future);
|
||||||
|
final sourcedTrackNotifier = ref.watch(
|
||||||
|
trackSourcesProvider(trackQuery).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
source: sourcedTrack,
|
||||||
|
track: audioPlayerState.activeTrack!,
|
||||||
|
notifier: sourcedTrackNotifier,
|
||||||
|
);
|
||||||
|
});
|
@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
@ -161,19 +161,19 @@ class ServerConnectRoutes {
|
|||||||
|
|
||||||
event.onLoad((event) async {
|
event.onLoad((event) async {
|
||||||
await audioPlayerNotifier.load(
|
await audioPlayerNotifier.load(
|
||||||
event.data.tracks,
|
event.data.tracks as List<SpotubeFullTrackObject>,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
initialIndex: event.data.initialIndex ?? 0,
|
initialIndex: event.data.initialIndex ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.data.collectionId == null) return;
|
if (event.data.collectionId == null) return;
|
||||||
audioPlayerNotifier.addCollection(event.data.collectionId!);
|
audioPlayerNotifier.addCollection(event.data.collectionId!);
|
||||||
if (event.data.collection is AlbumSimple) {
|
if (event.data.collection is SpotubeSimpleAlbumObject) {
|
||||||
historyNotifier
|
historyNotifier.addAlbums(
|
||||||
.addAlbums([event.data.collection as AlbumSimple]);
|
[event.data.collection as SpotubeSimpleAlbumObject]);
|
||||||
} else {
|
} else {
|
||||||
historyNotifier.addPlaylists(
|
historyNotifier.addPlaylists(
|
||||||
[event.data.collection as PlaylistSimple]);
|
[event.data.collection as SpotubeSimplePlaylistObject]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart' hide Response;
|
import 'package:dio/dio.dart' hide Response;
|
||||||
import 'package:dio/dio.dart' as dio_lib;
|
import 'package:dio/dio.dart' as dio_lib;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -8,15 +9,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/models/parser/range_headers.dart';
|
import 'package:spotube/models/parser/range_headers.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||||
import 'package:spotube/provider/server/sourced_track.dart';
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
@ -55,7 +55,7 @@ class ServerPlaybackRoutes {
|
|||||||
join(
|
join(
|
||||||
await UserPreferencesNotifier.getMusicCacheDir(),
|
await UserPreferencesNotifier.getMusicCacheDir(),
|
||||||
ServiceUtils.sanitizeFilename(
|
ServiceUtils.sanitizeFilename(
|
||||||
'${track.name} - ${track.artists?.asString()} (${track.sourceInfo.id}).${track.codec.name}',
|
'${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -127,16 +127,16 @@ class ServerPlaybackRoutes {
|
|||||||
.catchError((e, stack) async {
|
.catchError((e, stack) async {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
final sourcedTrack = await ref
|
final sourcedTrack = await ref
|
||||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
.read(trackSourcesProvider(track.query).notifier)
|
||||||
.refreshStreamingUrl();
|
.refreshStreamingUrl();
|
||||||
|
|
||||||
if (playlist.activeTrack?.id == sourcedTrack?.id &&
|
// It gets updated by itself.
|
||||||
sourcedTrack != null) {
|
// if (playlist.activeTrack?.id == sourcedTrack.query.id) {
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
// ref.read(activeTrackSourcesProvider.notifier).update(sourcedTrack);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return await dio.get<Uint8List>(
|
return await dio.get<Uint8List>(
|
||||||
sourcedTrack!.url,
|
sourcedTrack.url,
|
||||||
options: options.copyWith(headers: {
|
options: options.copyWith(headers: {
|
||||||
...?options.headers,
|
...?options.headers,
|
||||||
"user-agent": _randomUserAgent,
|
"user-agent": _randomUserAgent,
|
||||||
@ -174,8 +174,18 @@ class ServerPlaybackRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) {
|
if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) {
|
||||||
|
final playlistTrack = playlist.tracks.firstWhereOrNull(
|
||||||
|
(element) => element.id == track.query.id,
|
||||||
|
);
|
||||||
|
if (playlistTrack == null) {
|
||||||
|
AppLogger.log.e(
|
||||||
|
"Track ${track.query.id} not found in playlist, cannot write metadata.",
|
||||||
|
);
|
||||||
|
return (response: res, bytes: bytes);
|
||||||
|
}
|
||||||
|
|
||||||
final imageBytes = await ServiceUtils.downloadImage(
|
final imageBytes = await ServiceUtils.downloadImage(
|
||||||
(track.album?.images).asUrlString(
|
(playlistTrack.album?.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
index: 1,
|
index: 1,
|
||||||
),
|
),
|
||||||
@ -183,9 +193,9 @@ class ServerPlaybackRoutes {
|
|||||||
|
|
||||||
await MetadataGod.writeMetadata(
|
await MetadataGod.writeMetadata(
|
||||||
file: trackCacheFile.path,
|
file: trackCacheFile.path,
|
||||||
metadata: track.toMetadata(
|
metadata: (playlistTrack as SpotubeFullTrackObject).toMetadata(
|
||||||
fileLength: fileLength,
|
|
||||||
imageBytes: imageBytes,
|
imageBytes: imageBytes,
|
||||||
|
fileLength: fileLength,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -199,15 +209,21 @@ class ServerPlaybackRoutes {
|
|||||||
final track =
|
final track =
|
||||||
playlist.tracks.firstWhere((element) => element.id == trackId);
|
playlist.tracks.firstWhere((element) => element.id == trackId);
|
||||||
|
|
||||||
final activeSourcedTrack = ref.read(activeSourcedTrackProvider);
|
final activeSourcedTrack =
|
||||||
final sourcedTrack = activeSourcedTrack?.id == track.id
|
await ref.read(activeTrackSourcesProvider.future);
|
||||||
? activeSourcedTrack
|
final sourcedTrack = activeSourcedTrack?.track.id == track.id
|
||||||
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
|
? activeSourcedTrack?.source
|
||||||
|
: await ref.read(
|
||||||
|
trackSourcesProvider(
|
||||||
|
TrackSourceQuery.parseUri(request.url.toString()),
|
||||||
|
).future,
|
||||||
|
);
|
||||||
|
|
||||||
if (playlist.activeTrack?.id == sourcedTrack?.id &&
|
// This will be automatically updated by the notifier.
|
||||||
sourcedTrack != null) {
|
// if (playlist.activeTrack?.id == sourcedTrack?.query.id &&
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
// sourcedTrack != null) {
|
||||||
}
|
// ref.read(activeTrackSourcesProvider.notifier).update(sourcedTrack);
|
||||||
|
// }
|
||||||
|
|
||||||
final (bytes: audioBytes, response: res) =
|
final (bytes: audioBytes, response: res) =
|
||||||
await streamTrack(sourcedTrack!, request.headers);
|
await streamTrack(sourcedTrack!, request.headers);
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
|
|
||||||
class SourcedTrackNotifier
|
|
||||||
extends FamilyAsyncNotifier<SourcedTrack?, SpotubeMedia?> {
|
|
||||||
@override
|
|
||||||
build(media) async {
|
|
||||||
final track = media?.track;
|
|
||||||
if (track == null || track is LocalTrack) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.listen(
|
|
||||||
audioPlayerProvider.select((value) => value.tracks),
|
|
||||||
(old, next) {
|
|
||||||
if (next.isEmpty || next.none((element) => element.id == track.id)) {
|
|
||||||
ref.invalidateSelf();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final sourcedTrack =
|
|
||||||
await SourcedTrack.fetchFromTrack(track: track, ref: ref);
|
|
||||||
|
|
||||||
return sourcedTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SourcedTrack?> refreshStreamingUrl() async {
|
|
||||||
if (arg == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await update((prev) async {
|
|
||||||
return await SourcedTrack.fetchFromTrack(
|
|
||||||
track: state.value!,
|
|
||||||
ref: ref,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
|
|
||||||
SourcedTrack?, SpotubeMedia?>(
|
|
||||||
() => SourcedTrackNotifier(),
|
|
||||||
);
|
|
45
lib/provider/server/track_sources.dart
Normal file
45
lib/provider/server/track_sources.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
|
class TrackSourcesNotifier
|
||||||
|
extends FamilyAsyncNotifier<SourcedTrack, TrackSourceQuery> {
|
||||||
|
@override
|
||||||
|
FutureOr<SourcedTrack> build(query) {
|
||||||
|
ref.watch(userPreferencesProvider.select((p) => p.audioQuality));
|
||||||
|
ref.watch(userPreferencesProvider.select((p) => p.audioSource));
|
||||||
|
ref.watch(userPreferencesProvider.select((p) => p.streamMusicCodec));
|
||||||
|
ref.watch(userPreferencesProvider.select((p) => p.downloadMusicCodec));
|
||||||
|
|
||||||
|
return SourcedTrack.fetchFromQuery(query: query, ref: ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SourcedTrack> refreshStreamingUrl() async {
|
||||||
|
return await update((prev) async {
|
||||||
|
return await prev.refreshStream();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SourcedTrack> copyWithSibling(
|
||||||
|
TrackSourceInfo info,
|
||||||
|
TrackSourceQuery query,
|
||||||
|
) async {
|
||||||
|
return await update((prev) async {
|
||||||
|
return prev.copyWithSibling();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SourcedTrack> swapWithSibling(TrackSourceInfo sibling) async {
|
||||||
|
return await update((prev) async {
|
||||||
|
return await prev.swapWithSibling(sibling) ?? prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final trackSourcesProvider = AsyncNotifierProviderFamily<TrackSourcesNotifier,
|
||||||
|
SourcedTrack, TrackSourceQuery>(
|
||||||
|
() => TrackSourcesNotifier(),
|
||||||
|
);
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:spotube/models/database/database.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/database/database.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/dio/dio.dart';
|
import 'package:spotube/services/dio/dio.dart';
|
||||||
@ -81,8 +82,11 @@ Future<List<SkipSegmentTableData>> getAndCacheSkipSegments(
|
|||||||
|
|
||||||
final segmentProvider = FutureProvider<SourcedSegments?>(
|
final segmentProvider = FutureProvider<SourcedSegments?>(
|
||||||
(ref) async {
|
(ref) async {
|
||||||
final track = ref.watch(activeSourcedTrackProvider);
|
final snapshot = await ref.watch(activeTrackSourcesProvider.future);
|
||||||
if (track == null) return null;
|
if (snapshot == null) return null;
|
||||||
|
final (:track, :source, :notifier) = snapshot;
|
||||||
|
if (track is SpotubeLocalTrackObject) return null;
|
||||||
|
if (source!.source case AudioSource.jiosaavn) return null;
|
||||||
|
|
||||||
final skipNonMusic = ref.watch(
|
final skipNonMusic = ref.watch(
|
||||||
userPreferencesProvider.select(
|
userPreferencesProvider.select(
|
||||||
@ -96,16 +100,13 @@ final segmentProvider = FutureProvider<SourcedSegments?>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!skipNonMusic) {
|
if (!skipNonMusic) {
|
||||||
return SourcedSegments(
|
return SourcedSegments(segments: [], source: source.info.id);
|
||||||
segments: [],
|
|
||||||
source: track.sourceInfo.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final segments = await getAndCacheSkipSegments(track.sourceInfo.id, ref);
|
final segments = await getAndCacheSkipSegments(source.info.id, ref);
|
||||||
|
|
||||||
return SourcedSegments(
|
return SourcedSegments(
|
||||||
source: track.sourceInfo.id,
|
source: source.info.id,
|
||||||
segments: segments,
|
segments: segments,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Playlist;
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/services/audio_player/custom_player.dart';
|
import 'package:spotube/services/audio_player/custom_player.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -11,33 +12,41 @@ import 'dart:async';
|
|||||||
import 'package:media_kit/media_kit.dart' as mk;
|
import 'package:media_kit/media_kit.dart' as mk;
|
||||||
|
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
part 'audio_players_streams_mixin.dart';
|
part 'audio_players_streams_mixin.dart';
|
||||||
part 'audio_player_impl.dart';
|
part 'audio_player_impl.dart';
|
||||||
|
|
||||||
class SpotubeMedia extends mk.Media {
|
class SpotubeMedia extends mk.Media {
|
||||||
final Track track;
|
|
||||||
|
|
||||||
static int serverPort = 0;
|
static int serverPort = 0;
|
||||||
|
|
||||||
|
final SpotubeTrackObject track;
|
||||||
|
|
||||||
|
static String get _host =>
|
||||||
|
kIsWindows ? "localhost" : InternetAddress.anyIPv4.address;
|
||||||
|
|
||||||
|
static String _queries(SpotubeFullTrackObject track) {
|
||||||
|
final params = TrackSourceQuery.fromTrack(track).toJson();
|
||||||
|
|
||||||
|
return params.entries
|
||||||
|
.map((e) =>
|
||||||
|
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}")
|
||||||
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
SpotubeMedia(
|
SpotubeMedia(
|
||||||
this.track, {
|
this.track, {
|
||||||
Map<String, dynamic>? extras,
|
Map<String, dynamic>? extras,
|
||||||
super.httpHeaders,
|
super.httpHeaders,
|
||||||
}) : super(
|
}) : assert(
|
||||||
track is LocalTrack
|
track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject,
|
||||||
|
"Track must be a either a local track or a full track object with ISRC",
|
||||||
|
),
|
||||||
|
// If the track is a local track, use its path, otherwise use the server URL
|
||||||
|
super(
|
||||||
|
track is SpotubeLocalTrackObject
|
||||||
? track.path
|
? track.path
|
||||||
: "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}",
|
: "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}",
|
||||||
extras: {
|
|
||||||
...?extras,
|
|
||||||
"track": switch (track) {
|
|
||||||
LocalTrack() => track.toJson(),
|
|
||||||
SourcedTrack() => track.toJson(),
|
|
||||||
_ => track.toJson(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -46,23 +55,11 @@ class SpotubeMedia extends mk.Media {
|
|||||||
/// [super.uri] must be used instead of [track.path] to prevent wrong
|
/// [super.uri] must be used instead of [track.path] to prevent wrong
|
||||||
/// path format exceptions in Windows causing [extras] to be null
|
/// path format exceptions in Windows causing [extras] to be null
|
||||||
LocalTrack() => super.uri,
|
LocalTrack() => super.uri,
|
||||||
_ =>
|
_ => "http://$_host:"
|
||||||
"http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:"
|
"$serverPort/stream/${track.id}",
|
||||||
"$serverPort/stream/${track.id}",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory SpotubeMedia.fromMedia(mk.Media media) {
|
|
||||||
final track = media.uri.startsWith("http")
|
|
||||||
? Track.fromJson(media.extras?["track"])
|
|
||||||
: LocalTrack.fromJson(media.extras?["track"]);
|
|
||||||
return SpotubeMedia(
|
|
||||||
track,
|
|
||||||
extras: media.extras,
|
|
||||||
httpHeaders: media.httpHeaders,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @override
|
// @override
|
||||||
// operator ==(Object other) {
|
// operator ==(Object other) {
|
||||||
// if (other is! SpotubeMedia) return false;
|
// if (other is! SpotubeMedia) return false;
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/env.dart';
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
||||||
@ -49,16 +47,14 @@ class AudioServices with WidgetsBindingObserver {
|
|||||||
return AudioServices(mobile, smtc);
|
return AudioServices(mobile, smtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(Track track) async {
|
Future<void> addTrack(SpotubeTrackObject track) async {
|
||||||
await smtc?.addTrack(track);
|
await smtc?.addTrack(track);
|
||||||
mobile?.addItem(MediaItem(
|
mobile?.addItem(MediaItem(
|
||||||
id: track.id!,
|
id: track.id,
|
||||||
album: track.album?.name ?? "",
|
album: track.album?.name ?? "",
|
||||||
title: track.name!,
|
title: track.name,
|
||||||
artist: (track.artists)?.asString() ?? "",
|
artist: track.artists.asString(),
|
||||||
duration: track is SourcedTrack
|
duration: Duration(milliseconds: track.durationMs),
|
||||||
? track.sourceInfo.duration
|
|
||||||
: Duration(milliseconds: track.durationMs ?? 0),
|
|
||||||
artUri: Uri.parse(
|
artUri: Uri.parse(
|
||||||
(track.album?.images).asUrlString(
|
(track.album?.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
@ -2,9 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:smtc_windows/smtc_windows.dart';
|
import 'package:smtc_windows/smtc_windows.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
@ -77,15 +75,15 @@ class WindowsAudioService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(Track track) async {
|
Future<void> addTrack(SpotubeTrackObject track) async {
|
||||||
if (!smtc.enabled) {
|
if (!smtc.enabled) {
|
||||||
await smtc.enableSmtc();
|
await smtc.enableSmtc();
|
||||||
}
|
}
|
||||||
await smtc.updateMetadata(
|
await smtc.updateMetadata(
|
||||||
MusicMetadata(
|
MusicMetadata(
|
||||||
title: track.name!,
|
title: track.name,
|
||||||
albumArtist: track.artists?.firstOrNull?.name ?? "Unknown",
|
albumArtist: track.artists.firstOrNull?.name ?? "Unknown",
|
||||||
artist: track.artists?.asString() ?? "Unknown",
|
artist: track.artists.asString(),
|
||||||
album: track.album?.name ?? "Unknown",
|
album: track.album?.name ?? "Unknown",
|
||||||
thumbnail: (track.album?.images).asUrlString(
|
thumbnail: (track.album?.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|
||||||
|
|
||||||
enum SourceCodecs {
|
enum SourceCodecs {
|
||||||
m4a._("M4a (Best for downloaded music)"),
|
m4a._("M4a (Best for downloaded music)"),
|
||||||
@ -15,4 +14,7 @@ enum SourceQualities {
|
|||||||
low,
|
low,
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef SiblingType<T extends SourceInfo> = ({T info, SourceMap? source});
|
typedef SiblingType<T extends TrackSourceInfo> = ({
|
||||||
|
T info,
|
||||||
|
List<TrackSource>? source
|
||||||
|
});
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
|
|
||||||
class TrackNotFoundError extends Error {
|
class TrackNotFoundError extends Error {
|
||||||
final Track track;
|
final TrackSourceQuery track;
|
||||||
|
|
||||||
TrackNotFoundError(this.track);
|
TrackNotFoundError(this.track);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '[TrackNotFoundError] ${track.name} - ${track.artists?.map((e) => e.name).join(", ")}';
|
return '[TrackNotFoundError] ${track.title} - ${track.artists.join(", ")}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'source_info.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class SourceInfo {
|
|
||||||
final String id;
|
|
||||||
final String title;
|
|
||||||
final String artist;
|
|
||||||
final String artistUrl;
|
|
||||||
final String? album;
|
|
||||||
|
|
||||||
final String thumbnail;
|
|
||||||
final String pageUrl;
|
|
||||||
|
|
||||||
final Duration duration;
|
|
||||||
|
|
||||||
SourceInfo({
|
|
||||||
required this.id,
|
|
||||||
required this.title,
|
|
||||||
required this.artist,
|
|
||||||
required this.thumbnail,
|
|
||||||
required this.pageUrl,
|
|
||||||
required this.duration,
|
|
||||||
required this.artistUrl,
|
|
||||||
this.album,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory SourceInfo.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SourceInfoFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SourceInfoToJson(this);
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'source_info.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo(
|
|
||||||
id: json['id'] as String,
|
|
||||||
title: json['title'] as String,
|
|
||||||
artist: json['artist'] as String,
|
|
||||||
thumbnail: json['thumbnail'] as String,
|
|
||||||
pageUrl: json['pageUrl'] as String,
|
|
||||||
duration: Duration(microseconds: (json['duration'] as num).toInt()),
|
|
||||||
artistUrl: json['artistUrl'] as String,
|
|
||||||
album: json['album'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SourceInfoToJson(SourceInfo instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'title': instance.title,
|
|
||||||
'artist': instance.artist,
|
|
||||||
'artistUrl': instance.artistUrl,
|
|
||||||
'album': instance.album,
|
|
||||||
'thumbnail': instance.thumbnail,
|
|
||||||
'pageUrl': instance.pageUrl,
|
|
||||||
'duration': instance.duration.inMicroseconds,
|
|
||||||
};
|
|
@ -1,58 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
|
||||||
|
|
||||||
part 'source_map.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class SourceQualityMap {
|
|
||||||
final String high;
|
|
||||||
final String medium;
|
|
||||||
final String low;
|
|
||||||
|
|
||||||
const SourceQualityMap({
|
|
||||||
required this.high,
|
|
||||||
required this.medium,
|
|
||||||
required this.low,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory SourceQualityMap.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SourceQualityMapFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SourceQualityMapToJson(this);
|
|
||||||
|
|
||||||
operator [](SourceQualities key) {
|
|
||||||
switch (key) {
|
|
||||||
case SourceQualities.high:
|
|
||||||
return high;
|
|
||||||
case SourceQualities.medium:
|
|
||||||
return medium;
|
|
||||||
case SourceQualities.low:
|
|
||||||
return low;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class SourceMap {
|
|
||||||
final SourceQualityMap? weba;
|
|
||||||
final SourceQualityMap? m4a;
|
|
||||||
|
|
||||||
const SourceMap({
|
|
||||||
this.weba,
|
|
||||||
this.m4a,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory SourceMap.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SourceMapFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SourceMapToJson(this);
|
|
||||||
|
|
||||||
operator [](SourceCodecs key) {
|
|
||||||
switch (key) {
|
|
||||||
case SourceCodecs.weba:
|
|
||||||
return weba;
|
|
||||||
case SourceCodecs.m4a:
|
|
||||||
return m4a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'source_map.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
SourceQualityMap _$SourceQualityMapFromJson(Map json) => SourceQualityMap(
|
|
||||||
high: json['high'] as String,
|
|
||||||
medium: json['medium'] as String,
|
|
||||||
low: json['low'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SourceQualityMapToJson(SourceQualityMap instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'high': instance.high,
|
|
||||||
'medium': instance.medium,
|
|
||||||
'low': instance.low,
|
|
||||||
};
|
|
||||||
|
|
||||||
SourceMap _$SourceMapFromJson(Map json) => SourceMap(
|
|
||||||
weba: json['weba'] == null
|
|
||||||
? null
|
|
||||||
: SourceQualityMap.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['weba'] as Map)),
|
|
||||||
m4a: json['m4a'] == null
|
|
||||||
? null
|
|
||||||
: SourceQualityMap.fromJson(
|
|
||||||
Map<String, dynamic>.from(json['m4a'] as Map)),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SourceMapToJson(SourceMap instance) => <String, dynamic>{
|
|
||||||
'weba': instance.weba?.toJson(),
|
|
||||||
'm4a': instance.m4a?.toJson(),
|
|
||||||
};
|
|
@ -1,47 +1,27 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
abstract class SourcedTrack extends Track {
|
abstract class SourcedTrack extends BasicSourcedTrack {
|
||||||
final SourceMap source;
|
|
||||||
final List<SourceInfo> siblings;
|
|
||||||
final SourceInfo sourceInfo;
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
SourcedTrack({
|
SourcedTrack({
|
||||||
required this.ref,
|
required this.ref,
|
||||||
required this.source,
|
required super.info,
|
||||||
required this.siblings,
|
required super.query,
|
||||||
required this.sourceInfo,
|
required super.source,
|
||||||
required Track track,
|
required super.siblings,
|
||||||
}) {
|
required super.sources,
|
||||||
id = track.id;
|
});
|
||||||
name = track.name;
|
|
||||||
artists = track.artists;
|
|
||||||
album = track.album;
|
|
||||||
durationMs = track.durationMs;
|
|
||||||
discNumber = track.discNumber;
|
|
||||||
explicit = track.explicit;
|
|
||||||
externalIds = track.externalIds;
|
|
||||||
href = track.href;
|
|
||||||
isPlayable = track.isPlayable;
|
|
||||||
linkedFrom = track.linkedFrom;
|
|
||||||
popularity = track.popularity;
|
|
||||||
previewUrl = track.previewUrl;
|
|
||||||
trackNumber = track.trackNumber;
|
|
||||||
type = track.type;
|
|
||||||
uri = track.uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
static SourcedTrack fromJson(
|
static SourcedTrack fromJson(
|
||||||
Map<String, dynamic> json, {
|
Map<String, dynamic> json, {
|
||||||
@ -49,110 +29,116 @@ abstract class SourcedTrack extends Track {
|
|||||||
}) {
|
}) {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
final sourceInfo = SourceInfo.fromJson(json);
|
final info = TrackSourceInfo.fromJson(json["info"]);
|
||||||
final source = SourceMap.fromJson(json);
|
final query = TrackSourceQuery.fromJson(json["query"]);
|
||||||
final track = Track.fromJson(json);
|
final source = AudioSource.values.firstWhereOrNull(
|
||||||
|
(source) => source.name == json["source"],
|
||||||
|
) ??
|
||||||
|
preferences.audioSource;
|
||||||
final siblings = (json["siblings"] as List)
|
final siblings = (json["siblings"] as List)
|
||||||
.map((sibling) => SourceInfo.fromJson(sibling))
|
.map((s) => TrackSourceInfo.fromJson(s))
|
||||||
.toList()
|
.toList();
|
||||||
.cast<SourceInfo>();
|
final sources =
|
||||||
|
(json["sources"] as List).map((s) => TrackSource.fromJson(s)).toList();
|
||||||
|
|
||||||
return switch (preferences.audioSource) {
|
return switch (preferences.audioSource) {
|
||||||
AudioSource.youtube => YoutubeSourcedTrack(
|
AudioSource.youtube => YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: track,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
),
|
),
|
||||||
AudioSource.piped => PipedSourcedTrack(
|
AudioSource.piped => PipedSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: track,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
),
|
),
|
||||||
AudioSource.jiosaavn => JioSaavnSourcedTrack(
|
AudioSource.jiosaavn => JioSaavnSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: track,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
),
|
),
|
||||||
AudioSource.invidious => InvidiousSourcedTrack(
|
AudioSource.invidious => InvidiousSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: track,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getSearchTerm(Track track) {
|
static String getSearchTerm(TrackSourceQuery track) {
|
||||||
final artists =
|
|
||||||
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
|
||||||
|
|
||||||
final title = ServiceUtils.getTitle(
|
final title = ServiceUtils.getTitle(
|
||||||
track.name!,
|
track.title,
|
||||||
artists: artists,
|
artists: track.artists,
|
||||||
onlyCleanArtist: true,
|
onlyCleanArtist: true,
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
return "$title - ${artists.join(", ")}";
|
return "$title - ${track.artists.join(", ")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromQuery({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
try {
|
try {
|
||||||
return switch (preferences.audioSource) {
|
return switch (preferences.audioSource) {
|
||||||
AudioSource.youtube =>
|
AudioSource.youtube =>
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
AudioSource.piped =>
|
AudioSource.piped =>
|
||||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await PipedSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
AudioSource.invidious =>
|
AudioSource.invidious =>
|
||||||
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
AudioSource.jiosaavn =>
|
AudioSource.jiosaavn =>
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (preferences.audioSource == AudioSource.youtube) {
|
if (preferences.audioSource == AudioSource.youtube) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
return await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) {
|
}) {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
return switch (preferences.audioSource) {
|
return switch (preferences.audioSource) {
|
||||||
AudioSource.piped =>
|
AudioSource.piped =>
|
||||||
PipedSourcedTrack.fetchSiblings(track: track, ref: ref),
|
PipedSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
AudioSource.youtube =>
|
AudioSource.youtube =>
|
||||||
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
|
YoutubeSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
AudioSource.jiosaavn =>
|
AudioSource.jiosaavn =>
|
||||||
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
|
JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
AudioSource.invidious =>
|
AudioSource.invidious =>
|
||||||
InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref),
|
InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SourcedTrack> copyWithSibling();
|
Future<SourcedTrack> copyWithSibling();
|
||||||
|
|
||||||
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling);
|
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling);
|
||||||
|
|
||||||
Future<SourcedTrack?> swapWithSiblingOfIndex(int index) {
|
Future<SourcedTrack?> swapWithSiblingOfIndex(int index) {
|
||||||
return swapWithSibling(siblings[index]);
|
return swapWithSibling(siblings[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SourcedTrack> refreshStream();
|
||||||
String get url {
|
String get url {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
@ -166,10 +152,22 @@ abstract class SourcedTrack extends Track {
|
|||||||
String getUrlOfCodec(SourceCodecs codec) {
|
String getUrlOfCodec(SourceCodecs codec) {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
return source[codec]?[preferences.audioQuality] ??
|
return sources
|
||||||
// this will ensure playback doesn't break
|
.firstWhereOrNull(
|
||||||
source[codec == SourceCodecs.m4a ? SourceCodecs.weba : SourceCodecs.m4a]
|
(source) =>
|
||||||
[preferences.audioQuality];
|
source.codec == codec &&
|
||||||
|
source.quality == preferences.audioQuality,
|
||||||
|
)
|
||||||
|
?.url ??
|
||||||
|
// fallback to the first available source of the same codec
|
||||||
|
sources.firstWhereOrNull((source) => source.codec == codec)?.url ??
|
||||||
|
// fallback to the first available source of any codec
|
||||||
|
sources
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(source) => source.quality == preferences.audioQuality)
|
||||||
|
?.url ??
|
||||||
|
// fallback to the first available source
|
||||||
|
sources.first.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
SourceCodecs get codec {
|
SourceCodecs get codec {
|
||||||
@ -179,4 +177,12 @@ abstract class SourcedTrack extends Track {
|
|||||||
? SourceCodecs.m4a
|
? SourceCodecs.m4a
|
||||||
: preferences.streamMusicCodec;
|
: preferences.streamMusicCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackSource get activeTrackSource {
|
||||||
|
final audioQuality = ref.read(userPreferencesProvider).audioQuality;
|
||||||
|
return sources.firstWhereOrNull(
|
||||||
|
(source) => source.codec == codec && source.quality == audioQuality,
|
||||||
|
) ??
|
||||||
|
sources.first;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:invidious/invidious.dart';
|
import 'package:invidious/invidious.dart';
|
||||||
@ -24,51 +22,24 @@ final invidiousProvider = Provider<InvidiousClient>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
class InvidiousSourceInfo extends SourceInfo {
|
|
||||||
InvidiousSourceInfo({
|
|
||||||
required super.id,
|
|
||||||
required super.title,
|
|
||||||
required super.artist,
|
|
||||||
required super.thumbnail,
|
|
||||||
required super.pageUrl,
|
|
||||||
required super.duration,
|
|
||||||
required super.artistUrl,
|
|
||||||
required super.album,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvidiousSourcedTrack extends SourcedTrack {
|
class InvidiousSourcedTrack extends SourcedTrack {
|
||||||
InvidiousSourcedTrack({
|
InvidiousSourcedTrack({
|
||||||
required super.ref,
|
required super.ref,
|
||||||
required super.source,
|
required super.source,
|
||||||
required super.siblings,
|
required super.siblings,
|
||||||
required super.sourceInfo,
|
required super.info,
|
||||||
required super.track,
|
required super.query,
|
||||||
|
required super.sources,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
// Indicates a stream url refresh
|
final audioSource = ref.read(userPreferencesProvider).audioSource;
|
||||||
if (track is InvidiousSourcedTrack) {
|
|
||||||
final manifest = await ref
|
|
||||||
.read(invidiousProvider)
|
|
||||||
.videos
|
|
||||||
.get(track.sourceInfo.id, local: true);
|
|
||||||
|
|
||||||
return InvidiousSourcedTrack(
|
|
||||||
ref: ref,
|
|
||||||
siblings: track.siblings,
|
|
||||||
source: toSourceMap(manifest),
|
|
||||||
sourceInfo: track.sourceInfo,
|
|
||||||
track: track,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
..where((s) => s.trackId.equals(track.id!))
|
..where((s) => s.trackId.equals(query.id))
|
||||||
..limit(1)
|
..limit(1)
|
||||||
..orderBy([
|
..orderBy([
|
||||||
(s) =>
|
(s) =>
|
||||||
@ -78,14 +49,14 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
final invidiousClient = ref.read(invidiousProvider);
|
final invidiousClient = ref.read(invidiousProvider);
|
||||||
|
|
||||||
if (cachedSource == null) {
|
if (cachedSource == null) {
|
||||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
final siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: track.id!,
|
trackId: query.id,
|
||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: const Value(SourceType.youtube),
|
sourceType: const Value(SourceType.youtube),
|
||||||
),
|
),
|
||||||
@ -94,9 +65,10 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
return InvidiousSourcedTrack(
|
return InvidiousSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
source: siblings.first.source as SourceMap,
|
sources: siblings.first.source as List<TrackSource>,
|
||||||
sourceInfo: siblings.first.info,
|
info: siblings.first.info,
|
||||||
track: track,
|
query: query,
|
||||||
|
source: audioSource,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final manifest =
|
final manifest =
|
||||||
@ -105,44 +77,36 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
return InvidiousSourcedTrack(
|
return InvidiousSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: [],
|
siblings: [],
|
||||||
source: toSourceMap(manifest),
|
sources: toSources(manifest),
|
||||||
sourceInfo: InvidiousSourceInfo(
|
info: TrackSourceInfo(
|
||||||
id: manifest.videoId,
|
id: manifest.videoId,
|
||||||
artist: manifest.author,
|
artists: manifest.author,
|
||||||
artistUrl: manifest.authorUrl,
|
|
||||||
pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}",
|
pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}",
|
||||||
thumbnail: manifest.videoThumbnails.first.url,
|
thumbnail: manifest.videoThumbnails.first.url,
|
||||||
title: manifest.title,
|
title: manifest.title,
|
||||||
duration: Duration(seconds: manifest.lengthSeconds),
|
durationMs: Duration(seconds: manifest.lengthSeconds).inMilliseconds,
|
||||||
album: null,
|
|
||||||
),
|
),
|
||||||
track: track,
|
query: query,
|
||||||
|
source: audioSource,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static SourceMap toSourceMap(InvidiousVideoResponse manifest) {
|
static List<TrackSource> toSources(InvidiousVideoResponse manifest) {
|
||||||
final m4a = manifest.adaptiveFormats
|
return manifest.adaptiveFormats.map((stream) {
|
||||||
.where((audio) => audio.type.contains("audio/mp4"))
|
return TrackSource(
|
||||||
.sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate)));
|
url: stream.url.toString(),
|
||||||
|
quality: switch (stream.qualityLabel) {
|
||||||
final weba = manifest.adaptiveFormats
|
"high" => SourceQualities.high,
|
||||||
.where((audio) => audio.type.contains("audio/webm"))
|
"medium" => SourceQualities.medium,
|
||||||
.sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate)));
|
_ => SourceQualities.low,
|
||||||
|
},
|
||||||
return SourceMap(
|
codec: stream.type.contains("audio/webm")
|
||||||
m4a: SourceQualityMap(
|
? SourceCodecs.weba
|
||||||
high: m4a.first.url.toString(),
|
: SourceCodecs.m4a,
|
||||||
medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(),
|
bitrate: stream.bitrate,
|
||||||
low: m4a.last.url.toString(),
|
);
|
||||||
),
|
}).toList();
|
||||||
weba: SourceQualityMap(
|
|
||||||
high: weba.first.url.toString(),
|
|
||||||
medium:
|
|
||||||
(weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(),
|
|
||||||
low: weba.last.url.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<SiblingType> toSiblingType(
|
static Future<SiblingType> toSiblingType(
|
||||||
@ -150,22 +114,20 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
YoutubeVideoInfo item,
|
YoutubeVideoInfo item,
|
||||||
InvidiousClient invidiousClient,
|
InvidiousClient invidiousClient,
|
||||||
) async {
|
) async {
|
||||||
SourceMap? sourceMap;
|
List<TrackSource>? sourceMap;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
final manifest = await invidiousClient.videos.get(item.id, local: true);
|
final manifest = await invidiousClient.videos.get(item.id, local: true);
|
||||||
sourceMap = toSourceMap(manifest);
|
sourceMap = toSources(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SiblingType sibling = (
|
final SiblingType sibling = (
|
||||||
info: InvidiousSourceInfo(
|
info: TrackSourceInfo(
|
||||||
id: item.id,
|
id: item.id,
|
||||||
artist: item.channelName,
|
artists: item.channelName,
|
||||||
artistUrl: "https://www.youtube.com/${item.channelId}",
|
|
||||||
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
||||||
thumbnail: item.thumbnailUrl,
|
thumbnail: item.thumbnailUrl,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
duration: item.duration,
|
durationMs: item.duration.inMilliseconds,
|
||||||
album: null,
|
|
||||||
),
|
),
|
||||||
source: sourceMap,
|
source: sourceMap,
|
||||||
);
|
);
|
||||||
@ -174,20 +136,20 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final invidiousClient = ref.read(invidiousProvider);
|
final invidiousClient = ref.read(invidiousProvider);
|
||||||
final preference = ref.read(userPreferencesProvider);
|
final preference = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final searchQuery = SourcedTrack.getSearchTerm(query);
|
||||||
|
|
||||||
final searchResults = await invidiousClient.search.list(
|
final searchResults = await invidiousClient.search.list(
|
||||||
query,
|
searchQuery,
|
||||||
type: InvidiousSearchType.video,
|
type: InvidiousSearchType.video,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(searchQuery)) {
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
searchResults
|
searchResults
|
||||||
.whereType<InvidiousSearchResponseVideo>()
|
.whereType<InvidiousSearchResponseVideo>()
|
||||||
@ -211,7 +173,7 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
track,
|
query,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
@ -224,23 +186,24 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
if (siblings.isNotEmpty) {
|
if (siblings.isNotEmpty) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
|
||||||
return InvidiousSourcedTrack(
|
return InvidiousSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: fetchedSiblings
|
siblings: fetchedSiblings
|
||||||
.where((s) => s.info.id != sourceInfo.id)
|
.where((s) => s.info.id != info.id)
|
||||||
.map((s) => s.info)
|
.map((s) => s.info)
|
||||||
.toList(),
|
.toList(),
|
||||||
source: source,
|
source: source,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: this,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == info.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +214,7 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
? sibling
|
? sibling
|
||||||
: siblings.firstWhere((s) => s.id == sibling.id);
|
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
..insert(0, sourceInfo);
|
..insert(0, info);
|
||||||
|
|
||||||
final pipedClient = ref.read(invidiousProvider);
|
final pipedClient = ref.read(invidiousProvider);
|
||||||
|
|
||||||
@ -261,7 +224,7 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: id!,
|
trackId: query.id,
|
||||||
sourceId: newSourceInfo.id,
|
sourceId: newSourceInfo.id,
|
||||||
sourceType: const Value(SourceType.youtube),
|
sourceType: const Value(SourceType.youtube),
|
||||||
// Because we're sorting by createdAt in the query
|
// Because we're sorting by createdAt in the query
|
||||||
@ -274,9 +237,25 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
return InvidiousSourcedTrack(
|
return InvidiousSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: newSiblings,
|
siblings: newSiblings,
|
||||||
source: toSourceMap(manifest),
|
sources: toSources(manifest),
|
||||||
sourceInfo: newSourceInfo,
|
info: newSourceInfo,
|
||||||
track: this,
|
query: query,
|
||||||
|
source: source,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SourcedTrack> refreshStream() async {
|
||||||
|
final manifest =
|
||||||
|
await ref.read(invidiousProvider).videos.get(info.id, local: true);
|
||||||
|
|
||||||
|
return InvidiousSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: siblings,
|
||||||
|
sources: toSources(manifest),
|
||||||
|
info: info,
|
||||||
|
query: query,
|
||||||
|
source: source,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,49 +1,35 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:jiosaavn/jiosaavn.dart';
|
import 'package:jiosaavn/jiosaavn.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
|
|
||||||
final jiosaavnClient = JioSaavnClient();
|
final jiosaavnClient = JioSaavnClient();
|
||||||
|
|
||||||
class JioSaavnSourceInfo extends SourceInfo {
|
|
||||||
JioSaavnSourceInfo({
|
|
||||||
required super.id,
|
|
||||||
required super.title,
|
|
||||||
required super.artist,
|
|
||||||
required super.thumbnail,
|
|
||||||
required super.pageUrl,
|
|
||||||
required super.duration,
|
|
||||||
required super.artistUrl,
|
|
||||||
required super.album,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class JioSaavnSourcedTrack extends SourcedTrack {
|
class JioSaavnSourcedTrack extends SourcedTrack {
|
||||||
JioSaavnSourcedTrack({
|
JioSaavnSourcedTrack({
|
||||||
required super.ref,
|
required super.ref,
|
||||||
required super.source,
|
required super.source,
|
||||||
required super.siblings,
|
required super.siblings,
|
||||||
required super.sourceInfo,
|
required super.info,
|
||||||
required super.track,
|
required super.query,
|
||||||
|
required super.sources,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
bool weakMatch = false,
|
bool weakMatch = false,
|
||||||
}) async {
|
}) async {
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
..where((s) => s.trackId.equals(track.id!))
|
..where((s) => s.trackId.equals(query.id))
|
||||||
..limit(1)
|
..limit(1)
|
||||||
..orderBy([
|
..orderBy([
|
||||||
(s) =>
|
(s) =>
|
||||||
@ -54,15 +40,15 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
if (cachedSource == null ||
|
if (cachedSource == null ||
|
||||||
cachedSource.sourceType != SourceType.jiosaavn) {
|
cachedSource.sourceType != SourceType.jiosaavn) {
|
||||||
final siblings =
|
final siblings =
|
||||||
await fetchSiblings(ref: ref, track: track, weakMatch: weakMatch);
|
await fetchSiblings(ref: ref, query: query, weakMatch: weakMatch);
|
||||||
|
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: track.id!,
|
trackId: query.id,
|
||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: const Value(SourceType.jiosaavn),
|
sourceType: const Value(SourceType.jiosaavn),
|
||||||
),
|
),
|
||||||
@ -71,9 +57,10 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
return JioSaavnSourcedTrack(
|
return JioSaavnSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
source: siblings.first.source!,
|
sources: siblings.first.source!,
|
||||||
sourceInfo: siblings.first.info,
|
info: siblings.first.info,
|
||||||
track: track,
|
query: query,
|
||||||
|
source: AudioSource.jiosaavn,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,80 +72,77 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
return JioSaavnSourcedTrack(
|
return JioSaavnSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: [],
|
siblings: [],
|
||||||
source: source!,
|
sources: source!,
|
||||||
sourceInfo: info,
|
query: query,
|
||||||
track: track,
|
info: info,
|
||||||
|
source: AudioSource.jiosaavn,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SiblingType toSiblingType(SongResponse result) {
|
static SiblingType toSiblingType(SongResponse result) {
|
||||||
final SiblingType sibling = (
|
final SiblingType sibling = (
|
||||||
info: JioSaavnSourceInfo(
|
info: TrackSourceInfo(
|
||||||
artist: [
|
artists: [
|
||||||
result.primaryArtists,
|
result.primaryArtists,
|
||||||
if (result.featuredArtists.isNotEmpty) ", ",
|
if (result.featuredArtists.isNotEmpty) ", ",
|
||||||
result.featuredArtists
|
result.featuredArtists
|
||||||
].join("").unescapeHtml(),
|
].join("").unescapeHtml(),
|
||||||
artistUrl:
|
durationMs:
|
||||||
"https://www.jiosaavn.com/artist/${result.primaryArtistsId.split(",").firstOrNull ?? ""}",
|
Duration(seconds: int.parse(result.duration)).inMilliseconds,
|
||||||
duration: Duration(seconds: int.parse(result.duration)),
|
|
||||||
id: result.id,
|
id: result.id,
|
||||||
pageUrl: result.url,
|
pageUrl: result.url,
|
||||||
thumbnail: result.image?.last.link ?? "",
|
thumbnail: result.image?.last.link ?? "",
|
||||||
title: result.name!.unescapeHtml(),
|
title: result.name!.unescapeHtml(),
|
||||||
album: result.album.name,
|
|
||||||
),
|
|
||||||
source: SourceMap(
|
|
||||||
m4a: SourceQualityMap(
|
|
||||||
high: result.downloadUrl!
|
|
||||||
.firstWhere((element) => element.quality == "320kbps")
|
|
||||||
.link,
|
|
||||||
medium: result.downloadUrl!
|
|
||||||
.firstWhere((element) => element.quality == "160kbps")
|
|
||||||
.link,
|
|
||||||
low: result.downloadUrl!
|
|
||||||
.firstWhere((element) => element.quality == "96kbps")
|
|
||||||
.link,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
source: result.downloadUrl!.map((link) {
|
||||||
|
return TrackSource(
|
||||||
|
url: link.link,
|
||||||
|
quality: link.quality == "320kbps"
|
||||||
|
? SourceQualities.high
|
||||||
|
: link.quality == "160kbps"
|
||||||
|
? SourceQualities.medium
|
||||||
|
: SourceQualities.low,
|
||||||
|
codec: SourceCodecs.m4a,
|
||||||
|
bitrate: link.quality,
|
||||||
|
);
|
||||||
|
}).toList()
|
||||||
);
|
);
|
||||||
|
|
||||||
return sibling;
|
return sibling;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
bool weakMatch = false,
|
bool weakMatch = false,
|
||||||
}) async {
|
}) async {
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final searchQuery = SourcedTrack.getSearchTerm(query);
|
||||||
|
|
||||||
final SongSearchResponse(:results) =
|
final SongSearchResponse(:results) =
|
||||||
await jiosaavnClient.search.songs(query, limit: 20);
|
await jiosaavnClient.search.songs(searchQuery, limit: 20);
|
||||||
|
|
||||||
final trackArtistNames = track.artists?.map((ar) => ar.name).toList();
|
final trackArtistNames = query.artists;
|
||||||
|
|
||||||
final matchedResults = results
|
final matchedResults = results
|
||||||
.where(
|
.where(
|
||||||
(s) {
|
(s) {
|
||||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
s.name?.unescapeHtml().contains(query.title) ?? false;
|
||||||
|
|
||||||
final sameName = s.name?.unescapeHtml() == track.name;
|
final sameName = s.name?.unescapeHtml() == query.title;
|
||||||
final artistNames = [
|
final artistNames = [
|
||||||
s.primaryArtists,
|
s.primaryArtists,
|
||||||
if (s.featuredArtists.isNotEmpty) ", ",
|
if (s.featuredArtists.isNotEmpty) ", ",
|
||||||
s.featuredArtists
|
s.featuredArtists
|
||||||
].join("").unescapeHtml();
|
].join("").unescapeHtml();
|
||||||
final sameArtists = artistNames.split(", ").any(
|
final sameArtists = artistNames.split(", ").any(
|
||||||
(artist) =>
|
(artist) => trackArtistNames.any((ar) => artist == ar),
|
||||||
trackArtistNames?.any((ar) => artist == ar) ?? false,
|
|
||||||
);
|
);
|
||||||
if (weakMatch) {
|
if (weakMatch) {
|
||||||
final containsName =
|
final containsName =
|
||||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
s.name?.unescapeHtml().contains(query.title) ?? false;
|
||||||
final containsPrimaryArtist = s.primaryArtists
|
final containsPrimaryArtist = s.primaryArtists
|
||||||
.unescapeHtml()
|
.unescapeHtml()
|
||||||
.contains(trackArtistNames?.first ?? "");
|
.contains(trackArtistNames.first);
|
||||||
|
|
||||||
return containsName && containsPrimaryArtist;
|
return containsName && containsPrimaryArtist;
|
||||||
}
|
}
|
||||||
@ -181,23 +165,24 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
if (siblings.isNotEmpty) {
|
if (siblings.isNotEmpty) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
|
||||||
return JioSaavnSourcedTrack(
|
return JioSaavnSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: fetchedSiblings
|
siblings: fetchedSiblings
|
||||||
.where((s) => s.info.id != sourceInfo.id)
|
.where((s) => s.info.id != info.id)
|
||||||
.map((s) => s.info)
|
.map((s) => s.info)
|
||||||
.toList(),
|
.toList(),
|
||||||
source: source,
|
source: source,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: this,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<JioSaavnSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<JioSaavnSourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == this.info.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +193,7 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
? sibling
|
? sibling
|
||||||
: siblings.firstWhere((s) => s.id == sibling.id);
|
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
..insert(0, sourceInfo);
|
..insert(0, this.info);
|
||||||
|
|
||||||
final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]);
|
final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]);
|
||||||
|
|
||||||
@ -217,7 +202,7 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: id!,
|
trackId: query.id,
|
||||||
sourceId: info.id,
|
sourceId: info.id,
|
||||||
sourceType: const Value(SourceType.jiosaavn),
|
sourceType: const Value(SourceType.jiosaavn),
|
||||||
// Because we're sorting by createdAt in the query
|
// Because we're sorting by createdAt in the query
|
||||||
@ -230,9 +215,16 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
|||||||
return JioSaavnSourcedTrack(
|
return JioSaavnSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: newSiblings,
|
siblings: newSiblings,
|
||||||
source: source!,
|
sources: source!,
|
||||||
sourceInfo: info,
|
info: info,
|
||||||
track: this,
|
query: query,
|
||||||
|
source: AudioSource.jiosaavn,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SourcedTrack> refreshStream() async {
|
||||||
|
// There's no need to refresh the stream for JioSaavnSourcedTrack
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,13 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:piped_client/piped_client.dart';
|
import 'package:piped_client/piped_client.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
@ -24,48 +22,24 @@ final pipedProvider = Provider<PipedClient>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
class PipedSourceInfo extends SourceInfo {
|
|
||||||
PipedSourceInfo({
|
|
||||||
required super.id,
|
|
||||||
required super.title,
|
|
||||||
required super.artist,
|
|
||||||
required super.thumbnail,
|
|
||||||
required super.pageUrl,
|
|
||||||
required super.duration,
|
|
||||||
required super.artistUrl,
|
|
||||||
required super.album,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class PipedSourcedTrack extends SourcedTrack {
|
class PipedSourcedTrack extends SourcedTrack {
|
||||||
PipedSourcedTrack({
|
PipedSourcedTrack({
|
||||||
required super.ref,
|
required super.ref,
|
||||||
required super.source,
|
required super.source,
|
||||||
required super.siblings,
|
required super.siblings,
|
||||||
required super.sourceInfo,
|
required super.info,
|
||||||
required super.track,
|
required super.query,
|
||||||
|
required super.sources,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
// Means it wants a refresh of the stream
|
final audioSource = ref.read(userPreferencesProvider).audioSource;
|
||||||
if (track is PipedSourcedTrack) {
|
|
||||||
final manifest =
|
|
||||||
await ref.read(pipedProvider).streams(track.sourceInfo.id);
|
|
||||||
return PipedSourcedTrack(
|
|
||||||
ref: ref,
|
|
||||||
siblings: track.siblings,
|
|
||||||
sourceInfo: track.sourceInfo,
|
|
||||||
source: toSourceMap(manifest),
|
|
||||||
track: track,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
..where((s) => s.trackId.equals(track.id!))
|
..where((s) => s.trackId.equals(query.id))
|
||||||
..limit(1)
|
..limit(1)
|
||||||
..orderBy([
|
..orderBy([
|
||||||
(s) =>
|
(s) =>
|
||||||
@ -76,14 +50,14 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
final pipedClient = ref.read(pipedProvider);
|
final pipedClient = ref.read(pipedProvider);
|
||||||
|
|
||||||
if (cachedSource == null) {
|
if (cachedSource == null) {
|
||||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
final siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: track.id!,
|
trackId: query.id,
|
||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: Value(
|
sourceType: Value(
|
||||||
preferences.searchMode == SearchMode.youtube
|
preferences.searchMode == SearchMode.youtube
|
||||||
@ -96,9 +70,10 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
return PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
source: siblings.first.source as SourceMap,
|
source: audioSource,
|
||||||
sourceInfo: siblings.first.info,
|
info: siblings.first.info,
|
||||||
track: track,
|
query: query,
|
||||||
|
sources: siblings.first.source!,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final manifest = await pipedClient.streams(cachedSource.sourceId);
|
final manifest = await pipedClient.streams(cachedSource.sourceId);
|
||||||
@ -106,44 +81,36 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
return PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: [],
|
siblings: [],
|
||||||
source: toSourceMap(manifest),
|
sources: toSources(manifest),
|
||||||
sourceInfo: PipedSourceInfo(
|
info: TrackSourceInfo(
|
||||||
id: manifest.id,
|
id: manifest.id,
|
||||||
artist: manifest.uploader,
|
artists: manifest.uploader,
|
||||||
artistUrl: manifest.uploaderUrl,
|
|
||||||
pageUrl: "https://www.youtube.com/watch?v=${manifest.id}",
|
pageUrl: "https://www.youtube.com/watch?v=${manifest.id}",
|
||||||
thumbnail: manifest.thumbnailUrl,
|
thumbnail: manifest.thumbnailUrl,
|
||||||
title: manifest.title,
|
title: manifest.title,
|
||||||
duration: manifest.duration,
|
durationMs: manifest.duration.inMilliseconds,
|
||||||
album: null,
|
|
||||||
),
|
),
|
||||||
track: track,
|
query: query,
|
||||||
|
source: audioSource,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static SourceMap toSourceMap(PipedStreamResponse manifest) {
|
static List<TrackSource> toSources(PipedStreamResponse manifest) {
|
||||||
final m4a = manifest.audioStreams
|
return manifest.audioStreams.map((audio) {
|
||||||
.where((audio) => audio.format == PipedAudioStreamFormat.m4a)
|
return TrackSource(
|
||||||
.sorted((a, b) => a.bitrate.compareTo(b.bitrate));
|
url: audio.url.toString(),
|
||||||
|
quality: switch (audio.quality) {
|
||||||
final weba = manifest.audioStreams
|
"high" => SourceQualities.high,
|
||||||
.where((audio) => audio.format == PipedAudioStreamFormat.webm)
|
"medium" => SourceQualities.medium,
|
||||||
.sorted((a, b) => a.bitrate.compareTo(b.bitrate));
|
_ => SourceQualities.low,
|
||||||
|
},
|
||||||
return SourceMap(
|
codec: audio.format == PipedAudioStreamFormat.m4a
|
||||||
m4a: SourceQualityMap(
|
? SourceCodecs.m4a
|
||||||
high: m4a.first.url.toString(),
|
: SourceCodecs.weba,
|
||||||
medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(),
|
bitrate: audio.bitrate.toString(),
|
||||||
low: m4a.last.url.toString(),
|
);
|
||||||
),
|
}).toList();
|
||||||
weba: SourceQualityMap(
|
|
||||||
high: weba.first.url.toString(),
|
|
||||||
medium:
|
|
||||||
(weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(),
|
|
||||||
low: weba.last.url.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<SiblingType> toSiblingType(
|
static Future<SiblingType> toSiblingType(
|
||||||
@ -151,40 +118,38 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
YoutubeVideoInfo item,
|
YoutubeVideoInfo item,
|
||||||
PipedClient pipedClient,
|
PipedClient pipedClient,
|
||||||
) async {
|
) async {
|
||||||
SourceMap? sourceMap;
|
List<TrackSource>? sources;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
final manifest = await pipedClient.streams(item.id);
|
final manifest = await pipedClient.streams(item.id);
|
||||||
sourceMap = toSourceMap(manifest);
|
sources = toSources(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SiblingType sibling = (
|
final SiblingType sibling = (
|
||||||
info: PipedSourceInfo(
|
info: TrackSourceInfo(
|
||||||
id: item.id,
|
id: item.id,
|
||||||
artist: item.channelName,
|
artists: item.channelName,
|
||||||
artistUrl: "https://www.youtube.com/${item.channelId}",
|
|
||||||
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
||||||
thumbnail: item.thumbnailUrl,
|
thumbnail: item.thumbnailUrl,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
duration: item.duration,
|
durationMs: item.duration.inMilliseconds,
|
||||||
album: null,
|
|
||||||
),
|
),
|
||||||
source: sourceMap,
|
source: sources,
|
||||||
);
|
);
|
||||||
|
|
||||||
return sibling;
|
return sibling;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final pipedClient = ref.read(pipedProvider);
|
final pipedClient = ref.read(pipedProvider);
|
||||||
final preference = ref.read(userPreferencesProvider);
|
final preference = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final searchQuery = SourcedTrack.getSearchTerm(query);
|
||||||
|
|
||||||
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
||||||
query,
|
searchQuery,
|
||||||
preference.searchMode == SearchMode.youtube
|
preference.searchMode == SearchMode.youtube
|
||||||
? PipedFilter.videos
|
? PipedFilter.videos
|
||||||
: PipedFilter.musicSongs,
|
: PipedFilter.musicSongs,
|
||||||
@ -196,8 +161,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
: preference.searchMode == SearchMode.youtubeMusic;
|
: preference.searchMode == SearchMode.youtubeMusic;
|
||||||
|
|
||||||
if (isYouTubeMusic) {
|
if (isYouTubeMusic) {
|
||||||
final artists =
|
final artists = query.artists;
|
||||||
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
|
||||||
|
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
searchResults
|
searchResults
|
||||||
@ -218,7 +182,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(searchQuery)) {
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
searchResults
|
searchResults
|
||||||
.whereType<PipedSearchItemStream>()
|
.whereType<PipedSearchItemStream>()
|
||||||
@ -241,7 +205,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
track,
|
query,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
@ -254,23 +218,24 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
if (siblings.isNotEmpty) {
|
if (siblings.isNotEmpty) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
|
||||||
return PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: fetchedSiblings
|
siblings: fetchedSiblings
|
||||||
.where((s) => s.info.id != sourceInfo.id)
|
.where((s) => s.info.id != info.id)
|
||||||
.map((s) => s.info)
|
.map((s) => s.info)
|
||||||
.toList(),
|
.toList(),
|
||||||
source: source,
|
source: source,
|
||||||
sourceInfo: sourceInfo,
|
info: info,
|
||||||
track: this,
|
query: query,
|
||||||
|
sources: sources,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == info.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +246,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
? sibling
|
? sibling
|
||||||
: siblings.firstWhere((s) => s.id == sibling.id);
|
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
..insert(0, sourceInfo);
|
..insert(0, info);
|
||||||
|
|
||||||
final pipedClient = ref.read(pipedProvider);
|
final pipedClient = ref.read(pipedProvider);
|
||||||
|
|
||||||
@ -290,7 +255,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: id!,
|
trackId: query.id,
|
||||||
sourceId: newSourceInfo.id,
|
sourceId: newSourceInfo.id,
|
||||||
sourceType: const Value(SourceType.youtube),
|
sourceType: const Value(SourceType.youtube),
|
||||||
// Because we're sorting by createdAt in the query
|
// Because we're sorting by createdAt in the query
|
||||||
@ -303,9 +268,23 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
return PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: newSiblings,
|
siblings: newSiblings,
|
||||||
source: toSourceMap(manifest),
|
sources: toSources(manifest),
|
||||||
sourceInfo: newSourceInfo,
|
info: info,
|
||||||
track: this,
|
query: query,
|
||||||
|
source: source,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SourcedTrack> refreshStream() async {
|
||||||
|
final manifest = await ref.read(pipedProvider).streams(info.id);
|
||||||
|
return PipedSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: siblings,
|
||||||
|
info: info,
|
||||||
|
source: source,
|
||||||
|
query: query,
|
||||||
|
sources: toSources(manifest),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/playback/track_sources.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/song_link/song_link.dart';
|
import 'package:spotube/services/song_link/song_link.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -21,54 +20,24 @@ final officialMusicRegex = RegExp(
|
|||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
class YoutubeSourceInfo extends SourceInfo {
|
|
||||||
YoutubeSourceInfo({
|
|
||||||
required super.id,
|
|
||||||
required super.title,
|
|
||||||
required super.artist,
|
|
||||||
required super.thumbnail,
|
|
||||||
required super.pageUrl,
|
|
||||||
required super.duration,
|
|
||||||
required super.artistUrl,
|
|
||||||
required super.album,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class YoutubeSourcedTrack extends SourcedTrack {
|
class YoutubeSourcedTrack extends SourcedTrack {
|
||||||
YoutubeSourcedTrack({
|
YoutubeSourcedTrack({
|
||||||
required super.source,
|
required super.source,
|
||||||
required super.siblings,
|
required super.siblings,
|
||||||
required super.sourceInfo,
|
required super.info,
|
||||||
required super.track,
|
required super.query,
|
||||||
|
required super.sources,
|
||||||
required super.ref,
|
required super.ref,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
// Indicates the track is requesting a stream refresh
|
final audioSource = ref.read(userPreferencesProvider).audioSource;
|
||||||
if (track is YoutubeSourcedTrack) {
|
|
||||||
final manifest = await ref
|
|
||||||
.read(youtubeEngineProvider)
|
|
||||||
.getStreamManifest(track.sourceInfo.id);
|
|
||||||
|
|
||||||
final sourcedTrack = YoutubeSourcedTrack(
|
|
||||||
ref: ref,
|
|
||||||
siblings: track.siblings,
|
|
||||||
source: toSourceMap(manifest),
|
|
||||||
sourceInfo: track.sourceInfo,
|
|
||||||
track: track,
|
|
||||||
);
|
|
||||||
|
|
||||||
AppLogger.log.i("Refreshing ${track.name}: ${sourcedTrack.url}");
|
|
||||||
|
|
||||||
return sourcedTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
..where((s) => s.trackId.equals(track.id!))
|
..where((s) => s.trackId.equals(query.id))
|
||||||
..limit(1)
|
..limit(1)
|
||||||
..orderBy([
|
..orderBy([
|
||||||
(s) =>
|
(s) =>
|
||||||
@ -78,14 +47,14 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
.then((s) => s.firstOrNull);
|
.then((s) => s.firstOrNull);
|
||||||
|
|
||||||
if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) {
|
if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) {
|
||||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
final siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: track.id!,
|
trackId: query.id,
|
||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: const Value(SourceType.youtube),
|
sourceType: const Value(SourceType.youtube),
|
||||||
),
|
),
|
||||||
@ -94,9 +63,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
return YoutubeSourcedTrack(
|
return YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
source: siblings.first.source as SourceMap,
|
info: siblings.first.info,
|
||||||
sourceInfo: siblings.first.info,
|
source: audioSource,
|
||||||
track: track,
|
sources: siblings.first.source ?? [],
|
||||||
|
query: query,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final (item, manifest) = await ref
|
final (item, manifest) = await ref
|
||||||
@ -106,26 +76,25 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
final sourcedTrack = YoutubeSourcedTrack(
|
final sourcedTrack = YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: [],
|
siblings: [],
|
||||||
source: toSourceMap(manifest),
|
sources: toTrackSources(manifest),
|
||||||
sourceInfo: YoutubeSourceInfo(
|
info: TrackSourceInfo(
|
||||||
id: item.id.value,
|
id: item.id.value,
|
||||||
artist: item.author,
|
artists: item.author,
|
||||||
artistUrl: "https://www.youtube.com/channel/${item.channelId}",
|
|
||||||
pageUrl: item.url,
|
pageUrl: item.url,
|
||||||
thumbnail: item.thumbnails.highResUrl,
|
thumbnail: item.thumbnails.highResUrl,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
duration: item.duration ?? Duration.zero,
|
durationMs: item.duration?.inMilliseconds ?? 0,
|
||||||
album: null,
|
|
||||||
),
|
),
|
||||||
track: track,
|
query: query,
|
||||||
|
source: audioSource,
|
||||||
);
|
);
|
||||||
|
|
||||||
AppLogger.log.i("${track.name}: ${sourcedTrack.url}");
|
AppLogger.log.i("${query.title}: ${sourcedTrack.url}");
|
||||||
|
|
||||||
return sourcedTrack;
|
return sourcedTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SourceMap toSourceMap(StreamManifest manifest) {
|
static List<TrackSource> toTrackSources(StreamManifest manifest) {
|
||||||
var m4a = manifest.audioOnly
|
var m4a = manifest.audioOnly
|
||||||
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
||||||
.sortByBitrate();
|
.sortByBitrate();
|
||||||
@ -137,19 +106,20 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
m4a = m4a.isEmpty ? weba.toList() : m4a;
|
m4a = m4a.isEmpty ? weba.toList() : m4a;
|
||||||
weba = weba.isEmpty ? m4a.toList() : weba;
|
weba = weba.isEmpty ? m4a.toList() : weba;
|
||||||
|
|
||||||
return SourceMap(
|
return manifest.audioOnly.map((streamInfo) {
|
||||||
m4a: SourceQualityMap(
|
return TrackSource(
|
||||||
high: m4a.first.url.toString(),
|
url: streamInfo.url.toString(),
|
||||||
medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(),
|
quality: streamInfo.qualityLabel == "AUDIO_QUALITY_HIGH"
|
||||||
low: m4a.last.url.toString(),
|
? SourceQualities.high
|
||||||
),
|
: streamInfo.qualityLabel == "AUDIO_QUALITY_MEDIUM"
|
||||||
weba: SourceQualityMap(
|
? SourceQualities.medium
|
||||||
high: weba.first.url.toString(),
|
: SourceQualities.low,
|
||||||
medium:
|
codec: streamInfo.codec.mimeType == "audio/mp4"
|
||||||
(weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(),
|
? SourceCodecs.m4a
|
||||||
low: weba.last.url.toString(),
|
: SourceCodecs.weba,
|
||||||
),
|
bitrate: streamInfo.bitrate.bitsPerSecond.toString(),
|
||||||
);
|
);
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<SiblingType> toSiblingType(
|
static Future<SiblingType> toSiblingType(
|
||||||
@ -158,23 +128,21 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
dynamic ref,
|
dynamic ref,
|
||||||
) async {
|
) async {
|
||||||
assert(ref is WidgetRef || ref is Ref, "Invalid ref type");
|
assert(ref is WidgetRef || ref is Ref, "Invalid ref type");
|
||||||
SourceMap? sourceMap;
|
List<TrackSource>? sourceMap;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
final manifest =
|
final manifest =
|
||||||
await ref.read(youtubeEngineProvider).getStreamManifest(item.id);
|
await ref.read(youtubeEngineProvider).getStreamManifest(item.id);
|
||||||
sourceMap = toSourceMap(manifest);
|
sourceMap = toTrackSources(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SiblingType sibling = (
|
final SiblingType sibling = (
|
||||||
info: YoutubeSourceInfo(
|
info: TrackSourceInfo(
|
||||||
id: item.id,
|
id: item.id,
|
||||||
artist: item.channelName,
|
artists: item.channelName,
|
||||||
artistUrl: "https://www.youtube.com/channel/${item.channelId}",
|
|
||||||
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
||||||
thumbnail: item.thumbnailUrl,
|
thumbnail: item.thumbnailUrl,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
duration: item.duration,
|
durationMs: item.duration.inMilliseconds,
|
||||||
album: null,
|
|
||||||
),
|
),
|
||||||
source: sourceMap,
|
source: sourceMap,
|
||||||
);
|
);
|
||||||
@ -183,16 +151,13 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static List<YoutubeVideoInfo> rankResults(
|
static List<YoutubeVideoInfo> rankResults(
|
||||||
List<YoutubeVideoInfo> results, Track track) {
|
List<YoutubeVideoInfo> results, TrackSourceQuery track) {
|
||||||
final artists =
|
|
||||||
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
.sorted((a, b) => b.views.compareTo(a.views))
|
.sorted((a, b) => b.views.compareTo(a.views))
|
||||||
.map((sibling) {
|
.map((sibling) {
|
||||||
int score = 0;
|
int score = 0;
|
||||||
|
|
||||||
for (final artist in artists) {
|
for (final artist in track.artists) {
|
||||||
final isSameChannelArtist =
|
final isSameChannelArtist =
|
||||||
sibling.channelName.toLowerCase() == artist.toLowerCase();
|
sibling.channelName.toLowerCase() == artist.toLowerCase();
|
||||||
final channelContainsArtist = sibling.channelName
|
final channelContainsArtist = sibling.channelName
|
||||||
@ -212,7 +177,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final titleContainsTrackName =
|
final titleContainsTrackName =
|
||||||
sibling.title.toLowerCase().contains(track.name!.toLowerCase());
|
sibling.title.toLowerCase().contains(track.title.toLowerCase());
|
||||||
|
|
||||||
final hasOfficialFlag =
|
final hasOfficialFlag =
|
||||||
officialMusicRegex.hasMatch(sibling.title.toLowerCase());
|
officialMusicRegex.hasMatch(sibling.title.toLowerCase());
|
||||||
@ -237,12 +202,12 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
|
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
|
||||||
required Track track,
|
required TrackSourceQuery track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final isrcResults = <YoutubeVideoInfo>[];
|
final isrcResults = <YoutubeVideoInfo>[];
|
||||||
final isrc = track.externalIds?.isrc;
|
final isrc = track.isrc;
|
||||||
if (isrc != null && isrc.isNotEmpty) {
|
if (isrc.isNotEmpty) {
|
||||||
final searchedVideos =
|
final searchedVideos =
|
||||||
await ref.read(youtubeEngineProvider).searchVideos(isrc.toString());
|
await ref.read(youtubeEngineProvider).searchVideos(isrc.toString());
|
||||||
if (searchedVideos.isNotEmpty) {
|
if (searchedVideos.isNotEmpty) {
|
||||||
@ -254,15 +219,18 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
|
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
|
||||||
.split(RegExp(r'\p{Z}+', unicode: true))
|
.split(RegExp(r'\p{Z}+', unicode: true))
|
||||||
.where((item) => item.isNotEmpty);
|
.where((item) => item.isNotEmpty);
|
||||||
final spWords = track.name!
|
final spWords = track.title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
|
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
|
||||||
.split(RegExp(r'\p{Z}+', unicode: true))
|
.split(RegExp(r'\p{Z}+', unicode: true))
|
||||||
.where((item) => item.isNotEmpty);
|
.where((item) => item.isNotEmpty);
|
||||||
// Single word and duration match with 3 second tolerance
|
// Single word and duration match with 3 second tolerance
|
||||||
if (ytWords.any((word) => spWords.contains(word)) &&
|
if (ytWords.any((word) => spWords.contains(word)) &&
|
||||||
(videoInfo.duration - track.duration!)
|
(videoInfo.duration -
|
||||||
.abs().inMilliseconds <= 3000) {
|
Duration(milliseconds: track.durationMs))
|
||||||
|
.abs()
|
||||||
|
.inMilliseconds <=
|
||||||
|
3000) {
|
||||||
return videoInfo;
|
return videoInfo;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -275,21 +243,21 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required TrackSourceQuery query,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final videoResults = <YoutubeVideoInfo>[];
|
final videoResults = <YoutubeVideoInfo>[];
|
||||||
|
|
||||||
if (track is! SourcedTrack) {
|
if (query is! SourcedTrack) {
|
||||||
final isrcResults = await fetchFromIsrc(
|
final isrcResults = await fetchFromIsrc(
|
||||||
track: track,
|
track: query,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
|
|
||||||
videoResults.addAll(isrcResults);
|
videoResults.addAll(isrcResults);
|
||||||
|
|
||||||
if (isrcResults.isEmpty) {
|
if (isrcResults.isEmpty) {
|
||||||
final links = await SongLinkService.links(track.id!);
|
final links = await SongLinkService.links(query.id);
|
||||||
final ytLink = links.firstWhereOrNull(
|
final ytLink = links.firstWhereOrNull(
|
||||||
(link) => link.platform == "youtube",
|
(link) => link.platform == "youtube",
|
||||||
);
|
);
|
||||||
@ -308,18 +276,18 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final searchQuery = SourcedTrack.getSearchTerm(query);
|
||||||
|
|
||||||
final searchResults =
|
final searchResults =
|
||||||
await ref.read(youtubeEngineProvider).searchVideos(query);
|
await ref.read(youtubeEngineProvider).searchVideos(searchQuery);
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(searchQuery)) {
|
||||||
videoResults
|
videoResults
|
||||||
.addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList());
|
.addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList());
|
||||||
} else {
|
} else {
|
||||||
videoResults.addAll(rankResults(
|
videoResults.addAll(rankResults(
|
||||||
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
||||||
track,
|
query,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,8 +306,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<YoutubeSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<YoutubeSourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == info.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +318,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
? sibling
|
? sibling
|
||||||
: siblings.firstWhere((s) => s.id == sibling.id);
|
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
..insert(0, sourceInfo);
|
..insert(0, info);
|
||||||
|
|
||||||
final manifest = await ref
|
final manifest = await ref
|
||||||
.read(youtubeEngineProvider)
|
.read(youtubeEngineProvider)
|
||||||
@ -360,7 +328,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
await database.into(database.sourceMatchTable).insert(
|
await database.into(database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: id!,
|
trackId: query.id,
|
||||||
sourceId: newSourceInfo.id,
|
sourceId: newSourceInfo.id,
|
||||||
sourceType: const Value(SourceType.youtube),
|
sourceType: const Value(SourceType.youtube),
|
||||||
// Because we're sorting by createdAt in the query
|
// Because we're sorting by createdAt in the query
|
||||||
@ -372,10 +340,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
return YoutubeSourcedTrack(
|
return YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
|
source: source,
|
||||||
siblings: newSiblings,
|
siblings: newSiblings,
|
||||||
source: toSourceMap(manifest),
|
sources: toTrackSources(manifest),
|
||||||
sourceInfo: newSourceInfo,
|
info: info,
|
||||||
track: this,
|
query: query,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,17 +353,37 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
if (siblings.isNotEmpty) {
|
if (siblings.isNotEmpty) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
|
||||||
return YoutubeSourcedTrack(
|
return YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: fetchedSiblings
|
siblings: fetchedSiblings
|
||||||
.where((s) => s.info.id != sourceInfo.id)
|
.where((s) => s.info.id != info.id)
|
||||||
.map((s) => s.info)
|
.map((s) => s.info)
|
||||||
.toList(),
|
.toList(),
|
||||||
source: source,
|
source: source,
|
||||||
sourceInfo: sourceInfo,
|
sources: sources,
|
||||||
track: this,
|
info: info,
|
||||||
|
query: query,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SourcedTrack> refreshStream() async {
|
||||||
|
final manifest =
|
||||||
|
await ref.read(youtubeEngineProvider).getStreamManifest(info.id);
|
||||||
|
|
||||||
|
final sourcedTrack = YoutubeSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: siblings,
|
||||||
|
source: source,
|
||||||
|
sources: toTrackSources(manifest),
|
||||||
|
info: info,
|
||||||
|
query: query,
|
||||||
|
);
|
||||||
|
|
||||||
|
AppLogger.log.i("Refreshing ${query.title}: ${sourcedTrack.url}");
|
||||||
|
|
||||||
|
return sourcedTrack;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,10 +195,9 @@ abstract class ServiceUtils {
|
|||||||
|
|
||||||
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
|
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
|
||||||
static Future<SubtitleSimple?> getTimedLyrics(SourcedTrack track) async {
|
static Future<SubtitleSimple?> getTimedLyrics(SourcedTrack track) async {
|
||||||
final artistNames =
|
final artistNames = track.query.artists;
|
||||||
track.artists?.map((artist) => artist.name!).toList() ?? [];
|
|
||||||
final query = getTitle(
|
final query = getTitle(
|
||||||
track.name!,
|
track.query.title,
|
||||||
artists: artistNames,
|
artists: artistNames,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -217,13 +216,11 @@ abstract class ServiceUtils {
|
|||||||
final rateSortedResults = results.map((result) {
|
final rateSortedResults = results.map((result) {
|
||||||
final title = result.text.trim().toLowerCase();
|
final title = result.text.trim().toLowerCase();
|
||||||
int points = 0;
|
int points = 0;
|
||||||
final hasAllArtists = track.artists
|
final hasAllArtists = track.query.artists
|
||||||
?.map((artist) => artist.name!)
|
.every((artist) => title.contains(artist.toLowerCase()));
|
||||||
.every((artist) => title.contains(artist.toLowerCase())) ??
|
final hasTrackName = title.contains(track.query.title.toLowerCase());
|
||||||
false;
|
|
||||||
final hasTrackName = title.contains(track.name!.toLowerCase());
|
|
||||||
final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live");
|
final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live");
|
||||||
final exactYtMatch = title == track.sourceInfo.title.toLowerCase();
|
final exactYtMatch = title == track.info.title.toLowerCase();
|
||||||
if (exactYtMatch) points = 7;
|
if (exactYtMatch) points = 7;
|
||||||
for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) {
|
for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) {
|
||||||
if (criteria) points++;
|
if (criteria) points++;
|
||||||
|
Loading…
Reference in New Issue
Block a user