From 4e6db8b9e1f219c24d4d0185ccd0b0478381c519 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 18 Jun 2025 21:30:32 +0600 Subject: [PATCH] refactor: move from Track to SpotubeTrackObject and use TrackSources object for providers --- lib/extensions/track.dart | 10 +- .../configurators/use_endless_playback.dart | 135 +- lib/models/connect/connect.dart | 2 +- lib/models/connect/connect.freezed.dart | 206 ++- lib/models/connect/connect.g.dart | 14 +- lib/models/connect/load.dart | 12 +- lib/models/connect/ws_event.dart | 9 +- lib/models/current_playlist.dart | 75 - lib/models/database/database.dart | 4 +- lib/models/database/database.g.dart | 1402 ++--------------- .../database/tables/audio_player_state.dart | 36 +- .../database/typeconverters/map_list.dart | 20 + lib/models/metadata/metadata.dart | 5 + lib/models/metadata/metadata.freezed.dart | 455 +++++- lib/models/metadata/metadata.g.dart | 31 + lib/models/metadata/track.dart | 46 +- lib/models/playback/track_sources.dart | 94 ++ .../playback/track_sources.freezed.dart | 776 +++++++++ lib/models/playback/track_sources.g.dart | 110 ++ lib/modules/player/player.dart | 4 +- lib/modules/player/sibling_tracks_sheet.dart | 6 +- lib/provider/audio_player/audio_player.dart | 181 +-- .../audio_player/audio_player_streams.dart | 49 +- .../audio_player/querying_track_info.dart | 30 +- lib/provider/audio_player/state.dart | 130 +- lib/provider/audio_player/state.freezed.dart | 297 ++++ lib/provider/audio_player/state.g.dart | 40 + lib/provider/blacklist_provider.dart | 25 +- lib/provider/connect/connect.dart | 7 +- lib/provider/discord_provider.dart | 14 +- lib/provider/download_manager_provider.dart | 101 +- lib/provider/glance/glance.dart | 8 +- lib/provider/history/history.dart | 22 +- lib/provider/scrobbler/scrobbler.dart | 30 +- lib/provider/server/active_sourced_track.dart | 47 - lib/provider/server/active_track_sources.dart | 42 + lib/provider/server/routes/connect.dart | 12 +- lib/provider/server/routes/playback.dart | 62 +- lib/provider/server/sourced_track.dart | 49 - lib/provider/server/track_sources.dart | 45 + lib/provider/skip_segments/skip_segments.dart | 19 +- lib/services/audio_player/audio_player.dart | 55 +- .../audio_services/audio_services.dart | 16 +- .../audio_services/windows_audio_service.dart | 12 +- lib/services/sourced_track/enums.dart | 8 +- lib/services/sourced_track/exceptions.dart | 6 +- .../sourced_track/models/source_info.dart | 33 - .../sourced_track/models/source_info.g.dart | 30 - .../sourced_track/models/source_map.dart | 58 - .../sourced_track/models/source_map.g.dart | 36 - lib/services/sourced_track/sourced_track.dart | 140 +- .../sourced_track/sources/invidious.dart | 167 +- .../sourced_track/sources/jiosaavn.dart | 130 +- lib/services/sourced_track/sources/piped.dart | 167 +- .../sourced_track/sources/youtube.dart | 199 ++- lib/utils/service_utils.dart | 15 +- 56 files changed, 3054 insertions(+), 2680 deletions(-) delete mode 100644 lib/models/current_playlist.dart create mode 100644 lib/models/database/typeconverters/map_list.dart create mode 100644 lib/models/playback/track_sources.dart create mode 100644 lib/models/playback/track_sources.freezed.dart create mode 100644 lib/models/playback/track_sources.g.dart create mode 100644 lib/provider/audio_player/state.freezed.dart create mode 100644 lib/provider/audio_player/state.g.dart delete mode 100644 lib/provider/server/active_sourced_track.dart create mode 100644 lib/provider/server/active_track_sources.dart delete mode 100644 lib/provider/server/sourced_track.dart create mode 100644 lib/provider/server/track_sources.dart delete mode 100644 lib/services/sourced_track/models/source_info.dart delete mode 100644 lib/services/sourced_track/models/source_info.g.dart delete mode 100644 lib/services/sourced_track/models/source_map.dart delete mode 100644 lib/services/sourced_track/models/source_map.g.dart diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart index 92d8b0da..bfe1f639 100644 --- a/lib/extensions/track.dart +++ b/lib/extensions/track.dart @@ -73,8 +73,8 @@ extension IterableTrackSimpleExtensions on Iterable { Future> asTracks(AlbumSimple album, ref) async { try { final spotify = ref.read(spotifyProvider); - final tracks = await spotify.invoke( - (api) => api.tracks.list(map((trackSimple) => trackSimple.id!).toList())); + final tracks = await spotify.invoke((api) => + api.tracks.list(map((trackSimple) => trackSimple.id!).toList())); return tracks.toList(); } catch (e, stack) { // Ignore errors and create the track locally @@ -105,9 +105,3 @@ extension IterableTrackSimpleExtensions on Iterable { } } } - -extension TracksToMediaExtension on Iterable { - List asMediaList() { - return map((track) => SpotubeMedia(track)).toList(); - } -} diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index b86a4865..96628442 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -1,3 +1,4 @@ +import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_hooks/flutter_hooks.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/services/audio_player/audio_player.dart'; +// TODO: Implement endless playback functionality void useEndlessPlayback(WidgetRef ref) { - final auth = ref.watch(authenticationProvider); - final playback = ref.watch(audioPlayerProvider.notifier); - final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist)); - final spotify = ref.watch(spotifyProvider); - final endlessPlayback = - ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); + // final auth = ref.watch(authenticationProvider); + // final playback = ref.watch(audioPlayerProvider.notifier); + // final audioPlayerState = ref.watch(audioPlayerProvider); + // final spotify = ref.watch(spotifyProvider); + // final endlessPlayback = + // ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); - useEffect( - () { - if (!endlessPlayback || auth.asData?.value == null) return null; + // useEffect( + // () { + // if (!endlessPlayback || auth.asData?.value == null) return null; - void listener(int index) async { - try { - final playlist = ref.read(audioPlayerProvider); - if (index != playlist.tracks.length - 1) return; + // void listener(int index) async { + // try { + // final playlist = ref.read(audioPlayerProvider); + // if (index != playlist.tracks.length - 1) return; - final track = playlist.tracks.last; + // final track = playlist.tracks.last; - final query = "${track.name} Radio"; - final pages = await spotify.invoke((api) => - api.search.get(query, types: [SearchType.playlist]).first()); + // final query = "${track.name} Radio"; + // final pages = await spotify.invoke((api) => + // api.search.get(query, types: [SearchType.playlist]).first()); - final radios = pages - .expand((e) => e.items?.toList() ?? []) - .toList() - .cast(); + // final radios = pages + // .expand((e) => e.items?.toList() ?? []) + // .toList() + // .cast(); - final artists = track.artists!.map((e) => e.name); + // final artists = track.artists.map((e) => e.name); - final radio = radios.firstWhere( - (e) { - final validPlaylists = - artists.where((a) => e.description!.contains(a!)); - return e.name == "${track.name} Radio" && - (validPlaylists.length >= 2 || - validPlaylists.length == artists.length) && - e.owner?.displayName != "Spotify"; - }, - orElse: () => radios.first, - ); + // final radio = radios.firstWhere( + // (e) { + // final validPlaylists = + // artists.where((a) => e.description!.contains(a)); + // return e.name == "${track.name} Radio" && + // (validPlaylists.length >= 2 || + // validPlaylists.length == artists.length) && + // e.owner?.displayName != "Spotify"; + // }, + // orElse: () => radios.first, + // ); - final tracks = await spotify.invoke( - (api) => api.playlists.getTracksByPlaylistId(radio.id!).all()); + // final tracks = + // ref.read(metadataPluginPlaylistTracksProvider(radio.id!)); - await playback.addTracks( - tracks.toList() - ..removeWhere((e) { - final playlist = ref.read(audioPlayerProvider); - final isDuplicate = playlist.tracks.any((t) => t.id == e.id); - return e.id == track.id || isDuplicate; - }), - ); - } catch (e, stack) { - AppLogger.reportError(e, stack); - } - } + // await playback.addTracks( + // tracks.toList() + // ..removeWhere((e) { + // final playlist = ref.read(audioPlayerProvider); + // final isDuplicate = playlist.tracks.any((t) => t.id == e.id); + // return e.id == track.id || isDuplicate; + // }), + // ); + // } catch (e, stack) { + // AppLogger.reportError(e, stack); + // } + // } - // Sometimes user can change settings for which the currentIndexChanged - // 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. - if (playlist.index == playlist.medias.length - 1 && - audioPlayer.isPlaying) { - listener(playlist.index); - } + // // Sometimes user can change settings for which the currentIndexChanged + // // 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. + // if (audioPlayerState.currentIndex == audioPlayerState.tracks.length - 1 && + // audioPlayer.isPlaying) { + // listener(audioPlayerState.currentIndex); + // } - final subscription = - audioPlayer.currentIndexChangedStream.listen(listener); + // final subscription = + // audioPlayer.currentIndexChangedStream.listen(listener); - return subscription.cancel; - }, - [ - spotify, - playback, - playlist.medias, - endlessPlayback, - auth, - ], - ); + // return subscription.cancel; + // }, + // [ + // spotify, + // playback, + // audioPlayerState.tracks, + // audioPlayerState.currentIndex, + // endlessPlayback, + // auth, + // ], + // ); } diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart index a70520ad..11370dcb 100644 --- a/lib/models/connect/connect.dart +++ b/lib/models/connect/connect.dart @@ -5,7 +5,7 @@ import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; 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'; part 'connect.freezed.dart'; diff --git a/lib/models/connect/connect.freezed.dart b/lib/models/connect/connect.freezed.dart index 9103dd2b..ead14cf8 100644 --- a/lib/models/connect/connect.freezed.dart +++ b/lib/models/connect/connect.freezed.dart @@ -33,49 +33,36 @@ WebSocketLoadEventData _$WebSocketLoadEventDataFromJson( /// @nodoc mixin _$WebSocketLoadEventData { - @JsonKey(name: 'tracks', toJson: _tracksJson) - List get tracks => throw _privateConstructorUsedError; + List get tracks => throw _privateConstructorUsedError; Object? get collection => throw _privateConstructorUsedError; int? get initialIndex => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ - required TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex) + required TResult Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex) playlist, - required TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex) + required TResult Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex) album, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult? Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex)? + TResult? Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex)? playlist, - TResult? Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex)? + TResult? Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex)? album, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ - TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex)? + TResult Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex)? playlist, - TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex)? + TResult Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex)? album, required TResult orElse(), }) => @@ -116,9 +103,7 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> { $Res Function(WebSocketLoadEventData) then) = _$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>; @useResult - $Res call( - {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - int? initialIndex}); + $Res call({List tracks, int? initialIndex}); } /// @nodoc @@ -144,7 +129,7 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res, tracks: null == tracks ? _value.tracks : tracks // ignore: cast_nullable_to_non_nullable - as List, + as List, initialIndex: freezed == initialIndex ? _value.initialIndex : initialIndex // ignore: cast_nullable_to_non_nullable @@ -163,9 +148,11 @@ abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, + {List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex}); + + $SpotubeSimplePlaylistObjectCopyWith<$Res>? get collection; } /// @nodoc @@ -191,17 +178,32 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res> tracks: null == tracks ? _value._tracks : tracks // ignore: cast_nullable_to_non_nullable - as List, + as List, collection: freezed == collection ? _value.collection : collection // ignore: cast_nullable_to_non_nullable - as PlaylistSimple?, + as SpotubeSimplePlaylistObject?, initialIndex: freezed == initialIndex ? _value.initialIndex : initialIndex // ignore: cast_nullable_to_non_nullable 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 @@ -209,8 +211,7 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res> class _$WebSocketLoadEventDataPlaylistImpl extends WebSocketLoadEventDataPlaylist { _$WebSocketLoadEventDataPlaylistImpl( - {@JsonKey(name: 'tracks', toJson: _tracksJson) - required final List tracks, + {required final List tracks, this.collection, this.initialIndex, final String? $type}) @@ -222,17 +223,16 @@ class _$WebSocketLoadEventDataPlaylistImpl Map json) => _$$WebSocketLoadEventDataPlaylistImplFromJson(json); - final List _tracks; + final List _tracks; @override - @JsonKey(name: 'tracks', toJson: _tracksJson) - List get tracks { + List get tracks { if (_tracks is EqualUnmodifiableListView) return _tracks; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_tracks); } @override - final PlaylistSimple? collection; + final SpotubeSimplePlaylistObject? collection; @override final int? initialIndex; @@ -274,15 +274,11 @@ class _$WebSocketLoadEventDataPlaylistImpl @override @optionalTypeArgs TResult when({ - required TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex) + required TResult Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex) playlist, - required TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex) + required TResult Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex) album, }) { return playlist(tracks, collection, initialIndex); @@ -291,15 +287,11 @@ class _$WebSocketLoadEventDataPlaylistImpl @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex)? + TResult? Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex)? playlist, - TResult? Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex)? + TResult? Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex)? album, }) { return playlist?.call(tracks, collection, initialIndex); @@ -308,15 +300,11 @@ class _$WebSocketLoadEventDataPlaylistImpl @override @optionalTypeArgs TResult maybeWhen({ - TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex)? + TResult Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex)? playlist, - TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex)? + TResult Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex)? album, required TResult orElse(), }) { @@ -367,9 +355,8 @@ class _$WebSocketLoadEventDataPlaylistImpl abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData { factory WebSocketLoadEventDataPlaylist( - {@JsonKey(name: 'tracks', toJson: _tracksJson) - required final List tracks, - final PlaylistSimple? collection, + {required final List tracks, + final SpotubeSimplePlaylistObject? collection, final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl; WebSocketLoadEventDataPlaylist._() : super._(); @@ -377,10 +364,9 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData { _$WebSocketLoadEventDataPlaylistImpl.fromJson; @override - @JsonKey(name: 'tracks', toJson: _tracksJson) - List get tracks; + List get tracks; @override - PlaylistSimple? get collection; + SpotubeSimplePlaylistObject? get collection; @override int? get initialIndex; @@ -403,9 +389,11 @@ abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, + {List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex}); + + $SpotubeSimpleAlbumObjectCopyWith<$Res>? get collection; } /// @nodoc @@ -431,25 +419,38 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res> tracks: null == tracks ? _value._tracks : tracks // ignore: cast_nullable_to_non_nullable - as List, + as List, collection: freezed == collection ? _value.collection : collection // ignore: cast_nullable_to_non_nullable - as AlbumSimple?, + as SpotubeSimpleAlbumObject?, initialIndex: freezed == initialIndex ? _value.initialIndex : initialIndex // ignore: cast_nullable_to_non_nullable 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 @JsonSerializable() class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { _$WebSocketLoadEventDataAlbumImpl( - {@JsonKey(name: 'tracks', toJson: _tracksJson) - required final List tracks, + {required final List tracks, this.collection, this.initialIndex, final String? $type}) @@ -461,17 +462,16 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { Map json) => _$$WebSocketLoadEventDataAlbumImplFromJson(json); - final List _tracks; + final List _tracks; @override - @JsonKey(name: 'tracks', toJson: _tracksJson) - List get tracks { + List get tracks { if (_tracks is EqualUnmodifiableListView) return _tracks; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_tracks); } @override - final AlbumSimple? collection; + final SpotubeSimpleAlbumObject? collection; @override final int? initialIndex; @@ -512,15 +512,11 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { @override @optionalTypeArgs TResult when({ - required TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex) + required TResult Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex) playlist, - required TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex) + required TResult Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex) album, }) { return album(tracks, collection, initialIndex); @@ -529,15 +525,11 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex)? + TResult? Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex)? playlist, - TResult? Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex)? + TResult? Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex)? album, }) { return album?.call(tracks, collection, initialIndex); @@ -546,15 +538,11 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { @override @optionalTypeArgs TResult maybeWhen({ - TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - PlaylistSimple? collection, - int? initialIndex)? + TResult Function(List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex)? playlist, - TResult Function( - @JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, - AlbumSimple? collection, - int? initialIndex)? + TResult Function(List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex)? album, required TResult orElse(), }) { @@ -605,9 +593,8 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData { factory WebSocketLoadEventDataAlbum( - {@JsonKey(name: 'tracks', toJson: _tracksJson) - required final List tracks, - final AlbumSimple? collection, + {required final List tracks, + final SpotubeSimpleAlbumObject? collection, final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl; WebSocketLoadEventDataAlbum._() : super._(); @@ -615,10 +602,9 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData { _$WebSocketLoadEventDataAlbumImpl.fromJson; @override - @JsonKey(name: 'tracks', toJson: _tracksJson) - List get tracks; + List get tracks; @override - AlbumSimple? get collection; + SpotubeSimpleAlbumObject? get collection; @override int? get initialIndex; diff --git a/lib/models/connect/connect.g.dart b/lib/models/connect/connect.g.dart index 10f46c65..465dc42e 100644 --- a/lib/models/connect/connect.g.dart +++ b/lib/models/connect/connect.g.dart @@ -10,11 +10,12 @@ _$WebSocketLoadEventDataPlaylistImpl _$$WebSocketLoadEventDataPlaylistImplFromJson(Map json) => _$WebSocketLoadEventDataPlaylistImpl( tracks: (json['tracks'] as List) - .map((e) => Track.fromJson(Map.from(e as Map))) + .map((e) => SpotubeFullTrackObject.fromJson( + Map.from(e as Map))) .toList(), collection: json['collection'] == null ? null - : PlaylistSimple.fromJson( + : SpotubeSimplePlaylistObject.fromJson( Map.from(json['collection'] as Map)), initialIndex: (json['initialIndex'] as num?)?.toInt(), $type: json['runtimeType'] as String?, @@ -23,7 +24,7 @@ _$WebSocketLoadEventDataPlaylistImpl Map _$$WebSocketLoadEventDataPlaylistImplToJson( _$WebSocketLoadEventDataPlaylistImpl instance) => { - 'tracks': _tracksJson(instance.tracks), + 'tracks': instance.tracks.map((e) => e.toJson()).toList(), 'collection': instance.collection?.toJson(), 'initialIndex': instance.initialIndex, 'runtimeType': instance.$type, @@ -33,11 +34,12 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson( Map json) => _$WebSocketLoadEventDataAlbumImpl( tracks: (json['tracks'] as List) - .map((e) => Track.fromJson(Map.from(e as Map))) + .map((e) => SpotubeFullTrackObject.fromJson( + Map.from(e as Map))) .toList(), collection: json['collection'] == null ? null - : AlbumSimple.fromJson( + : SpotubeSimpleAlbumObject.fromJson( Map.from(json['collection'] as Map)), initialIndex: (json['initialIndex'] as num?)?.toInt(), $type: json['runtimeType'] as String?, @@ -46,7 +48,7 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson( Map _$$WebSocketLoadEventDataAlbumImplToJson( _$WebSocketLoadEventDataAlbumImpl instance) => { - 'tracks': _tracksJson(instance.tracks), + 'tracks': instance.tracks.map((e) => e.toJson()).toList(), 'collection': instance.collection?.toJson(), 'initialIndex': instance.initialIndex, 'runtimeType': instance.$type, diff --git a/lib/models/connect/load.dart b/lib/models/connect/load.dart index bf0e164d..36d3c3ba 100644 --- a/lib/models/connect/load.dart +++ b/lib/models/connect/load.dart @@ -1,22 +1,18 @@ part of 'connect.dart'; -List> _tracksJson(List tracks) { - return tracks.map((e) => e.toJson()).toList(); -} - @freezed class WebSocketLoadEventData with _$WebSocketLoadEventData { const WebSocketLoadEventData._(); factory WebSocketLoadEventData.playlist({ - @JsonKey(name: 'tracks', toJson: _tracksJson) required List tracks, - PlaylistSimple? collection, + required List tracks, + SpotubeSimplePlaylistObject? collection, int? initialIndex, }) = WebSocketLoadEventDataPlaylist; factory WebSocketLoadEventData.album({ - @JsonKey(name: 'tracks', toJson: _tracksJson) required List tracks, - AlbumSimple? collection, + required List tracks, + SpotubeSimpleAlbumObject? collection, int? initialIndex, }) = WebSocketLoadEventDataAlbum; diff --git a/lib/models/connect/ws_event.dart b/lib/models/connect/ws_event.dart index d1047646..7867f686 100644 --- a/lib/models/connect/ws_event.dart +++ b/lib/models/connect/ws_event.dart @@ -338,13 +338,16 @@ class WebSocketRemoveTrackEvent extends WebSocketEvent { WebSocketRemoveTrackEvent(String data) : super(WsEvent.removeTrack, data); } -class WebSocketAddTrackEvent extends WebSocketEvent { - WebSocketAddTrackEvent(Track data) : super(WsEvent.addTrack, data); +class WebSocketAddTrackEvent extends WebSocketEvent { + WebSocketAddTrackEvent(SpotubeFullTrackObject data) + : super(WsEvent.addTrack, data); WebSocketAddTrackEvent.fromJson(Map json) : super( WsEvent.addTrack, - Track.fromJson(json["data"] as Map), + SpotubeFullTrackObject.fromJson( + json["data"] as Map, + ), ); } diff --git a/lib/models/current_playlist.dart b/lib/models/current_playlist.dart deleted file mode 100644 index 7e55e393..00000000 --- a/lib/models/current_playlist.dart +++ /dev/null @@ -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? _tempTrack; - List 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 map, Ref ref) { - return CurrentPlaylist( - id: map["id"], - tracks: List.castFrom(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 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 toJson() { - return { - "id": id, - "name": name, - "tracks": tracks - .map((track) => - track is SourcedTrack ? track.toJson() : track.toJson()) - .toList(), - "thumbnail": thumbnail, - "isLocal": isLocal, - }; - } -} diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 20aa649a..8186fd92 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -13,6 +13,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors; import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/database/database.steps.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/kv_store.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -43,6 +44,7 @@ part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; part 'typeconverters/encrypted_text.dart'; part 'typeconverters/map.dart'; +part 'typeconverters/map_list.dart'; part 'typeconverters/subtitle.dart'; @DriftDatabase( @@ -54,8 +56,6 @@ part 'typeconverters/subtitle.dart'; SkipSegmentTable, SourceMatchTable, AudioPlayerStateTable, - PlaylistTable, - PlaylistMediaTable, HistoryTable, LyricsTable, MetadataPluginsTable, diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 02f75640..35905ce4 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -2926,9 +2926,22 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable type: DriftSqlType.string, requiredDuringInsert: true) .withConverter>( $AudioPlayerStateTableTable.$convertercollections); + static const VerificationMeta _tracksMeta = const VerificationMeta('tracks'); + @override + late final GeneratedColumnWithTypeConverter, String> + tracks = GeneratedColumn('tracks', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $AudioPlayerStateTableTable.$convertertracks); + static const VerificationMeta _currentIndexMeta = + const VerificationMeta('currentIndex'); + @override + late final GeneratedColumn currentIndex = GeneratedColumn( + 'current_index', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); @override List get $columns => - [id, playing, loopMode, shuffled, collections]; + [id, playing, loopMode, shuffled, collections, tracks, currentIndex]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2957,6 +2970,15 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable context.missing(_shuffledMeta); } context.handle(_collectionsMeta, const VerificationResult.success()); + context.handle(_tracksMeta, const VerificationResult.success()); + if (data.containsKey('current_index')) { + context.handle( + _currentIndexMeta, + currentIndex.isAcceptableOrUnknown( + data['current_index']!, _currentIndexMeta)); + } else if (isInserting) { + context.missing(_currentIndexMeta); + } return context; } @@ -2979,6 +3001,11 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable collections: $AudioPlayerStateTableTable.$convertercollections.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}collections'])!), + tracks: $AudioPlayerStateTableTable.$convertertracks.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}tracks'])!), + currentIndex: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}current_index'])!, ); } @@ -2991,6 +3018,8 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable const EnumNameConverter(PlaylistMode.values); static TypeConverter, String> $convertercollections = const StringListConverter(); + static TypeConverter, String> $convertertracks = + const SpotubeTrackObjectListConverter(); } class AudioPlayerStateTableData extends DataClass @@ -3000,12 +3029,16 @@ class AudioPlayerStateTableData extends DataClass final PlaylistMode loopMode; final bool shuffled; final List collections; + final List tracks; + final int currentIndex; const AudioPlayerStateTableData( {required this.id, required this.playing, required this.loopMode, required this.shuffled, - required this.collections}); + required this.collections, + required this.tracks, + required this.currentIndex}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -3020,6 +3053,11 @@ class AudioPlayerStateTableData extends DataClass map['collections'] = Variable( $AudioPlayerStateTableTable.$convertercollections.toSql(collections)); } + { + map['tracks'] = Variable( + $AudioPlayerStateTableTable.$convertertracks.toSql(tracks)); + } + map['current_index'] = Variable(currentIndex); return map; } @@ -3030,6 +3068,8 @@ class AudioPlayerStateTableData extends DataClass loopMode: Value(loopMode), shuffled: Value(shuffled), collections: Value(collections), + tracks: Value(tracks), + currentIndex: Value(currentIndex), ); } @@ -3043,6 +3083,8 @@ class AudioPlayerStateTableData extends DataClass .fromJson(serializer.fromJson(json['loopMode'])), shuffled: serializer.fromJson(json['shuffled']), collections: serializer.fromJson>(json['collections']), + tracks: serializer.fromJson>(json['tracks']), + currentIndex: serializer.fromJson(json['currentIndex']), ); } @override @@ -3055,6 +3097,8 @@ class AudioPlayerStateTableData extends DataClass $AudioPlayerStateTableTable.$converterloopMode.toJson(loopMode)), 'shuffled': serializer.toJson(shuffled), 'collections': serializer.toJson>(collections), + 'tracks': serializer.toJson>(tracks), + 'currentIndex': serializer.toJson(currentIndex), }; } @@ -3063,13 +3107,17 @@ class AudioPlayerStateTableData extends DataClass bool? playing, PlaylistMode? loopMode, bool? shuffled, - List? collections}) => + List? collections, + List? tracks, + int? currentIndex}) => AudioPlayerStateTableData( id: id ?? this.id, playing: playing ?? this.playing, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, ); AudioPlayerStateTableData copyWithCompanion( AudioPlayerStateTableCompanion data) { @@ -3080,6 +3128,10 @@ class AudioPlayerStateTableData extends DataClass shuffled: data.shuffled.present ? data.shuffled.value : this.shuffled, collections: data.collections.present ? data.collections.value : this.collections, + tracks: data.tracks.present ? data.tracks.value : this.tracks, + currentIndex: data.currentIndex.present + ? data.currentIndex.value + : this.currentIndex, ); } @@ -3090,13 +3142,16 @@ class AudioPlayerStateTableData extends DataClass ..write('playing: $playing, ') ..write('loopMode: $loopMode, ') ..write('shuffled: $shuffled, ') - ..write('collections: $collections') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, playing, loopMode, shuffled, collections); + int get hashCode => Object.hash( + id, playing, loopMode, shuffled, collections, tracks, currentIndex); @override bool operator ==(Object other) => identical(this, other) || @@ -3105,7 +3160,9 @@ class AudioPlayerStateTableData extends DataClass other.playing == this.playing && other.loopMode == this.loopMode && other.shuffled == this.shuffled && - other.collections == this.collections); + other.collections == this.collections && + other.tracks == this.tracks && + other.currentIndex == this.currentIndex); } class AudioPlayerStateTableCompanion @@ -3115,12 +3172,16 @@ class AudioPlayerStateTableCompanion final Value loopMode; final Value shuffled; final Value> collections; + final Value> tracks; + final Value currentIndex; const AudioPlayerStateTableCompanion({ this.id = const Value.absent(), this.playing = const Value.absent(), this.loopMode = const Value.absent(), this.shuffled = const Value.absent(), this.collections = const Value.absent(), + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), }); AudioPlayerStateTableCompanion.insert({ this.id = const Value.absent(), @@ -3128,16 +3189,22 @@ class AudioPlayerStateTableCompanion required PlaylistMode loopMode, required bool shuffled, required List collections, + required List tracks, + required int currentIndex, }) : playing = Value(playing), loopMode = Value(loopMode), shuffled = Value(shuffled), - collections = Value(collections); + collections = Value(collections), + tracks = Value(tracks), + currentIndex = Value(currentIndex); static Insertable custom({ Expression? id, Expression? playing, Expression? loopMode, Expression? shuffled, Expression? collections, + Expression? tracks, + Expression? currentIndex, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -3145,6 +3212,8 @@ class AudioPlayerStateTableCompanion if (loopMode != null) 'loop_mode': loopMode, if (shuffled != null) 'shuffled': shuffled, if (collections != null) 'collections': collections, + if (tracks != null) 'tracks': tracks, + if (currentIndex != null) 'current_index': currentIndex, }); } @@ -3153,13 +3222,17 @@ class AudioPlayerStateTableCompanion Value? playing, Value? loopMode, Value? shuffled, - Value>? collections}) { + Value>? collections, + Value>? tracks, + Value? currentIndex}) { return AudioPlayerStateTableCompanion( id: id ?? this.id, playing: playing ?? this.playing, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, ); } @@ -3184,6 +3257,13 @@ class AudioPlayerStateTableCompanion .$convertercollections .toSql(collections.value)); } + if (tracks.present) { + map['tracks'] = Variable( + $AudioPlayerStateTableTable.$convertertracks.toSql(tracks.value)); + } + if (currentIndex.present) { + map['current_index'] = Variable(currentIndex.value); + } return map; } @@ -3194,557 +3274,9 @@ class AudioPlayerStateTableCompanion ..write('playing: $playing, ') ..write('loopMode: $loopMode, ') ..write('shuffled: $shuffled, ') - ..write('collections: $collections') - ..write(')')) - .toString(); - } -} - -class $PlaylistTableTable extends PlaylistTable - with TableInfo<$PlaylistTableTable, PlaylistTableData> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $PlaylistTableTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _audioPlayerStateIdMeta = - const VerificationMeta('audioPlayerStateId'); - @override - late final GeneratedColumn audioPlayerStateId = GeneratedColumn( - 'audio_player_state_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES audio_player_state_table (id)')); - static const VerificationMeta _indexMeta = const VerificationMeta('index'); - @override - late final GeneratedColumn index = GeneratedColumn( - 'index', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, audioPlayerStateId, index]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'playlist_table'; - @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('audio_player_state_id')) { - context.handle( - _audioPlayerStateIdMeta, - audioPlayerStateId.isAcceptableOrUnknown( - data['audio_player_state_id']!, _audioPlayerStateIdMeta)); - } else if (isInserting) { - context.missing(_audioPlayerStateIdMeta); - } - if (data.containsKey('index')) { - context.handle( - _indexMeta, index.isAcceptableOrUnknown(data['index']!, _indexMeta)); - } else if (isInserting) { - context.missing(_indexMeta); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - PlaylistTableData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PlaylistTableData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - audioPlayerStateId: attachedDatabase.typeMapping.read( - DriftSqlType.int, data['${effectivePrefix}audio_player_state_id'])!, - index: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}index'])!, - ); - } - - @override - $PlaylistTableTable createAlias(String alias) { - return $PlaylistTableTable(attachedDatabase, alias); - } -} - -class PlaylistTableData extends DataClass - implements Insertable { - final int id; - final int audioPlayerStateId; - final int index; - const PlaylistTableData( - {required this.id, - required this.audioPlayerStateId, - required this.index}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['audio_player_state_id'] = Variable(audioPlayerStateId); - map['index'] = Variable(index); - return map; - } - - PlaylistTableCompanion toCompanion(bool nullToAbsent) { - return PlaylistTableCompanion( - id: Value(id), - audioPlayerStateId: Value(audioPlayerStateId), - index: Value(index), - ); - } - - factory PlaylistTableData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return PlaylistTableData( - id: serializer.fromJson(json['id']), - audioPlayerStateId: serializer.fromJson(json['audioPlayerStateId']), - index: serializer.fromJson(json['index']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'audioPlayerStateId': serializer.toJson(audioPlayerStateId), - 'index': serializer.toJson(index), - }; - } - - PlaylistTableData copyWith({int? id, int? audioPlayerStateId, int? index}) => - PlaylistTableData( - id: id ?? this.id, - audioPlayerStateId: audioPlayerStateId ?? this.audioPlayerStateId, - index: index ?? this.index, - ); - PlaylistTableData copyWithCompanion(PlaylistTableCompanion data) { - return PlaylistTableData( - id: data.id.present ? data.id.value : this.id, - audioPlayerStateId: data.audioPlayerStateId.present - ? data.audioPlayerStateId.value - : this.audioPlayerStateId, - index: data.index.present ? data.index.value : this.index, - ); - } - - @override - String toString() { - return (StringBuffer('PlaylistTableData(') - ..write('id: $id, ') - ..write('audioPlayerStateId: $audioPlayerStateId, ') - ..write('index: $index') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, audioPlayerStateId, index); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is PlaylistTableData && - other.id == this.id && - other.audioPlayerStateId == this.audioPlayerStateId && - other.index == this.index); -} - -class PlaylistTableCompanion extends UpdateCompanion { - final Value id; - final Value audioPlayerStateId; - final Value index; - const PlaylistTableCompanion({ - this.id = const Value.absent(), - this.audioPlayerStateId = const Value.absent(), - this.index = const Value.absent(), - }); - PlaylistTableCompanion.insert({ - this.id = const Value.absent(), - required int audioPlayerStateId, - required int index, - }) : audioPlayerStateId = Value(audioPlayerStateId), - index = Value(index); - static Insertable custom({ - Expression? id, - Expression? audioPlayerStateId, - Expression? index, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (audioPlayerStateId != null) - 'audio_player_state_id': audioPlayerStateId, - if (index != null) 'index': index, - }); - } - - PlaylistTableCompanion copyWith( - {Value? id, Value? audioPlayerStateId, Value? index}) { - return PlaylistTableCompanion( - id: id ?? this.id, - audioPlayerStateId: audioPlayerStateId ?? this.audioPlayerStateId, - index: index ?? this.index, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (audioPlayerStateId.present) { - map['audio_player_state_id'] = Variable(audioPlayerStateId.value); - } - if (index.present) { - map['index'] = Variable(index.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('PlaylistTableCompanion(') - ..write('id: $id, ') - ..write('audioPlayerStateId: $audioPlayerStateId, ') - ..write('index: $index') - ..write(')')) - .toString(); - } -} - -class $PlaylistMediaTableTable extends PlaylistMediaTable - with TableInfo<$PlaylistMediaTableTable, PlaylistMediaTableData> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $PlaylistMediaTableTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _playlistIdMeta = - const VerificationMeta('playlistId'); - @override - late final GeneratedColumn playlistId = GeneratedColumn( - 'playlist_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES playlist_table (id)')); - static const VerificationMeta _uriMeta = const VerificationMeta('uri'); - @override - late final GeneratedColumn uri = GeneratedColumn( - 'uri', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _extrasMeta = const VerificationMeta('extras'); - @override - late final GeneratedColumnWithTypeConverter?, String> - extras = GeneratedColumn('extras', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PlaylistMediaTableTable.$converterextrasn); - static const VerificationMeta _httpHeadersMeta = - const VerificationMeta('httpHeaders'); - @override - late final GeneratedColumnWithTypeConverter?, String> - httpHeaders = GeneratedColumn('http_headers', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PlaylistMediaTableTable.$converterhttpHeadersn); - @override - List get $columns => - [id, playlistId, uri, extras, httpHeaders]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'playlist_media_table'; - @override - VerificationContext validateIntegrity( - Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('playlist_id')) { - context.handle( - _playlistIdMeta, - playlistId.isAcceptableOrUnknown( - data['playlist_id']!, _playlistIdMeta)); - } else if (isInserting) { - context.missing(_playlistIdMeta); - } - if (data.containsKey('uri')) { - context.handle( - _uriMeta, uri.isAcceptableOrUnknown(data['uri']!, _uriMeta)); - } else if (isInserting) { - context.missing(_uriMeta); - } - context.handle(_extrasMeta, const VerificationResult.success()); - context.handle(_httpHeadersMeta, const VerificationResult.success()); - return context; - } - - @override - Set get $primaryKey => {id}; - @override - PlaylistMediaTableData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PlaylistMediaTableData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - playlistId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}playlist_id'])!, - uri: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}uri'])!, - extras: $PlaylistMediaTableTable.$converterextrasn.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extras'])), - httpHeaders: $PlaylistMediaTableTable.$converterhttpHeadersn.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}http_headers'])), - ); - } - - @override - $PlaylistMediaTableTable createAlias(String alias) { - return $PlaylistMediaTableTable(attachedDatabase, alias); - } - - static TypeConverter, String> $converterextras = - const MapTypeConverter(); - static TypeConverter?, String?> $converterextrasn = - NullAwareTypeConverter.wrap($converterextras); - static TypeConverter, String> $converterhttpHeaders = - const MapTypeConverter(); - static TypeConverter?, String?> $converterhttpHeadersn = - NullAwareTypeConverter.wrap($converterhttpHeaders); -} - -class PlaylistMediaTableData extends DataClass - implements Insertable { - final int id; - final int playlistId; - final String uri; - final Map? extras; - final Map? httpHeaders; - const PlaylistMediaTableData( - {required this.id, - required this.playlistId, - required this.uri, - this.extras, - this.httpHeaders}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['playlist_id'] = Variable(playlistId); - map['uri'] = Variable(uri); - if (!nullToAbsent || extras != null) { - map['extras'] = Variable( - $PlaylistMediaTableTable.$converterextrasn.toSql(extras)); - } - if (!nullToAbsent || httpHeaders != null) { - map['http_headers'] = Variable( - $PlaylistMediaTableTable.$converterhttpHeadersn.toSql(httpHeaders)); - } - return map; - } - - PlaylistMediaTableCompanion toCompanion(bool nullToAbsent) { - return PlaylistMediaTableCompanion( - id: Value(id), - playlistId: Value(playlistId), - uri: Value(uri), - extras: - extras == null && nullToAbsent ? const Value.absent() : Value(extras), - httpHeaders: httpHeaders == null && nullToAbsent - ? const Value.absent() - : Value(httpHeaders), - ); - } - - factory PlaylistMediaTableData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return PlaylistMediaTableData( - id: serializer.fromJson(json['id']), - playlistId: serializer.fromJson(json['playlistId']), - uri: serializer.fromJson(json['uri']), - extras: serializer.fromJson?>(json['extras']), - httpHeaders: - serializer.fromJson?>(json['httpHeaders']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'playlistId': serializer.toJson(playlistId), - 'uri': serializer.toJson(uri), - 'extras': serializer.toJson?>(extras), - 'httpHeaders': serializer.toJson?>(httpHeaders), - }; - } - - PlaylistMediaTableData copyWith( - {int? id, - int? playlistId, - String? uri, - Value?> extras = const Value.absent(), - Value?> httpHeaders = const Value.absent()}) => - PlaylistMediaTableData( - id: id ?? this.id, - playlistId: playlistId ?? this.playlistId, - uri: uri ?? this.uri, - extras: extras.present ? extras.value : this.extras, - httpHeaders: httpHeaders.present ? httpHeaders.value : this.httpHeaders, - ); - PlaylistMediaTableData copyWithCompanion(PlaylistMediaTableCompanion data) { - return PlaylistMediaTableData( - id: data.id.present ? data.id.value : this.id, - playlistId: - data.playlistId.present ? data.playlistId.value : this.playlistId, - uri: data.uri.present ? data.uri.value : this.uri, - extras: data.extras.present ? data.extras.value : this.extras, - httpHeaders: - data.httpHeaders.present ? data.httpHeaders.value : this.httpHeaders, - ); - } - - @override - String toString() { - return (StringBuffer('PlaylistMediaTableData(') - ..write('id: $id, ') - ..write('playlistId: $playlistId, ') - ..write('uri: $uri, ') - ..write('extras: $extras, ') - ..write('httpHeaders: $httpHeaders') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, playlistId, uri, extras, httpHeaders); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is PlaylistMediaTableData && - other.id == this.id && - other.playlistId == this.playlistId && - other.uri == this.uri && - other.extras == this.extras && - other.httpHeaders == this.httpHeaders); -} - -class PlaylistMediaTableCompanion - extends UpdateCompanion { - final Value id; - final Value playlistId; - final Value uri; - final Value?> extras; - final Value?> httpHeaders; - const PlaylistMediaTableCompanion({ - this.id = const Value.absent(), - this.playlistId = const Value.absent(), - this.uri = const Value.absent(), - this.extras = const Value.absent(), - this.httpHeaders = const Value.absent(), - }); - PlaylistMediaTableCompanion.insert({ - this.id = const Value.absent(), - required int playlistId, - required String uri, - this.extras = const Value.absent(), - this.httpHeaders = const Value.absent(), - }) : playlistId = Value(playlistId), - uri = Value(uri); - static Insertable custom({ - Expression? id, - Expression? playlistId, - Expression? uri, - Expression? extras, - Expression? httpHeaders, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (playlistId != null) 'playlist_id': playlistId, - if (uri != null) 'uri': uri, - if (extras != null) 'extras': extras, - if (httpHeaders != null) 'http_headers': httpHeaders, - }); - } - - PlaylistMediaTableCompanion copyWith( - {Value? id, - Value? playlistId, - Value? uri, - Value?>? extras, - Value?>? httpHeaders}) { - return PlaylistMediaTableCompanion( - id: id ?? this.id, - playlistId: playlistId ?? this.playlistId, - uri: uri ?? this.uri, - extras: extras ?? this.extras, - httpHeaders: httpHeaders ?? this.httpHeaders, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (playlistId.present) { - map['playlist_id'] = Variable(playlistId.value); - } - if (uri.present) { - map['uri'] = Variable(uri.value); - } - if (extras.present) { - map['extras'] = Variable( - $PlaylistMediaTableTable.$converterextrasn.toSql(extras.value)); - } - if (httpHeaders.present) { - map['http_headers'] = Variable($PlaylistMediaTableTable - .$converterhttpHeadersn - .toSql(httpHeaders.value)); - } - return map; - } - - @override - String toString() { - return (StringBuffer('PlaylistMediaTableCompanion(') - ..write('id: $id, ') - ..write('playlistId: $playlistId, ') - ..write('uri: $uri, ') - ..write('extras: $extras, ') - ..write('httpHeaders: $httpHeaders') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') ..write(')')) .toString(); } @@ -4769,9 +4301,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { $SourceMatchTableTable(this); late final $AudioPlayerStateTableTable audioPlayerStateTable = $AudioPlayerStateTableTable(this); - late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this); - late final $PlaylistMediaTableTable playlistMediaTable = - $PlaylistMediaTableTable(this); late final $HistoryTableTable historyTable = $HistoryTableTable(this); late final $LyricsTableTable lyricsTable = $LyricsTableTable(this); late final $MetadataPluginsTableTable metadataPluginsTable = @@ -4792,8 +4321,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { skipSegmentTable, sourceMatchTable, audioPlayerStateTable, - playlistTable, - playlistMediaTable, historyTable, lyricsTable, metadataPluginsTable, @@ -6224,6 +5751,8 @@ typedef $$AudioPlayerStateTableTableCreateCompanionBuilder required PlaylistMode loopMode, required bool shuffled, required List collections, + required List tracks, + required int currentIndex, }); typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder = AudioPlayerStateTableCompanion Function({ @@ -6232,29 +5761,10 @@ typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder Value loopMode, Value shuffled, Value> collections, + Value> tracks, + Value currentIndex, }); -final class $$AudioPlayerStateTableTableReferences extends BaseReferences< - _$AppDatabase, $AudioPlayerStateTableTable, AudioPlayerStateTableData> { - $$AudioPlayerStateTableTableReferences( - super.$_db, super.$_table, super.$_typedResult); - - static MultiTypedResultKey<$PlaylistTableTable, List> - _playlistTableRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable(db.playlistTable, - aliasName: $_aliasNameGenerator(db.audioPlayerStateTable.id, - db.playlistTable.audioPlayerStateId)); - - $$PlaylistTableTableProcessedTableManager get playlistTableRefs { - final manager = $$PlaylistTableTableTableManager($_db, $_db.playlistTable) - .filter((f) => f.audioPlayerStateId.id($_item.id)); - - final cache = $_typedResult.readTableOrNull(_playlistTableRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } -} - class $$AudioPlayerStateTableTableFilterComposer extends Composer<_$AppDatabase, $AudioPlayerStateTableTable> { $$AudioPlayerStateTableTableFilterComposer({ @@ -6283,26 +5793,14 @@ class $$AudioPlayerStateTableTableFilterComposer column: $table.collections, builder: (column) => ColumnWithTypeConverterFilters(column)); - Expression playlistTableRefs( - Expression Function($$PlaylistTableTableFilterComposer f) f) { - final $$PlaylistTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.playlistTable, - getReferencedColumn: (t) => t.audioPlayerStateId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistTableTableFilterComposer( - $db: $db, - $table: $db.playlistTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } + ColumnWithTypeConverterFilters, + List, String> + get tracks => $composableBuilder( + column: $table.tracks, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get currentIndex => $composableBuilder( + column: $table.currentIndex, builder: (column) => ColumnFilters(column)); } class $$AudioPlayerStateTableTableOrderingComposer @@ -6328,6 +5826,13 @@ class $$AudioPlayerStateTableTableOrderingComposer ColumnOrderings get collections => $composableBuilder( column: $table.collections, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get tracks => $composableBuilder( + column: $table.tracks, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get currentIndex => $composableBuilder( + column: $table.currentIndex, + builder: (column) => ColumnOrderings(column)); } class $$AudioPlayerStateTableTableAnnotationComposer @@ -6355,26 +5860,12 @@ class $$AudioPlayerStateTableTableAnnotationComposer $composableBuilder( column: $table.collections, builder: (column) => column); - Expression playlistTableRefs( - Expression Function($$PlaylistTableTableAnnotationComposer a) f) { - final $$PlaylistTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.playlistTable, - getReferencedColumn: (t) => t.audioPlayerStateId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistTableTableAnnotationComposer( - $db: $db, - $table: $db.playlistTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } + GeneratedColumnWithTypeConverter, String> + get tracks => $composableBuilder( + column: $table.tracks, builder: (column) => column); + + GeneratedColumn get currentIndex => $composableBuilder( + column: $table.currentIndex, builder: (column) => column); } class $$AudioPlayerStateTableTableTableManager extends RootTableManager< @@ -6386,9 +5877,13 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< $$AudioPlayerStateTableTableAnnotationComposer, $$AudioPlayerStateTableTableCreateCompanionBuilder, $$AudioPlayerStateTableTableUpdateCompanionBuilder, - (AudioPlayerStateTableData, $$AudioPlayerStateTableTableReferences), + ( + AudioPlayerStateTableData, + BaseReferences<_$AppDatabase, $AudioPlayerStateTableTable, + AudioPlayerStateTableData> + ), AudioPlayerStateTableData, - PrefetchHooks Function({bool playlistTableRefs})> { + PrefetchHooks Function()> { $$AudioPlayerStateTableTableTableManager( _$AppDatabase db, $AudioPlayerStateTableTable table) : super(TableManagerState( @@ -6409,6 +5904,8 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< Value loopMode = const Value.absent(), Value shuffled = const Value.absent(), Value> collections = const Value.absent(), + Value> tracks = const Value.absent(), + Value currentIndex = const Value.absent(), }) => AudioPlayerStateTableCompanion( id: id, @@ -6416,6 +5913,8 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< loopMode: loopMode, shuffled: shuffled, collections: collections, + tracks: tracks, + currentIndex: currentIndex, ), createCompanionCallback: ({ Value id = const Value.absent(), @@ -6423,6 +5922,8 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< required PlaylistMode loopMode, required bool shuffled, required List collections, + required List tracks, + required int currentIndex, }) => AudioPlayerStateTableCompanion.insert( id: id, @@ -6430,39 +5931,13 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< loopMode: loopMode, shuffled: shuffled, collections: collections, + tracks: tracks, + currentIndex: currentIndex, ), withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$AudioPlayerStateTableTableReferences(db, table, e) - )) + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({playlistTableRefs = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [ - if (playlistTableRefs) db.playlistTable - ], - addJoins: null, - getPrefetchedDataCallback: (items) async { - return [ - if (playlistTableRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $$AudioPlayerStateTableTableReferences - ._playlistTableRefsTable(db), - managerFromTypedResult: (p0) => - $$AudioPlayerStateTableTableReferences( - db, table, p0) - .playlistTableRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.audioPlayerStateId == item.id), - typedResults: items) - ]; - }, - ); - }, + prefetchHooksCallback: null, )); } @@ -6476,610 +5951,13 @@ typedef $$AudioPlayerStateTableTableProcessedTableManager $$AudioPlayerStateTableTableAnnotationComposer, $$AudioPlayerStateTableTableCreateCompanionBuilder, $$AudioPlayerStateTableTableUpdateCompanionBuilder, - (AudioPlayerStateTableData, $$AudioPlayerStateTableTableReferences), + ( + AudioPlayerStateTableData, + BaseReferences<_$AppDatabase, $AudioPlayerStateTableTable, + AudioPlayerStateTableData> + ), AudioPlayerStateTableData, - PrefetchHooks Function({bool playlistTableRefs})>; -typedef $$PlaylistTableTableCreateCompanionBuilder = PlaylistTableCompanion - Function({ - Value id, - required int audioPlayerStateId, - required int index, -}); -typedef $$PlaylistTableTableUpdateCompanionBuilder = PlaylistTableCompanion - Function({ - Value id, - Value audioPlayerStateId, - Value index, -}); - -final class $$PlaylistTableTableReferences extends BaseReferences<_$AppDatabase, - $PlaylistTableTable, PlaylistTableData> { - $$PlaylistTableTableReferences( - super.$_db, super.$_table, super.$_typedResult); - - static $AudioPlayerStateTableTable _audioPlayerStateIdTable( - _$AppDatabase db) => - db.audioPlayerStateTable.createAlias($_aliasNameGenerator( - db.playlistTable.audioPlayerStateId, db.audioPlayerStateTable.id)); - - $$AudioPlayerStateTableTableProcessedTableManager get audioPlayerStateId { - final manager = $$AudioPlayerStateTableTableTableManager( - $_db, $_db.audioPlayerStateTable) - .filter((f) => f.id($_item.audioPlayerStateId!)); - final item = $_typedResult.readTableOrNull(_audioPlayerStateIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - - static MultiTypedResultKey<$PlaylistMediaTableTable, - List> _playlistMediaTableRefsTable( - _$AppDatabase db) => - MultiTypedResultKey.fromTable(db.playlistMediaTable, - aliasName: $_aliasNameGenerator( - db.playlistTable.id, db.playlistMediaTable.playlistId)); - - $$PlaylistMediaTableTableProcessedTableManager get playlistMediaTableRefs { - final manager = - $$PlaylistMediaTableTableTableManager($_db, $_db.playlistMediaTable) - .filter((f) => f.playlistId.id($_item.id)); - - final cache = - $_typedResult.readTableOrNull(_playlistMediaTableRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } -} - -class $$PlaylistTableTableFilterComposer - extends Composer<_$AppDatabase, $PlaylistTableTable> { - $$PlaylistTableTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get index => $composableBuilder( - column: $table.index, builder: (column) => ColumnFilters(column)); - - $$AudioPlayerStateTableTableFilterComposer get audioPlayerStateId { - final $$AudioPlayerStateTableTableFilterComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.audioPlayerStateId, - referencedTable: $db.audioPlayerStateTable, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$AudioPlayerStateTableTableFilterComposer( - $db: $db, - $table: $db.audioPlayerStateTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - - Expression playlistMediaTableRefs( - Expression Function($$PlaylistMediaTableTableFilterComposer f) f) { - final $$PlaylistMediaTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.playlistMediaTable, - getReferencedColumn: (t) => t.playlistId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistMediaTableTableFilterComposer( - $db: $db, - $table: $db.playlistMediaTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } -} - -class $$PlaylistTableTableOrderingComposer - extends Composer<_$AppDatabase, $PlaylistTableTable> { - $$PlaylistTableTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get index => $composableBuilder( - column: $table.index, builder: (column) => ColumnOrderings(column)); - - $$AudioPlayerStateTableTableOrderingComposer get audioPlayerStateId { - final $$AudioPlayerStateTableTableOrderingComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.audioPlayerStateId, - referencedTable: $db.audioPlayerStateTable, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$AudioPlayerStateTableTableOrderingComposer( - $db: $db, - $table: $db.audioPlayerStateTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $$PlaylistTableTableAnnotationComposer - extends Composer<_$AppDatabase, $PlaylistTableTable> { - $$PlaylistTableTableAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get index => - $composableBuilder(column: $table.index, builder: (column) => column); - - $$AudioPlayerStateTableTableAnnotationComposer get audioPlayerStateId { - final $$AudioPlayerStateTableTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.audioPlayerStateId, - referencedTable: $db.audioPlayerStateTable, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$AudioPlayerStateTableTableAnnotationComposer( - $db: $db, - $table: $db.audioPlayerStateTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - - Expression playlistMediaTableRefs( - Expression Function($$PlaylistMediaTableTableAnnotationComposer a) f) { - final $$PlaylistMediaTableTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.playlistMediaTable, - getReferencedColumn: (t) => t.playlistId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistMediaTableTableAnnotationComposer( - $db: $db, - $table: $db.playlistMediaTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } -} - -class $$PlaylistTableTableTableManager extends RootTableManager< - _$AppDatabase, - $PlaylistTableTable, - PlaylistTableData, - $$PlaylistTableTableFilterComposer, - $$PlaylistTableTableOrderingComposer, - $$PlaylistTableTableAnnotationComposer, - $$PlaylistTableTableCreateCompanionBuilder, - $$PlaylistTableTableUpdateCompanionBuilder, - (PlaylistTableData, $$PlaylistTableTableReferences), - PlaylistTableData, - PrefetchHooks Function( - {bool audioPlayerStateId, bool playlistMediaTableRefs})> { - $$PlaylistTableTableTableManager(_$AppDatabase db, $PlaylistTableTable table) - : super(TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $$PlaylistTableTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$PlaylistTableTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$PlaylistTableTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value audioPlayerStateId = const Value.absent(), - Value index = const Value.absent(), - }) => - PlaylistTableCompanion( - id: id, - audioPlayerStateId: audioPlayerStateId, - index: index, - ), - createCompanionCallback: ({ - Value id = const Value.absent(), - required int audioPlayerStateId, - required int index, - }) => - PlaylistTableCompanion.insert( - id: id, - audioPlayerStateId: audioPlayerStateId, - index: index, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$PlaylistTableTableReferences(db, table, e) - )) - .toList(), - prefetchHooksCallback: ( - {audioPlayerStateId = false, playlistMediaTableRefs = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [ - if (playlistMediaTableRefs) db.playlistMediaTable - ], - addJoins: < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic>>(state) { - if (audioPlayerStateId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.audioPlayerStateId, - referencedTable: $$PlaylistTableTableReferences - ._audioPlayerStateIdTable(db), - referencedColumn: $$PlaylistTableTableReferences - ._audioPlayerStateIdTable(db) - .id, - ) as T; - } - - return state; - }, - getPrefetchedDataCallback: (items) async { - return [ - if (playlistMediaTableRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $$PlaylistTableTableReferences - ._playlistMediaTableRefsTable(db), - managerFromTypedResult: (p0) => - $$PlaylistTableTableReferences(db, table, p0) - .playlistMediaTableRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.playlistId == item.id), - typedResults: items) - ]; - }, - ); - }, - )); -} - -typedef $$PlaylistTableTableProcessedTableManager = ProcessedTableManager< - _$AppDatabase, - $PlaylistTableTable, - PlaylistTableData, - $$PlaylistTableTableFilterComposer, - $$PlaylistTableTableOrderingComposer, - $$PlaylistTableTableAnnotationComposer, - $$PlaylistTableTableCreateCompanionBuilder, - $$PlaylistTableTableUpdateCompanionBuilder, - (PlaylistTableData, $$PlaylistTableTableReferences), - PlaylistTableData, - PrefetchHooks Function( - {bool audioPlayerStateId, bool playlistMediaTableRefs})>; -typedef $$PlaylistMediaTableTableCreateCompanionBuilder - = PlaylistMediaTableCompanion Function({ - Value id, - required int playlistId, - required String uri, - Value?> extras, - Value?> httpHeaders, -}); -typedef $$PlaylistMediaTableTableUpdateCompanionBuilder - = PlaylistMediaTableCompanion Function({ - Value id, - Value playlistId, - Value uri, - Value?> extras, - Value?> httpHeaders, -}); - -final class $$PlaylistMediaTableTableReferences extends BaseReferences< - _$AppDatabase, $PlaylistMediaTableTable, PlaylistMediaTableData> { - $$PlaylistMediaTableTableReferences( - super.$_db, super.$_table, super.$_typedResult); - - static $PlaylistTableTable _playlistIdTable(_$AppDatabase db) => - db.playlistTable.createAlias($_aliasNameGenerator( - db.playlistMediaTable.playlistId, db.playlistTable.id)); - - $$PlaylistTableTableProcessedTableManager get playlistId { - final manager = $$PlaylistTableTableTableManager($_db, $_db.playlistTable) - .filter((f) => f.id($_item.playlistId!)); - final item = $_typedResult.readTableOrNull(_playlistIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } -} - -class $$PlaylistMediaTableTableFilterComposer - extends Composer<_$AppDatabase, $PlaylistMediaTableTable> { - $$PlaylistMediaTableTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get uri => $composableBuilder( - column: $table.uri, builder: (column) => ColumnFilters(column)); - - ColumnWithTypeConverterFilters?, Map, - String> - get extras => $composableBuilder( - column: $table.extras, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnWithTypeConverterFilters?, Map, - String> - get httpHeaders => $composableBuilder( - column: $table.httpHeaders, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - $$PlaylistTableTableFilterComposer get playlistId { - final $$PlaylistTableTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.playlistId, - referencedTable: $db.playlistTable, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistTableTableFilterComposer( - $db: $db, - $table: $db.playlistTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $$PlaylistMediaTableTableOrderingComposer - extends Composer<_$AppDatabase, $PlaylistMediaTableTable> { - $$PlaylistMediaTableTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get uri => $composableBuilder( - column: $table.uri, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get extras => $composableBuilder( - column: $table.extras, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get httpHeaders => $composableBuilder( - column: $table.httpHeaders, builder: (column) => ColumnOrderings(column)); - - $$PlaylistTableTableOrderingComposer get playlistId { - final $$PlaylistTableTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.playlistId, - referencedTable: $db.playlistTable, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistTableTableOrderingComposer( - $db: $db, - $table: $db.playlistTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $$PlaylistMediaTableTableAnnotationComposer - extends Composer<_$AppDatabase, $PlaylistMediaTableTable> { - $$PlaylistMediaTableTableAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get uri => - $composableBuilder(column: $table.uri, builder: (column) => column); - - GeneratedColumnWithTypeConverter?, String> get extras => - $composableBuilder(column: $table.extras, builder: (column) => column); - - GeneratedColumnWithTypeConverter?, String> - get httpHeaders => $composableBuilder( - column: $table.httpHeaders, builder: (column) => column); - - $$PlaylistTableTableAnnotationComposer get playlistId { - final $$PlaylistTableTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.playlistId, - referencedTable: $db.playlistTable, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PlaylistTableTableAnnotationComposer( - $db: $db, - $table: $db.playlistTable, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $$PlaylistMediaTableTableTableManager extends RootTableManager< - _$AppDatabase, - $PlaylistMediaTableTable, - PlaylistMediaTableData, - $$PlaylistMediaTableTableFilterComposer, - $$PlaylistMediaTableTableOrderingComposer, - $$PlaylistMediaTableTableAnnotationComposer, - $$PlaylistMediaTableTableCreateCompanionBuilder, - $$PlaylistMediaTableTableUpdateCompanionBuilder, - (PlaylistMediaTableData, $$PlaylistMediaTableTableReferences), - PlaylistMediaTableData, - PrefetchHooks Function({bool playlistId})> { - $$PlaylistMediaTableTableTableManager( - _$AppDatabase db, $PlaylistMediaTableTable table) - : super(TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $$PlaylistMediaTableTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$PlaylistMediaTableTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$PlaylistMediaTableTableAnnotationComposer( - $db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value playlistId = const Value.absent(), - Value uri = const Value.absent(), - Value?> extras = const Value.absent(), - Value?> httpHeaders = const Value.absent(), - }) => - PlaylistMediaTableCompanion( - id: id, - playlistId: playlistId, - uri: uri, - extras: extras, - httpHeaders: httpHeaders, - ), - createCompanionCallback: ({ - Value id = const Value.absent(), - required int playlistId, - required String uri, - Value?> extras = const Value.absent(), - Value?> httpHeaders = const Value.absent(), - }) => - PlaylistMediaTableCompanion.insert( - id: id, - playlistId: playlistId, - uri: uri, - extras: extras, - httpHeaders: httpHeaders, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$PlaylistMediaTableTableReferences(db, table, e) - )) - .toList(), - prefetchHooksCallback: ({playlistId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic>>(state) { - if (playlistId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.playlistId, - referencedTable: $$PlaylistMediaTableTableReferences - ._playlistIdTable(db), - referencedColumn: $$PlaylistMediaTableTableReferences - ._playlistIdTable(db) - .id, - ) as T; - } - - return state; - }, - getPrefetchedDataCallback: (items) async { - return []; - }, - ); - }, - )); -} - -typedef $$PlaylistMediaTableTableProcessedTableManager = ProcessedTableManager< - _$AppDatabase, - $PlaylistMediaTableTable, - PlaylistMediaTableData, - $$PlaylistMediaTableTableFilterComposer, - $$PlaylistMediaTableTableOrderingComposer, - $$PlaylistMediaTableTableAnnotationComposer, - $$PlaylistMediaTableTableCreateCompanionBuilder, - $$PlaylistMediaTableTableUpdateCompanionBuilder, - (PlaylistMediaTableData, $$PlaylistMediaTableTableReferences), - PlaylistMediaTableData, - PrefetchHooks Function({bool playlistId})>; + PrefetchHooks Function()>; typedef $$HistoryTableTableCreateCompanionBuilder = HistoryTableCompanion Function({ Value id, @@ -7646,10 +6524,6 @@ class $AppDatabaseManager { $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); $$AudioPlayerStateTableTableTableManager get audioPlayerStateTable => $$AudioPlayerStateTableTableTableManager(_db, _db.audioPlayerStateTable); - $$PlaylistTableTableTableManager get playlistTable => - $$PlaylistTableTableTableManager(_db, _db.playlistTable); - $$PlaylistMediaTableTableTableManager get playlistMediaTable => - $$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable); $$HistoryTableTableTableManager get historyTable => $$HistoryTableTableTableManager(_db, _db.historyTable); $$LyricsTableTableTableManager get lyricsTable => diff --git a/lib/models/database/tables/audio_player_state.dart b/lib/models/database/tables/audio_player_state.dart index 3e49cf6f..7cb62709 100644 --- a/lib/models/database/tables/audio_player_state.dart +++ b/lib/models/database/tables/audio_player_state.dart @@ -6,22 +6,30 @@ class AudioPlayerStateTable extends Table { TextColumn get loopMode => textEnum()(); BoolColumn get shuffled => boolean()(); TextColumn get collections => text().map(const StringListConverter())(); + TextColumn get tracks => + text().map(const SpotubeTrackObjectListConverter())(); + IntColumn get currentIndex => integer()(); } -class PlaylistTable extends Table { - IntColumn get id => integer().autoIncrement()(); - IntColumn get audioPlayerStateId => - integer().references(AudioPlayerStateTable, #id)(); - IntColumn get index => integer()(); -} +class SpotubeTrackObjectListConverter + extends TypeConverter, String> { + const SpotubeTrackObjectListConverter(); -class PlaylistMediaTable extends Table { - IntColumn get id => integer().autoIncrement()(); - IntColumn get playlistId => integer().references(PlaylistTable, #id)(); + @override + List fromSql(String fromDb) { + return fromDb + .split(",") + .where((e) => e.isNotEmpty) + .map( + (e) => SpotubeTrackObject.fromJson( + json.decode(e) as Map, + ), + ) + .toList(); + } - TextColumn get uri => text()(); - TextColumn get extras => - text().nullable().map(const MapTypeConverter())(); - TextColumn get httpHeaders => - text().nullable().map(const MapTypeConverter())(); + @override + String toSql(List value) { + return value.map((e) => json.encode(e)).join(","); + } } diff --git a/lib/models/database/typeconverters/map_list.dart b/lib/models/database/typeconverters/map_list.dart new file mode 100644 index 00000000..b92e781d --- /dev/null +++ b/lib/models/database/typeconverters/map_list.dart @@ -0,0 +1,20 @@ +part of '../database.dart'; + +class MapListConverter + extends TypeConverter>, String> { + const MapListConverter(); + + @override + List> fromSql(String fromDb) { + return fromDb + .split(",") + .where((e) => e.isNotEmpty) + .map((e) => json.decode(e) as Map) + .toList(); + } + + @override + String toSql(List> value) { + return value.map((e) => json.encode(e)).join(","); + } +} diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index c3ce0436..d7aebdb3 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -1,8 +1,13 @@ library metadata_objects; +import 'dart:typed_data'; + import 'package:collection/collection.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/services/audio_player/audio_player.dart'; import 'package:spotube/utils/primitive_utils.dart'; part 'metadata.g.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index d1fa4f45..45ce4e84 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -2822,6 +2822,8 @@ abstract class _SpotubeSearchResponseObject SpotubeTrackObject _$SpotubeTrackObjectFromJson(Map json) { switch (json['runtimeType']) { + case 'local': + return SpotubeLocalTrackObject.fromJson(json); case 'full': return SpotubeFullTrackObject.fromJson(json); case 'simple': @@ -2842,9 +2844,17 @@ mixin _$SpotubeTrackObject { throw _privateConstructorUsedError; SpotubeSimpleAlbumObject? get album => throw _privateConstructorUsedError; int get durationMs => throw _privateConstructorUsedError; - bool get explicit => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ + required TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path) + local, required TResult Function( String id, String name, @@ -2868,6 +2878,15 @@ mixin _$SpotubeTrackObject { throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ + TResult? Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, TResult? Function( String id, String name, @@ -2891,6 +2910,15 @@ mixin _$SpotubeTrackObject { throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ + TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, TResult Function( String id, String name, @@ -2915,18 +2943,21 @@ mixin _$SpotubeTrackObject { throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ + required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeFullTrackObject value) full, required TResult Function(SpotubeSimpleTrackObject value) simple, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeFullTrackObject value)? full, TResult? Function(SpotubeSimpleTrackObject value)? simple, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ + TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeFullTrackObject value)? full, TResult Function(SpotubeSimpleTrackObject value)? simple, required TResult orElse(), @@ -2955,8 +2986,7 @@ abstract class $SpotubeTrackObjectCopyWith<$Res> { String externalUri, List artists, SpotubeSimpleAlbumObject album, - int durationMs, - bool explicit}); + int durationMs}); $SpotubeSimpleAlbumObjectCopyWith<$Res>? get album; } @@ -2982,7 +3012,6 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject> Object? artists = null, Object? album = null, Object? durationMs = null, - Object? explicit = null, }) { return _then(_value.copyWith( id: null == id @@ -3009,10 +3038,6 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject> ? _value.durationMs : durationMs // ignore: cast_nullable_to_non_nullable as int, - explicit: null == explicit - ? _value.explicit - : explicit // ignore: cast_nullable_to_non_nullable - as bool, ) 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 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, + 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 artists = const [], + required this.album, + required this.durationMs, + required this.path, + final String? $type}) + : _artists = artists, + $type = $type ?? 'local'; + + factory _$SpotubeLocalTrackObjectImpl.fromJson(Map json) => + _$$SpotubeLocalTrackObjectImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String externalUri; + final List _artists; + @override + @JsonKey() + List 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({ + required TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path) + local, + required TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String isrc, + bool explicit) + full, + required TResult Function( + String id, + String name, + String externalUri, + int durationMs, + bool explicit, + List artists, + SpotubeSimpleAlbumObject? album) + simple, + }) { + return local(id, name, externalUri, artists, album, durationMs, path); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, + TResult? Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String isrc, + bool explicit)? + full, + TResult? Function( + String id, + String name, + String externalUri, + int durationMs, + bool explicit, + List artists, + SpotubeSimpleAlbumObject? album)? + simple, + }) { + return local?.call(id, name, externalUri, artists, album, durationMs, path); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, + TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String isrc, + bool explicit)? + full, + TResult Function( + String id, + String name, + String externalUri, + int durationMs, + bool explicit, + List 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({ + 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? Function(SpotubeLocalTrackObject value)? local, + TResult? Function(SpotubeFullTrackObject value)? full, + TResult? Function(SpotubeSimpleTrackObject value)? simple, + }) { + return local?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + 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 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 artists, + required final SpotubeSimpleAlbumObject album, + required final int durationMs, + required final String path}) = _$SpotubeLocalTrackObjectImpl; + + factory SpotubeLocalTrackObject.fromJson(Map json) = + _$SpotubeLocalTrackObjectImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get externalUri; + @override + List 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 abstract class _$$SpotubeFullTrackObjectImplCopyWith<$Res> implements $SpotubeTrackObjectCopyWith<$Res> { @@ -3218,6 +3595,15 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { @override @optionalTypeArgs TResult when({ + required TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path) + local, required TResult Function( String id, String name, @@ -3245,6 +3631,15 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { @override @optionalTypeArgs TResult? whenOrNull({ + TResult? Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, TResult? Function( String id, String name, @@ -3272,6 +3667,15 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { @override @optionalTypeArgs TResult maybeWhen({ + TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, TResult Function( String id, String name, @@ -3303,6 +3707,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { @override @optionalTypeArgs TResult map({ + required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeFullTrackObject value) full, required TResult Function(SpotubeSimpleTrackObject value) simple, }) { @@ -3312,6 +3717,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { @override @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeFullTrackObject value)? full, TResult? Function(SpotubeSimpleTrackObject value)? simple, }) { @@ -3321,6 +3727,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { @override @optionalTypeArgs TResult maybeMap({ + TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeFullTrackObject value)? full, TResult Function(SpotubeSimpleTrackObject value)? simple, required TResult orElse(), @@ -3366,7 +3773,6 @@ abstract class SpotubeFullTrackObject implements SpotubeTrackObject { @override int get durationMs; String get isrc; - @override bool get explicit; /// Create a copy of SpotubeTrackObject @@ -3544,6 +3950,15 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject { @override @optionalTypeArgs TResult when({ + required TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path) + local, required TResult Function( String id, String name, @@ -3570,6 +3985,15 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject { @override @optionalTypeArgs TResult? whenOrNull({ + TResult? Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, TResult? Function( String id, String name, @@ -3597,6 +4021,15 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject { @override @optionalTypeArgs TResult maybeWhen({ + TResult Function( + String id, + String name, + String externalUri, + List artists, + SpotubeSimpleAlbumObject album, + int durationMs, + String path)? + local, TResult Function( String id, String name, @@ -3628,6 +4061,7 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject { @override @optionalTypeArgs TResult map({ + required TResult Function(SpotubeLocalTrackObject value) local, required TResult Function(SpotubeFullTrackObject value) full, required TResult Function(SpotubeSimpleTrackObject value) simple, }) { @@ -3637,6 +4071,7 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject { @override @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(SpotubeLocalTrackObject value)? local, TResult? Function(SpotubeFullTrackObject value)? full, TResult? Function(SpotubeSimpleTrackObject value)? simple, }) { @@ -3646,6 +4081,7 @@ class _$SpotubeSimpleTrackObjectImpl implements SpotubeSimpleTrackObject { @override @optionalTypeArgs TResult maybeMap({ + TResult Function(SpotubeLocalTrackObject value)? local, TResult Function(SpotubeFullTrackObject value)? full, TResult Function(SpotubeSimpleTrackObject value)? simple, required TResult orElse(), @@ -3685,7 +4121,6 @@ abstract class SpotubeSimpleTrackObject implements SpotubeTrackObject { String get externalUri; @override int get durationMs; - @override bool get explicit; @override List get artists; diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 887df6d2..3303e324 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -287,6 +287,37 @@ Map _$$SpotubeSearchResponseObjectImplToJson( '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?) + ?.map((e) => SpotubeSimpleArtistObject.fromJson( + Map.from(e as Map))) + .toList() ?? + const [], + album: SpotubeSimpleAlbumObject.fromJson( + Map.from(json['album'] as Map)), + durationMs: (json['durationMs'] as num).toInt(), + path: json['path'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$$SpotubeLocalTrackObjectImplToJson( + _$SpotubeLocalTrackObjectImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'externalUri': instance.externalUri, + 'artists': instance.artists.map((e) => e.toJson()).toList(), + 'album': instance.album.toJson(), + 'durationMs': instance.durationMs, + 'path': instance.path, + 'runtimeType': instance.$type, + }; + _$SpotubeFullTrackObjectImpl _$$SpotubeFullTrackObjectImplFromJson(Map json) => _$SpotubeFullTrackObjectImpl( id: json['id'] as String, diff --git a/lib/models/metadata/track.dart b/lib/models/metadata/track.dart index eb82f2d8..b7cf1a3e 100644 --- a/lib/models/metadata/track.dart +++ b/lib/models/metadata/track.dart @@ -2,6 +2,16 @@ part of 'metadata.dart'; @freezed class SpotubeTrackObject with _$SpotubeTrackObject { + factory SpotubeTrackObject.local({ + required String id, + required String name, + required String externalUri, + @Default([]) List artists, + required SpotubeSimpleAlbumObject album, + required int durationMs, + required String path, + }) = SpotubeLocalTrackObject; + factory SpotubeTrackObject.full({ required String id, required String name, @@ -27,6 +37,40 @@ class SpotubeTrackObject with _$SpotubeTrackObject { _$SpotubeTrackObjectFromJson( json.containsKey("isrc") ? {...json, "runtimeType": "full"} - : {...json, "runtimeType": "simple"}, + : json.containsKey("path") + ? {...json, "runtimeType": "local"} + : {...json, "runtimeType": "simple"}, ); } + +extension AsMediaListSpotubeTrackObject on Iterable { + List 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, + ); + } +} diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart new file mode 100644 index 00000000..d894ff8b --- /dev/null +++ b/lib/models/playback/track_sources.dart @@ -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 artists, + required String album, + required int durationMs, + required String isrc, + required bool explicit, + }) = _TrackSourceQuery; + + factory TrackSourceQuery.fromJson(Map 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 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 json) => + _$TrackSourceFromJson(json); +} + +@JsonSerializable() +abstract class BasicSourcedTrack { + final TrackSourceQuery query; + final AudioSource source; + final TrackSourceInfo info; + final List sources; + @JsonKey(defaultValue: []) + final List siblings; + BasicSourcedTrack({ + required this.query, + required this.source, + required this.info, + required this.sources, + this.siblings = const [], + }); + + factory BasicSourcedTrack.fromJson(Map json) => + _$BasicSourcedTrackFromJson(json); + Map toJson() => _$BasicSourcedTrackToJson(this); +} diff --git a/lib/models/playback/track_sources.freezed.dart b/lib/models/playback/track_sources.freezed.dart new file mode 100644 index 00000000..1f78d2b3 --- /dev/null +++ b/lib/models/playback/track_sources.freezed.dart @@ -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 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 json) { + return _TrackSourceQuery.fromJson(json); +} + +/// @nodoc +mixin _$TrackSourceQuery { + String get id => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + List 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 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 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 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, + 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 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, + 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 artists, + required this.album, + required this.durationMs, + required this.isrc, + required this.explicit}) + : _artists = artists; + + factory _$TrackSourceQueryImpl.fromJson(Map json) => + _$$TrackSourceQueryImplFromJson(json); + + @override + final String id; + @override + final String title; + final List _artists; + @override + List 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 toJson() { + return _$$TrackSourceQueryImplToJson( + this, + ); + } +} + +abstract class _TrackSourceQuery implements TrackSourceQuery { + factory _TrackSourceQuery( + {required final String id, + required final String title, + required final List artists, + required final String album, + required final int durationMs, + required final String isrc, + required final bool explicit}) = _$TrackSourceQueryImpl; + + factory _TrackSourceQuery.fromJson(Map json) = + _$TrackSourceQueryImpl.fromJson; + + @override + String get id; + @override + String get title; + @override + List 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 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 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 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 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 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 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 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 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 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 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 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 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; +} diff --git a/lib/models/playback/track_sources.g.dart b/lib/models/playback/track_sources.g.dart new file mode 100644 index 00000000..e1a62ea6 --- /dev/null +++ b/lib/models/playback/track_sources.g.dart @@ -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.from(json['query'] as Map)), + source: $enumDecode(_$AudioSourceEnumMap, json['source']), + info: TrackSourceInfo.fromJson( + Map.from(json['info'] as Map)), + sources: (json['sources'] as List) + .map((e) => TrackSource.fromJson(Map.from(e as Map))) + .toList(), + siblings: (json['siblings'] as List?) + ?.map((e) => + TrackSourceInfo.fromJson(Map.from(e as Map))) + .toList() ?? + [], + ); + +Map _$BasicSourcedTrackToJson(BasicSourcedTrack instance) => + { + '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).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 _$$TrackSourceQueryImplToJson( + _$TrackSourceQueryImpl instance) => + { + '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 _$$TrackSourceInfoImplToJson( + _$TrackSourceInfoImpl instance) => + { + '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 _$$TrackSourceImplToJson(_$TrackSourceImpl instance) => + { + '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', +}; diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index b02910e9..28cd5835 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -24,7 +24,7 @@ import 'package:spotube/models/local_track.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/provider/authentication/authentication.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/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/platform.dart'; @@ -44,7 +44,7 @@ class PlayerView extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final auth = ref.watch(authenticationProvider); - final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider); + final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider); final currentActiveTrack = ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index d026cea9..472acf1b 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -19,7 +19,7 @@ import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player.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/youtube_engine/youtube_engine.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; @@ -75,9 +75,9 @@ class SiblingTracksSheet extends HookConsumerWidget { final isSearching = useState(false); final searchMode = useState(preferences.searchMode); - final activeTrackNotifier = ref.watch(activeSourcedTrackProvider.notifier); + final activeTrackNotifier = ref.watch(activeTrackSourcesProvider.notifier); final activeTrack = - ref.watch(activeSourcedTrackProvider) ?? playlist.activeTrack; + ref.watch(activeTrackSourcesProvider) ?? playlist.activeTrack; final title = ServiceUtils.getTitle( activeTrack?.name ?? "", diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index aa93bd4f..1432e5e3 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -3,22 +3,38 @@ import 'dart:math'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/track.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/blacklist_provider.dart'; import 'package:spotube/provider/database/database.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/logger/logger.dart'; class AudioPlayerNotifier extends Notifier { BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier); + void _assertAllowedTracks(Iterable 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 _syncSavedState() async { final database = ref.read(databaseProvider); @@ -32,6 +48,8 @@ class AudioPlayerNotifier extends Notifier { loopMode: audioPlayer.loopMode, shuffled: audioPlayer.isShuffled, collections: [], + tracks: [], + currentIndex: 0, id: const Value(0), ), ); @@ -43,51 +61,20 @@ class AudioPlayerNotifier extends Notifier { await audioPlayer.setShuffle(playerState.shuffled); } - var playlist = - await database.select(database.playlistTable).getSingleOrNull(); - final medias = await database.select(database.playlistMediaTable).get(); + final tracks = playerState.tracks; + final currentIndex = playerState.currentIndex; - if (playlist == null) { - await database.into(database.playlistTable).insert( - PlaylistTableCompanion.insert( - audioPlayerStateId: 0, - index: audioPlayer.playlist.index, - id: const Value(0), - ), - ); - - 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) { + if (tracks.isEmpty && state.tracks.isNotEmpty) { + await _updatePlayerState( + AudioPlayerStateTableCompanion( + tracks: Value(state.tracks), + currentIndex: Value(currentIndex), + ), + ); + } else if (tracks.isNotEmpty) { await audioPlayer.openPlaylist( - medias - .map( - (media) => SpotubeMedia.fromMedia( - Media( - media.uri, - extras: media.extras, - httpHeaders: media.httpHeaders, - ), - ), - ) - .toList(), - initialIndex: playlist.index, + tracks.asMediaList(), + initialIndex: currentIndex, autoPlay: false, ); } @@ -109,36 +96,6 @@ class AudioPlayerNotifier extends Notifier { .write(companion); } - Future _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 build() { final subscriptions = [ @@ -183,9 +140,25 @@ class AudioPlayerNotifier extends Notifier { }), audioPlayer.playlistStream.listen((playlist) async { 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) { AppLogger.reportError(e, stack); } @@ -203,8 +176,8 @@ class AudioPlayerNotifier extends Notifier { return AudioPlayerState( loopMode: audioPlayer.loopMode, playing: audioPlayer.isPlaying, - playlist: audioPlayer.playlist, shuffled: audioPlayer.isShuffled, + tracks: [], collections: [], ); } @@ -245,17 +218,16 @@ class AudioPlayerNotifier extends Notifier { await removeCollections([collectionId]); } - // Tracks related methods - Future addTracksAtFirst( - Iterable tracks, { + Iterable tracks, { bool allowDuplicates = false, }) async { + _assertAllowedTracks(tracks); if (state.tracks.length == 1) { return addTracks(tracks); } - tracks = _blacklist.filter(tracks).toList() as List; + tracks = _blacklist.filter(tracks).toList(); for (int i = 0; i < tracks.length; i++) { final track = tracks.elementAt(i); @@ -267,19 +239,23 @@ class AudioPlayerNotifier extends Notifier { await audioPlayer.addTrackAt( SpotubeMedia(track), - max(state.playlist.index, 0) + i + 1, + max(state.currentIndex, 0) + i + 1, ); } } - Future addTrack(Track track) async { + Future addTrack(SpotubeTrackObject track) async { + _assertAllowedTrack(track); + if (_blacklist.contains(track)) return; if (state.tracks.any((element) => _compareTracks(element, track))) return; await audioPlayer.addTrack(SpotubeMedia(track)); } - Future addTracks(Iterable tracks) async { - tracks = _blacklist.filter(tracks).toList() as List; + Future addTracks(Iterable tracks) async { + _assertAllowedTracks(tracks); + + tracks = _blacklist.filter(tracks).toList(); for (final track in tracks) { await audioPlayer.addTrack(SpotubeMedia(track)); } @@ -299,31 +275,40 @@ class AudioPlayerNotifier extends Notifier { } } - bool _compareTracks(Track a, Track b) { - if ((a is LocalTrack && b is! LocalTrack) || - (a is! LocalTrack && b is LocalTrack)) { + bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) { + if ((a is SpotubeLocalTrackObject && b is! SpotubeLocalTrackObject) || + (a is! SpotubeLocalTrackObject && b is SpotubeLocalTrackObject)) { return false; } - return a is LocalTrack && b is LocalTrack + return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject ? (a).path == (b).path : a.id == b.id; } Future load( - List tracks, { + List tracks, { int initialIndex = 0, bool autoPlay = false, }) async { - final medias = (_blacklist.filter(tracks).toList() as List) + _assertAllowedTracks(tracks); + + final medias = _blacklist + .filter(tracks) + .toList() .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 // because of timeout final intendedActiveTrack = medias.elementAt(initialIndex); - if (intendedActiveTrack.track is! LocalTrack) { - await ref.read(sourcedTrackProvider(intendedActiveTrack).future); + if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { + await ref.read( + trackSourcesProvider( + TrackSourceQuery.fromTrack( + intendedActiveTrack.track as SpotubeFullTrackObject), + ).future, + ); } if (medias.isEmpty) return; @@ -337,7 +322,7 @@ class AudioPlayerNotifier extends Notifier { ); } - Future jumpToTrack(Track track) async { + Future jumpToTrack(SpotubeTrackObject track) async { final index = state.tracks.toList().indexWhere((element) => element.id == track.id); if (index == -1) return; diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index c221a2b0..baf7b624 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -2,16 +2,17 @@ import 'dart:async'; import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.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/state.dart'; import 'package:spotube/provider/discord_provider.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/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/services/audio_player/audio_player.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 /// so we need to fetch them from the API - final activeTrack = - Track.fromJson(audioPlayerState.activeTrack!.toJson()); - if (audioPlayerState.activeTrack!.artists - ?.any((a) => a.images == null) ?? - false) { - activeTrack.artists = - await ref.read(spotifyProvider).api.artists.list([ - for (final artist in audioPlayerState.activeTrack!.artists!) - artist.id!, - ]).then((value) => value.toList()); + var activeTrack = audioPlayerState.activeTrack!; + if (activeTrack.artists.any((a) => a.images == null)) { + final metadataPlugin = await ref.read(metadataPluginProvider.future); + final artists = await Future.wait( + activeTrack.artists + .map((artist) => metadataPlugin!.artist.getArtist(artist.id)), + ); + activeTrack = activeTrack.copyWith( + artists: artists + .map((e) => SpotubeSimpleArtistObject.fromJson(e.toJson())) + .toList(), + ); } await history.addTrack(activeTrack); @@ -127,24 +130,26 @@ class AudioPlayerStreamListeners { (event.inSeconds / max(audioPlayer.duration.inSeconds, 1)) * 100; try { if (percentProgress < 80 || - audioPlayerState.playlist.index == -1 || - audioPlayerState.playlist.index == + audioPlayerState.currentIndex == -1 || + audioPlayerState.currentIndex == audioPlayerState.tracks.length - 1) { return; } - final nextTrack = SpotubeMedia.fromMedia( - audioPlayerState.playlist.medias - .elementAt(audioPlayerState.playlist.index + 1), - ); + final nextTrack = audioPlayerState.tracks + .elementAt(audioPlayerState.currentIndex + 1); - if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { + if (lastTrack == nextTrack.id || nextTrack is SpotubeLocalTrackObject) { return; } try { - await ref.read(sourcedTrackProvider(nextTrack).future); + await ref.read( + trackSourcesProvider( + TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject), + ).future, + ); } finally { - lastTrack = nextTrack.track.id!; + lastTrack = nextTrack.id; } } catch (e, stack) { AppLogger.reportError(e, stack); diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index 55590d48..d7e271ae 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -1,24 +1,20 @@ 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/sourced_track.dart'; -import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/provider/server/track_sources.dart'; final queryingTrackInfoProvider = Provider((ref) { - final media = audioPlayer.playlist.index == -1 || - audioPlayer.playlist.medias.isEmpty - ? null - : audioPlayer.playlist.medias.elementAtOrNull(audioPlayer.playlist.index); - final audioPlayerActiveTrack = - media == null ? null : SpotubeMedia.fromMedia(media); + final audioPlayer = ref.watch(audioPlayerProvider); - final activeMedia = ref.watch(audioPlayerProvider.select( - (s) => s.activeMedia == null - ? null - : SpotubeMedia.fromMedia(s.activeMedia!), - )) ?? - audioPlayerActiveTrack; + if (audioPlayer.activeTrack == null) { + return false; + } - if (activeMedia == null) return false; - - return ref.watch(sourcedTrackProvider(activeMedia)).isLoading; + return ref + .watch(trackSourcesProvider( + TrackSourceQuery.fromTrack( + audioPlayer.activeTrack! as SpotubeFullTrackObject), + )) + .isLoading; }); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 0e3004f5..bb0527bf 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -1,104 +1,60 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:media_kit/media_kit.dart' hide Track; -import 'package:spotify/spotify.dart' hide Playlist; -import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/models/metadata/metadata.dart'; -class AudioPlayerState { - final bool playing; - final PlaylistMode loopMode; - final bool shuffled; - final Playlist playlist; +part 'state.freezed.dart'; +part 'state.g.dart'; - final List tracks; - final List collections; +@freezed +class AudioPlayerState with _$AudioPlayerState { + const AudioPlayerState._(); - AudioPlayerState({ - required this.playing, - required this.loopMode, - required this.shuffled, - required this.playlist, - required this.collections, - List? tracks, - }) : tracks = tracks ?? - playlist.medias - .map((media) => SpotubeMedia.fromMedia(media).track) - .toList(); + factory AudioPlayerState._inner({ + required bool playing, + required PlaylistMode loopMode, + required bool shuffled, + required List collections, + @Default(0) int currentIndex, + @Default([]) List tracks, + }) = _AudioPlayerState; - factory AudioPlayerState.fromJson(Map json) { - return AudioPlayerState( - playing: json['playing'], - loopMode: PlaylistMode.values.firstWhere( - (e) => e.name == json['loopMode'], - orElse: () => audioPlayer.loopMode, - ), - shuffled: json['shuffled'], - playlist: Playlist( - json['playlist']['medias'] - .map( - (media) => SpotubeMedia.fromMedia(Media( - media['uri'], - extras: media['extras'], - httpHeaders: media['httpHeaders'], - )), - ) - .cast() - .toList(), - index: json['playlist']['index'], - ), - collections: List.from(json['collections']), - ); - } - - Map 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? collections, + factory AudioPlayerState({ + required bool playing, + required PlaylistMode loopMode, + required bool shuffled, + required List collections, + int currentIndex = 0, + List tracks = const [], }) { - return AudioPlayerState( - playing: playing ?? this.playing, - loopMode: loopMode ?? this.loopMode, - shuffled: shuffled ?? this.shuffled, - playlist: playlist ?? this.playlist, - collections: collections ?? this.collections, - tracks: playlist == null ? tracks : null, + assert( + tracks.every((track) => + track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject), + 'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject', + ); + + return AudioPlayerState._inner( + playing: playing, + loopMode: loopMode, + shuffled: shuffled, + currentIndex: currentIndex, + tracks: tracks, + collections: collections, ); } - Track? get activeTrack { - if (playlist.index == -1) return null; - return tracks.elementAtOrNull(playlist.index); + factory AudioPlayerState.fromJson(Map json) => + _$AudioPlayerStateFromJson(json); + + SpotubeTrackObject? get activeTrack { + if (currentIndex < 0 || currentIndex >= tracks.length) return null; + return tracks[currentIndex]; } - Media? get activeMedia { - if (playlist.index == -1 || playlist.medias.isEmpty) return null; - return playlist.medias.elementAt(playlist.index); - } - - bool containsTrack(Track track) { + bool containsTrack(SpotubeTrackObject track) { return tracks.any((t) => t.id == track.id); } - bool containsTracks(List tracks) { + bool containsTracks(List tracks) { return tracks.every(containsTrack); } diff --git a/lib/provider/audio_player/state.freezed.dart b/lib/provider/audio_player/state.freezed.dart new file mode 100644 index 00000000..0299cd2f --- /dev/null +++ b/lib/provider/audio_player/state.freezed.dart @@ -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 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 json) { + return _AudioPlayerState.fromJson(json); +} + +/// @nodoc +mixin _$AudioPlayerState { + bool get playing => throw _privateConstructorUsedError; + PlaylistMode get loopMode => throw _privateConstructorUsedError; + bool get shuffled => throw _privateConstructorUsedError; + List get collections => throw _privateConstructorUsedError; + int get currentIndex => throw _privateConstructorUsedError; + List get tracks => throw _privateConstructorUsedError; + + /// Serializes this AudioPlayerState to a JSON map. + Map 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 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 collections, + int currentIndex, + List 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, + 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, + ) 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 collections, + int currentIndex, + List 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, + 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, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AudioPlayerStateImpl extends _AudioPlayerState { + _$AudioPlayerStateImpl( + {required this.playing, + required this.loopMode, + required this.shuffled, + required final List collections, + this.currentIndex = 0, + final List tracks = const []}) + : _collections = collections, + _tracks = tracks, + super._(); + + factory _$AudioPlayerStateImpl.fromJson(Map json) => + _$$AudioPlayerStateImplFromJson(json); + + @override + final bool playing; + @override + final PlaylistMode loopMode; + @override + final bool shuffled; + final List _collections; + @override + List get collections { + if (_collections is EqualUnmodifiableListView) return _collections; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_collections); + } + + @override + @JsonKey() + final int currentIndex; + final List _tracks; + @override + @JsonKey() + List 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 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 collections, + final int currentIndex, + final List tracks}) = _$AudioPlayerStateImpl; + _AudioPlayerState._() : super._(); + + factory _AudioPlayerState.fromJson(Map json) = + _$AudioPlayerStateImpl.fromJson; + + @override + bool get playing; + @override + PlaylistMode get loopMode; + @override + bool get shuffled; + @override + List get collections; + @override + int get currentIndex; + @override + List 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; +} diff --git a/lib/provider/audio_player/state.g.dart b/lib/provider/audio_player/state.g.dart new file mode 100644 index 00000000..de5f6f1c --- /dev/null +++ b/lib/provider/audio_player/state.g.dart @@ -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) + .map((e) => e as String) + .toList(), + currentIndex: (json['currentIndex'] as num?)?.toInt() ?? 0, + tracks: (json['tracks'] as List?) + ?.map((e) => SpotubeTrackObject.fromJson( + Map.from(e as Map))) + .toList() ?? + const [], + ); + +Map _$$AudioPlayerStateImplToJson( + _$AudioPlayerStateImpl instance) => + { + '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', +}; diff --git a/lib/provider/blacklist_provider.dart b/lib/provider/blacklist_provider.dart index a51d399f..ff7ec8fb 100644 --- a/lib/provider/blacklist_provider.dart +++ b/lib/provider/blacklist_provider.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/current_playlist.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; class BlackListNotifier extends AsyncNotifier> { @@ -34,17 +34,15 @@ class BlackListNotifier extends AsyncNotifier> { .go(); } - bool contains(TrackSimple track) { + bool contains(SpotubeTrackObject track) { final containsTrack = state.asData?.value.any((element) => element.elementId == track.id) ?? false; - final containsTrackArtists = track.artists?.any( - (artist) => - state.asData?.value.any((el) => el.elementId == artist.id) ?? - false, - ) ?? - false; + final containsTrackArtists = track.artists.any( + (artist) => + state.asData?.value.any((el) => el.elementId == artist.id) ?? false, + ); return containsTrack || containsTrackArtists; } @@ -56,18 +54,9 @@ class BlackListNotifier extends AsyncNotifier> { } /// Filters the non blacklisted tracks from the given [tracks] - Iterable filter(Iterable tracks) { + Iterable filter(Iterable tracks) { 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 = diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index 93d2fb88..268b6567 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -5,11 +5,11 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/spotube_icons.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/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.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/provider/connect/clients.dart'; @@ -41,7 +41,8 @@ final queueProvider = StateProvider( playing: audioPlayer.isPlaying, loopMode: audioPlayer.loopMode, shuffled: audioPlayer.isShuffled, - playlist: audioPlayer.playlist, + tracks: [], + currentIndex: 0, collections: [], ), ); @@ -207,7 +208,7 @@ class ConnectNotifier extends AsyncNotifier { emit(WebSocketLoopEvent(value)); } - Future addTrack(Track data) async { + Future addTrack(SpotubeFullTrackObject data) async { emit(WebSocketAddTrackEvent(data)); } diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index 8f81fc51..ee068f10 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -2,8 +2,7 @@ import 'dart:async'; import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -74,20 +73,20 @@ class DiscordNotifier extends AsyncNotifier { } } - Future updatePresence(Track track) async { + Future updatePresence(SpotubeTrackObject track) async { if (!kIsDesktop) return; if (FlutterDiscordRPC.instance.isConnected == false) return; - final artistNames = track.artists?.asString(); + final artistNames = track.artists.asString(); final isPlaying = audioPlayer.isPlaying; final position = audioPlayer.position; await FlutterDiscordRPC.instance.setActivity( activity: RPCActivity( details: track.name, - state: artistNames != null ? "by $artistNames" : null, + state: artistNames, assets: RPCAssets( largeImage: - track.album?.images?.first.url ?? "spotube-logo-foreground", + track.album?.images.first.url ?? "spotube-logo-foreground", largeText: track.album?.name ?? "Unknown album", smallImage: "spotube-logo-foreground", smallText: "Spotube", @@ -95,8 +94,7 @@ class DiscordNotifier extends AsyncNotifier { buttons: [ RPCButton( label: "Listen on Spotify", - url: track.externalUrls?.spotify ?? - "https://open.spotify.com/tracks/${track.id}", + url: track.externalUri, ), ], timestamps: RPCTimestamps( diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 1b588399..a7e2b768 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -1,16 +1,15 @@ import 'dart:async'; 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:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:metadata_god/metadata_god.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/services/download_manager/download_manager.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -21,18 +20,22 @@ import 'package:spotube/utils/service_utils.dart'; class DownloadManagerProvider extends ChangeNotifier { DownloadManagerProvider({required this.ref}) : $history = {}, - $backHistory = {}, + $backHistory = {}, dl = DownloadManager() { dl.statusStream.listen((event) async { try { final (:request, :status) = event; - final track = $history.firstWhereOrNull( + final sourcedTrack = $history.firstWhereOrNull( (element) => element.getUrlOfCodec(downloadCodec) == request.url, ); + if (sourcedTrack == null) return; + final track = $backHistory.firstWhereOrNull( + (element) => element.id == sourcedTrack.query.id, + ); if (track == null) return; - final savePath = getTrackFileUrl(track); + final savePath = getTrackFileUrl(sourcedTrack); // related to onFileExists final oldFile = File("$savePath.old"); @@ -57,7 +60,7 @@ class DownloadManagerProvider extends ChangeNotifier { } final imageBytes = await ServiceUtils.downloadImage( - (track.album?.images).asUrlString( + (track.album.images).asUrlString( placeholder: ImagePlaceholder.albumArt, index: 1, ), @@ -78,7 +81,8 @@ class DownloadManagerProvider extends ChangeNotifier { }); } - Future Function(Track track) onFileExists = (Track track) async => true; + Future Function(SpotubeFullTrackObject track) onFileExists = + (SpotubeFullTrackObject track) async => true; final Ref ref; @@ -99,21 +103,19 @@ class DownloadManagerProvider extends ChangeNotifier { final Set $history; // these are the tracks which metadata hasn't been fetched yet - final Set $backHistory; + final Set $backHistory; final DownloadManager dl; - String getTrackFileUrl(Track track) { + String getTrackFileUrl(SourcedTrack track) { final name = - "${track.name} - ${track.artists?.asString() ?? ""}.${downloadCodec.name}"; + "${track.query.title} - ${track.query.artists.join(", ")}.${downloadCodec.name}"; return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); } - bool isActive(Track track) { + Future isActive(SpotubeFullTrackObject track) async { if ($backHistory.contains(track)) return true; - final sourcedTrack = mapToSourcedTrack(track); - - if (sourcedTrack == null) return false; + final sourcedTrack = await mapToSourcedTrack(track); return dl .getAllDownloads() @@ -128,8 +130,12 @@ class DownloadManagerProvider extends ChangeNotifier { } /// For singular downloads - Future addToQueue(Track track) async { - final savePath = getTrackFileUrl(track); + Future addToQueue(SpotubeFullTrackObject track) async { + final sourcedTrack = await ref.read( + trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, + ); + + final savePath = getTrackFileUrl(sourcedTrack); final oldFile = File(savePath); if (await oldFile.exists() && !await onFileExists(track)) { @@ -140,18 +146,21 @@ class DownloadManagerProvider extends ChangeNotifier { await oldFile.rename("$savePath.old"); } - if (track is SourcedTrack && track.codec == downloadCodec) { - final downloadTask = - await dl.addDownload(track.getUrlOfCodec(downloadCodec), savePath); + if (sourcedTrack.codec == downloadCodec) { + final downloadTask = await dl.addDownload( + sourcedTrack.getUrlOfCodec(downloadCodec), savePath); if (downloadTask != null) { - $history.add(track); + $history.add(sourcedTrack); } } else { $backHistory.add(track); - final sourcedTrack = await SourcedTrack.fetchFromTrack( - ref: ref, - track: track, - ).then((d) { + final sourcedTrack = await ref + .read( + trackSourcesProvider( + TrackSourceQuery.fromTrack(track), + ).future, + ) + .then((d) { $backHistory.remove(track); return d; }); @@ -167,10 +176,8 @@ class DownloadManagerProvider extends ChangeNotifier { notifyListeners(); } - Future batchAddToQueue(List tracks) async { - $backHistory.addAll( - tracks.where((element) => element is! SourcedTrack), - ); + Future batchAddToQueue(List tracks) async { + $backHistory.addAll(tracks); notifyListeners(); for (final track in tracks) { try { @@ -194,20 +201,23 @@ class DownloadManagerProvider extends ChangeNotifier { $history.remove(track); } - Future pause(SourcedTrack track) { - return dl.pauseDownload(track.getUrlOfCodec(downloadCodec)); + Future pause(SpotubeFullTrackObject track) async { + final sourcedTrack = await mapToSourcedTrack(track); + return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); } - Future resume(SourcedTrack track) { - return dl.resumeDownload(track.getUrlOfCodec(downloadCodec)); + Future resume(SpotubeFullTrackObject track) async { + final sourcedTrack = await mapToSourcedTrack(track); + return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); } - Future retry(SourcedTrack track) { + Future retry(SpotubeFullTrackObject track) { return addToQueue(track); } - void cancel(SourcedTrack track) { - dl.cancelDownload(track.getUrlOfCodec(downloadCodec)); + void cancel(SpotubeFullTrackObject track) async { + final sourcedTrack = await mapToSourcedTrack(track); + return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)); } void cancelAll() { @@ -217,12 +227,19 @@ class DownloadManagerProvider extends ChangeNotifier { } } - SourcedTrack? mapToSourcedTrack(Track track) { - if (track is SourcedTrack) { - return track; - } else { - return $history.firstWhereOrNull((element) => element.id == track.id); + Future mapToSourcedTrack(SpotubeFullTrackObject track) async { + final historicTrack = + $history.firstWhereOrNull((element) => element.query.id == track.id); + + if (historicTrack != null) { + return historicTrack; } + + final sourcedTrack = await ref.read( + trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, + ); + + return sourcedTrack; } ValueNotifier? getStatusNotifier(SourcedTrack track) { diff --git a/lib/provider/glance/glance.dart b/lib/provider/glance/glance.dart index 22faa13f..8afeda11 100644 --- a/lib/provider/glance/glance.dart +++ b/lib/provider/glance/glance.dart @@ -5,7 +5,7 @@ import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.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/server/server.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -71,7 +71,7 @@ Future _updateWidget() async { } } -Future _sendActiveTrack(Track? track) async { +Future _sendActiveTrack(SpotubeTrackObject? track) async { if (track == null) { await _saveWidgetData("activeTrack", null); await _updateWidget(); @@ -80,8 +80,8 @@ Future _sendActiveTrack(Track? track) async { final jsonTrack = track.toJson(); - final image = track.album?.images?.first; - final cachedImage = await DefaultCacheManager().getSingleFile(image!.url!); + final image = track.album?.images.first; + final cachedImage = await DefaultCacheManager().getSingleFile(image!.url); final data = { ...jsonTrack, "album": { diff --git a/lib/provider/history/history.dart b/lib/provider/history/history.dart index 25b722ff..b83e5db1 100644 --- a/lib/provider/history/history.dart +++ b/lib/provider/history/history.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; class PlaybackHistoryActions { @@ -16,31 +16,31 @@ class PlaybackHistoryActions { }); } - Future addPlaylists(List playlists) async { + Future addPlaylists(List playlists) async { await _batchInsertHistoryEntries([ for (final playlist in playlists) HistoryTableCompanion.insert( type: HistoryEntryType.playlist, - itemId: playlist.id!, + itemId: playlist.id, data: playlist.toJson(), ), ]); } - Future addAlbums(List albums) async { + Future addAlbums(List albums) async { await _batchInsertHistoryEntries([ for (final albums in albums) HistoryTableCompanion.insert( type: HistoryEntryType.album, - itemId: albums.id!, + itemId: albums.id, data: albums.toJson(), ), ]); } - Future addTracks(List tracks) async { + Future addTracks(List tracks) async { 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', ); @@ -48,22 +48,22 @@ class PlaybackHistoryActions { for (final track in tracks) HistoryTableCompanion.insert( type: HistoryEntryType.track, - itemId: track.id!, + itemId: track.id, data: track.toJson(), ), ]); } - Future addTrack(Track track) async { + Future addTrack(SpotubeTrackObject track) async { assert( - track.artists?.every((a) => a.images != null) ?? false, + track.artists.every((a) => a.images != null), 'Track artists must have images', ); await _db.into(_db.historyTable).insert( HistoryTableCompanion.insert( type: HistoryEntryType.track, - itemId: track.id!, + itemId: track.id, data: track.toJson(), ), ); diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart index 8aff0438..f65dc759 100644 --- a/lib/provider/scrobbler/scrobbler.dart +++ b/lib/provider/scrobbler/scrobbler.dart @@ -3,16 +3,15 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; -import 'package:spotify/spotify.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/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; class ScrobblerNotifier extends AsyncNotifier { - final StreamController _scrobbleController = - StreamController.broadcast(); + final StreamController _scrobbleController = + StreamController.broadcast(); @override build() async { final database = ref.watch(databaseProvider); @@ -47,13 +46,12 @@ class ScrobblerNotifier extends AsyncNotifier { _scrobbleController.stream.listen((track) async { try { await state.asData?.value?.track.scrobble( - artist: track.artists!.first.name!, - track: track.name!, - album: track.album!.name!, + artist: track.artists.first.name, + track: track.name, + album: track.album!.name, chosenByUser: true, - duration: track.duration, + duration: Duration(milliseconds: track.durationMs), timestamp: DateTime.now().toUtc(), - trackNumber: track.trackNumber, ); } catch (e, stackTrace) { AppLogger.reportError(e, stackTrace); @@ -109,21 +107,21 @@ class ScrobblerNotifier extends AsyncNotifier { await database.delete(database.scrobblerTable).go(); } - void scrobble(Track track) { + void scrobble(SpotubeTrackObject track) { _scrobbleController.add(track); } - Future love(Track track) async { + Future love(SpotubeTrackObject track) async { await state.asData?.value?.track.love( - artist: track.artists!.asString(), - track: track.name!, + artist: track.artists.asString(), + track: track.name, ); } - Future unlove(Track track) async { + Future unlove(SpotubeTrackObject track) async { await state.asData?.value?.track.unLove( - artist: track.artists!.asString(), - track: track.name!, + artist: track.artists.asString(), + track: track.name, ); } } diff --git a/lib/provider/server/active_sourced_track.dart b/lib/provider/server/active_sourced_track.dart deleted file mode 100644 index 37d0dec8..00000000 --- a/lib/provider/server/active_sourced_track.dart +++ /dev/null @@ -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 { - @override - build() { - return null; - } - - void update(SourcedTrack? sourcedTrack) { - state = sourcedTrack; - } - - Future populateSibling() async { - if (state == null) return; - state = await state!.copyWithSibling(); - } - - Future 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(), -); diff --git a/lib/provider/server/active_track_sources.dart b/lib/provider/server/active_track_sources.dart new file mode 100644 index 00000000..5b64dc26 --- /dev/null +++ b/lib/provider/server/active_track_sources.dart @@ -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, + ); +}); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart index 6c4d8ce0..ccfbf5ba 100644 --- a/lib/provider/server/routes/connect.dart +++ b/lib/provider/server/routes/connect.dart @@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_web_socket/shelf_web_socket.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/extensions/context.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/audio_player/audio_player.dart'; @@ -161,19 +161,19 @@ class ServerConnectRoutes { event.onLoad((event) async { await audioPlayerNotifier.load( - event.data.tracks, + event.data.tracks as List, autoPlay: true, initialIndex: event.data.initialIndex ?? 0, ); if (event.data.collectionId == null) return; audioPlayerNotifier.addCollection(event.data.collectionId!); - if (event.data.collection is AlbumSimple) { - historyNotifier - .addAlbums([event.data.collection as AlbumSimple]); + if (event.data.collection is SpotubeSimpleAlbumObject) { + historyNotifier.addAlbums( + [event.data.collection as SpotubeSimpleAlbumObject]); } else { historyNotifier.addPlaylists( - [event.data.collection as PlaylistSimple]); + [event.data.collection as SpotubeSimplePlaylistObject]); } }); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index ccbeb7ab..eeedc47b 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:dio/dio.dart' hide Response; import 'package:dio/dio.dart' as dio_lib; 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:path/path.dart'; import 'package:shelf/shelf.dart'; -import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/extensions/image.dart'; -import 'package:spotube/extensions/track.dart'; +import 'package:spotube/models/metadata/metadata.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/state.dart'; -import 'package:spotube/provider/server/active_sourced_track.dart'; -import 'package:spotube/provider/server/sourced_track.dart'; +import 'package:spotube/provider/server/active_track_sources.dart'; +import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -55,7 +55,7 @@ class ServerPlaybackRoutes { join( await UserPreferencesNotifier.getMusicCacheDir(), 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 { AppLogger.reportError(e, stack); final sourcedTrack = await ref - .read(sourcedTrackProvider(SpotubeMedia(track)).notifier) + .read(trackSourcesProvider(track.query).notifier) .refreshStreamingUrl(); - if (playlist.activeTrack?.id == sourcedTrack?.id && - sourcedTrack != null) { - ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); - } + // It gets updated by itself. + // if (playlist.activeTrack?.id == sourcedTrack.query.id) { + // ref.read(activeTrackSourcesProvider.notifier).update(sourcedTrack); + // } return await dio.get( - sourcedTrack!.url, + sourcedTrack.url, options: options.copyWith(headers: { ...?options.headers, "user-agent": _randomUserAgent, @@ -174,8 +174,18 @@ class ServerPlaybackRoutes { } 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( - (track.album?.images).asUrlString( + (playlistTrack.album?.images).asUrlString( placeholder: ImagePlaceholder.albumArt, index: 1, ), @@ -183,9 +193,9 @@ class ServerPlaybackRoutes { await MetadataGod.writeMetadata( file: trackCacheFile.path, - metadata: track.toMetadata( - fileLength: fileLength, + metadata: (playlistTrack as SpotubeFullTrackObject).toMetadata( imageBytes: imageBytes, + fileLength: fileLength, ), ); } @@ -199,15 +209,21 @@ class ServerPlaybackRoutes { final track = playlist.tracks.firstWhere((element) => element.id == trackId); - final activeSourcedTrack = ref.read(activeSourcedTrackProvider); - final sourcedTrack = activeSourcedTrack?.id == track.id - ? activeSourcedTrack - : await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future); + final activeSourcedTrack = + await ref.read(activeTrackSourcesProvider.future); + final sourcedTrack = activeSourcedTrack?.track.id == track.id + ? activeSourcedTrack?.source + : await ref.read( + trackSourcesProvider( + TrackSourceQuery.parseUri(request.url.toString()), + ).future, + ); - if (playlist.activeTrack?.id == sourcedTrack?.id && - sourcedTrack != null) { - ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); - } + // This will be automatically updated by the notifier. + // if (playlist.activeTrack?.id == sourcedTrack?.query.id && + // sourcedTrack != null) { + // ref.read(activeTrackSourcesProvider.notifier).update(sourcedTrack); + // } final (bytes: audioBytes, response: res) = await streamTrack(sourcedTrack!, request.headers); diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart deleted file mode 100644 index f733f9d6..00000000 --- a/lib/provider/server/sourced_track.dart +++ /dev/null @@ -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 { - @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 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(), -); diff --git a/lib/provider/server/track_sources.dart b/lib/provider/server/track_sources.dart new file mode 100644 index 00000000..2112c2af --- /dev/null +++ b/lib/provider/server/track_sources.dart @@ -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 { + @override + FutureOr 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 refreshStreamingUrl() async { + return await update((prev) async { + return await prev.refreshStream(); + }); + } + + Future copyWithSibling( + TrackSourceInfo info, + TrackSourceQuery query, + ) async { + return await update((prev) async { + return prev.copyWithSibling(); + }); + } + + Future swapWithSibling(TrackSourceInfo sibling) async { + return await update((prev) async { + return await prev.swapWithSibling(sibling) ?? prev; + }); + } +} + +final trackSourcesProvider = AsyncNotifierProviderFamily( + () => TrackSourcesNotifier(), +); diff --git a/lib/provider/skip_segments/skip_segments.dart b/lib/provider/skip_segments/skip_segments.dart index 005797f4..accccddd 100644 --- a/lib/provider/skip_segments/skip_segments.dart +++ b/lib/provider/skip_segments/skip_segments.dart @@ -1,9 +1,10 @@ import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.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/services/dio/dio.dart'; @@ -81,8 +82,11 @@ Future> getAndCacheSkipSegments( final segmentProvider = FutureProvider( (ref) async { - final track = ref.watch(activeSourcedTrackProvider); - if (track == null) return null; + final snapshot = await ref.watch(activeTrackSourcesProvider.future); + 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( userPreferencesProvider.select( @@ -96,16 +100,13 @@ final segmentProvider = FutureProvider( ); if (!skipNonMusic) { - return SourcedSegments( - segments: [], - source: track.sourceInfo.id, - ); + return SourcedSegments(segments: [], source: source.info.id); } - final segments = await getAndCacheSkipSegments(track.sourceInfo.id, ref); + final segments = await getAndCacheSkipSegments(source.info.id, ref); return SourcedSegments( - source: track.sourceInfo.id, + source: source.info.id, segments: segments, ); }, diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 4febecdf..ead81967 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,9 +1,10 @@ import 'dart:io'; 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:flutter/foundation.dart'; -import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/local_track.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; import 'dart:async'; @@ -11,33 +12,41 @@ import 'dart:async'; import 'package:media_kit/media_kit.dart' as mk; import 'package:spotube/services/audio_player/playback_state.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/platform.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; class SpotubeMedia extends mk.Media { - final Track track; - 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( this.track, { Map? extras, super.httpHeaders, - }) : super( - track is LocalTrack + }) : assert( + 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 - : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", - extras: { - ...?extras, - "track": switch (track) { - LocalTrack() => track.toJson(), - SourcedTrack() => track.toJson(), - _ => track.toJson(), - }, - }, + : "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}", ); @override @@ -46,23 +55,11 @@ class SpotubeMedia extends mk.Media { /// [super.uri] must be used instead of [track.path] to prevent wrong /// path format exceptions in Windows causing [extras] to be null LocalTrack() => super.uri, - _ => - "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:" - "$serverPort/stream/${track.id}", + _ => "http://$_host:" + "$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 // operator ==(Object other) { // if (other is! SpotubeMedia) return false; diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index 060a7f41..b3653555 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -1,10 +1,8 @@ import 'package:audio_service/audio_service.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/env.dart'; -import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/extensions/image.dart'; +import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; @@ -49,16 +47,14 @@ class AudioServices with WidgetsBindingObserver { return AudioServices(mobile, smtc); } - Future addTrack(Track track) async { + Future addTrack(SpotubeTrackObject track) async { await smtc?.addTrack(track); mobile?.addItem(MediaItem( - id: track.id!, + id: track.id, album: track.album?.name ?? "", - title: track.name!, - artist: (track.artists)?.asString() ?? "", - duration: track is SourcedTrack - ? track.sourceInfo.duration - : Duration(milliseconds: track.durationMs ?? 0), + title: track.name, + artist: track.artists.asString(), + duration: Duration(milliseconds: track.durationMs), artUri: Uri.parse( (track.album?.images).asUrlString( placeholder: ImagePlaceholder.albumArt, diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index 8edc5069..6cf101ab 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:smtc_windows/smtc_windows.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/extensions/image.dart'; +import 'package:spotube/models/metadata/metadata.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/playback_state.dart'; @@ -77,15 +75,15 @@ class WindowsAudioService { ]); } - Future addTrack(Track track) async { + Future addTrack(SpotubeTrackObject track) async { if (!smtc.enabled) { await smtc.enableSmtc(); } await smtc.updateMetadata( MusicMetadata( - title: track.name!, - albumArtist: track.artists?.firstOrNull?.name ?? "Unknown", - artist: track.artists?.asString() ?? "Unknown", + title: track.name, + albumArtist: track.artists.firstOrNull?.name ?? "Unknown", + artist: track.artists.asString(), album: track.album?.name ?? "Unknown", thumbnail: (track.album?.images).asUrlString( placeholder: ImagePlaceholder.albumArt, diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart index e47ee6bd..9ef6c080 100644 --- a/lib/services/sourced_track/enums.dart +++ b/lib/services/sourced_track/enums.dart @@ -1,5 +1,4 @@ -import 'package:spotube/services/sourced_track/models/source_info.dart'; -import 'package:spotube/services/sourced_track/models/source_map.dart'; +import 'package:spotube/models/playback/track_sources.dart'; enum SourceCodecs { m4a._("M4a (Best for downloaded music)"), @@ -15,4 +14,7 @@ enum SourceQualities { low, } -typedef SiblingType = ({T info, SourceMap? source}); +typedef SiblingType = ({ + T info, + List? source +}); diff --git a/lib/services/sourced_track/exceptions.dart b/lib/services/sourced_track/exceptions.dart index 85bc5b27..c841e1e2 100644 --- a/lib/services/sourced_track/exceptions.dart +++ b/lib/services/sourced_track/exceptions.dart @@ -1,12 +1,12 @@ -import 'package:spotify/spotify.dart'; +import 'package:spotube/models/playback/track_sources.dart'; class TrackNotFoundError extends Error { - final Track track; + final TrackSourceQuery track; TrackNotFoundError(this.track); @override String toString() { - return '[TrackNotFoundError] ${track.name} - ${track.artists?.map((e) => e.name).join(", ")}'; + return '[TrackNotFoundError] ${track.title} - ${track.artists.join(", ")}'; } } diff --git a/lib/services/sourced_track/models/source_info.dart b/lib/services/sourced_track/models/source_info.dart deleted file mode 100644 index 4ba90355..00000000 --- a/lib/services/sourced_track/models/source_info.dart +++ /dev/null @@ -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 json) => - _$SourceInfoFromJson(json); - - Map toJson() => _$SourceInfoToJson(this); -} diff --git a/lib/services/sourced_track/models/source_info.g.dart b/lib/services/sourced_track/models/source_info.g.dart deleted file mode 100644 index 54671f63..00000000 --- a/lib/services/sourced_track/models/source_info.g.dart +++ /dev/null @@ -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 _$SourceInfoToJson(SourceInfo instance) => - { - '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, - }; diff --git a/lib/services/sourced_track/models/source_map.dart b/lib/services/sourced_track/models/source_map.dart deleted file mode 100644 index f99f95e4..00000000 --- a/lib/services/sourced_track/models/source_map.dart +++ /dev/null @@ -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 json) => - _$SourceQualityMapFromJson(json); - - Map 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 json) => - _$SourceMapFromJson(json); - - Map toJson() => _$SourceMapToJson(this); - - operator [](SourceCodecs key) { - switch (key) { - case SourceCodecs.weba: - return weba; - case SourceCodecs.m4a: - return m4a; - } - } -} diff --git a/lib/services/sourced_track/models/source_map.g.dart b/lib/services/sourced_track/models/source_map.g.dart deleted file mode 100644 index a581cc67..00000000 --- a/lib/services/sourced_track/models/source_map.g.dart +++ /dev/null @@ -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 _$SourceQualityMapToJson(SourceQualityMap instance) => - { - 'high': instance.high, - 'medium': instance.medium, - 'low': instance.low, - }; - -SourceMap _$SourceMapFromJson(Map json) => SourceMap( - weba: json['weba'] == null - ? null - : SourceQualityMap.fromJson( - Map.from(json['weba'] as Map)), - m4a: json['m4a'] == null - ? null - : SourceQualityMap.fromJson( - Map.from(json['m4a'] as Map)), - ); - -Map _$SourceMapToJson(SourceMap instance) => { - 'weba': instance.weba?.toJson(), - 'm4a': instance.m4a?.toJson(), - }; diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index bf0b22e6..f8caa38d 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -1,47 +1,27 @@ +import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.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/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/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; -abstract class SourcedTrack extends Track { - final SourceMap source; - final List siblings; - final SourceInfo sourceInfo; +abstract class SourcedTrack extends BasicSourcedTrack { final Ref ref; SourcedTrack({ required this.ref, - required this.source, - required this.siblings, - required this.sourceInfo, - required Track track, - }) { - 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; - } + required super.info, + required super.query, + required super.source, + required super.siblings, + required super.sources, + }); static SourcedTrack fromJson( Map json, { @@ -49,110 +29,116 @@ abstract class SourcedTrack extends Track { }) { final preferences = ref.read(userPreferencesProvider); - final sourceInfo = SourceInfo.fromJson(json); - final source = SourceMap.fromJson(json); - final track = Track.fromJson(json); + final info = TrackSourceInfo.fromJson(json["info"]); + final query = TrackSourceQuery.fromJson(json["query"]); + final source = AudioSource.values.firstWhereOrNull( + (source) => source.name == json["source"], + ) ?? + preferences.audioSource; final siblings = (json["siblings"] as List) - .map((sibling) => SourceInfo.fromJson(sibling)) - .toList() - .cast(); + .map((s) => TrackSourceInfo.fromJson(s)) + .toList(); + final sources = + (json["sources"] as List).map((s) => TrackSource.fromJson(s)).toList(); return switch (preferences.audioSource) { AudioSource.youtube => YoutubeSourcedTrack( ref: ref, source: source, siblings: siblings, - sourceInfo: sourceInfo, - track: track, + info: info, + query: query, + sources: sources, ), AudioSource.piped => PipedSourcedTrack( ref: ref, source: source, siblings: siblings, - sourceInfo: sourceInfo, - track: track, + info: info, + query: query, + sources: sources, ), AudioSource.jiosaavn => JioSaavnSourcedTrack( ref: ref, source: source, siblings: siblings, - sourceInfo: sourceInfo, - track: track, + info: info, + query: query, + sources: sources, ), AudioSource.invidious => InvidiousSourcedTrack( ref: ref, source: source, siblings: siblings, - sourceInfo: sourceInfo, - track: track, + info: info, + query: query, + sources: sources, ), }; } - static String getSearchTerm(Track track) { - final artists = - (track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList(); - + static String getSearchTerm(TrackSourceQuery track) { final title = ServiceUtils.getTitle( - track.name!, - artists: artists, + track.title, + artists: track.artists, onlyCleanArtist: true, ).trim(); - return "$title - ${artists.join(", ")}"; + return "$title - ${track.artists.join(", ")}"; } - static Future fetchFromTrack({ - required Track track, + static Future fetchFromQuery({ + required TrackSourceQuery query, required Ref ref, }) async { final preferences = ref.read(userPreferencesProvider); try { return switch (preferences.audioSource) { AudioSource.youtube => - await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref), + await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref), AudioSource.piped => - await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref), + await PipedSourcedTrack.fetchFromTrack(query: query, ref: ref), AudioSource.invidious => - await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref), + await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref), AudioSource.jiosaavn => - await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), + await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref), }; } catch (e) { if (preferences.audioSource == AudioSource.youtube) { rethrow; } - return await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref); + return await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref); } } static Future> fetchSiblings({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) { final preferences = ref.read(userPreferencesProvider); return switch (preferences.audioSource) { AudioSource.piped => - PipedSourcedTrack.fetchSiblings(track: track, ref: ref), + PipedSourcedTrack.fetchSiblings(query: query, ref: ref), AudioSource.youtube => - YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref), + YoutubeSourcedTrack.fetchSiblings(query: query, ref: ref), AudioSource.jiosaavn => - JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref), + JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref), AudioSource.invidious => - InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref), + InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref), }; } Future copyWithSibling(); - Future swapWithSibling(SourceInfo sibling); + Future swapWithSibling(TrackSourceInfo sibling); Future swapWithSiblingOfIndex(int index) { return swapWithSibling(siblings[index]); } + Future refreshStream(); String get url { final preferences = ref.read(userPreferencesProvider); @@ -166,10 +152,22 @@ abstract class SourcedTrack extends Track { String getUrlOfCodec(SourceCodecs codec) { final preferences = ref.read(userPreferencesProvider); - return source[codec]?[preferences.audioQuality] ?? - // this will ensure playback doesn't break - source[codec == SourceCodecs.m4a ? SourceCodecs.weba : SourceCodecs.m4a] - [preferences.audioQuality]; + return sources + .firstWhereOrNull( + (source) => + 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 { @@ -179,4 +177,12 @@ abstract class SourcedTrack extends Track { ? SourceCodecs.m4a : preferences.streamMusicCodec; } + + TrackSource get activeTrackSource { + final audioQuality = ref.read(userPreferencesProvider).audioQuality; + return sources.firstWhereOrNull( + (source) => source.codec == codec && source.quality == audioQuality, + ) ?? + sources.first; + } } diff --git a/lib/services/sourced_track/sources/invidious.dart b/lib/services/sourced_track/sources/invidious.dart index 4a32ad41..82e001f5 100644 --- a/lib/services/sourced_track/sources/invidious.dart +++ b/lib/services/sourced_track/sources/invidious.dart @@ -1,14 +1,12 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.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/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/sourced_track/enums.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/sourced_track.dart'; import 'package:invidious/invidious.dart'; @@ -24,51 +22,24 @@ final invidiousProvider = Provider( }, ); -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 { InvidiousSourcedTrack({ required super.ref, required super.source, required super.siblings, - required super.sourceInfo, - required super.track, + required super.info, + required super.query, + required super.sources, }); static Future fetchFromTrack({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) async { - // Indicates a stream url refresh - 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 audioSource = ref.read(userPreferencesProvider).audioSource; final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!)) + ..where((s) => s.trackId.equals(query.id)) ..limit(1) ..orderBy([ (s) => @@ -78,14 +49,14 @@ class InvidiousSourcedTrack extends SourcedTrack { final invidiousClient = ref.read(invidiousProvider); if (cachedSource == null) { - final siblings = await fetchSiblings(ref: ref, track: track); + final siblings = await fetchSiblings(ref: ref, query: query); if (siblings.isEmpty) { - throw TrackNotFoundError(track); + throw TrackNotFoundError(query); } await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: track.id!, + trackId: query.id, sourceId: siblings.first.info.id, sourceType: const Value(SourceType.youtube), ), @@ -94,9 +65,10 @@ class InvidiousSourcedTrack extends SourcedTrack { return InvidiousSourcedTrack( ref: ref, siblings: siblings.map((s) => s.info).skip(1).toList(), - source: siblings.first.source as SourceMap, - sourceInfo: siblings.first.info, - track: track, + sources: siblings.first.source as List, + info: siblings.first.info, + query: query, + source: audioSource, ); } else { final manifest = @@ -105,44 +77,36 @@ class InvidiousSourcedTrack extends SourcedTrack { return InvidiousSourcedTrack( ref: ref, siblings: [], - source: toSourceMap(manifest), - sourceInfo: InvidiousSourceInfo( + sources: toSources(manifest), + info: TrackSourceInfo( id: manifest.videoId, - artist: manifest.author, - artistUrl: manifest.authorUrl, + artists: manifest.author, pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}", thumbnail: manifest.videoThumbnails.first.url, title: manifest.title, - duration: Duration(seconds: manifest.lengthSeconds), - album: null, + durationMs: Duration(seconds: manifest.lengthSeconds).inMilliseconds, ), - track: track, + query: query, + source: audioSource, ); } } - static SourceMap toSourceMap(InvidiousVideoResponse manifest) { - final m4a = manifest.adaptiveFormats - .where((audio) => audio.type.contains("audio/mp4")) - .sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate))); - - final weba = manifest.adaptiveFormats - .where((audio) => audio.type.contains("audio/webm")) - .sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate))); - - return SourceMap( - m4a: SourceQualityMap( - high: m4a.first.url.toString(), - medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(), - low: m4a.last.url.toString(), - ), - weba: SourceQualityMap( - high: weba.first.url.toString(), - medium: - (weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(), - low: weba.last.url.toString(), - ), - ); + static List toSources(InvidiousVideoResponse manifest) { + return manifest.adaptiveFormats.map((stream) { + return TrackSource( + url: stream.url.toString(), + quality: switch (stream.qualityLabel) { + "high" => SourceQualities.high, + "medium" => SourceQualities.medium, + _ => SourceQualities.low, + }, + codec: stream.type.contains("audio/webm") + ? SourceCodecs.weba + : SourceCodecs.m4a, + bitrate: stream.bitrate, + ); + }).toList(); } static Future toSiblingType( @@ -150,22 +114,20 @@ class InvidiousSourcedTrack extends SourcedTrack { YoutubeVideoInfo item, InvidiousClient invidiousClient, ) async { - SourceMap? sourceMap; + List? sourceMap; if (index == 0) { final manifest = await invidiousClient.videos.get(item.id, local: true); - sourceMap = toSourceMap(manifest); + sourceMap = toSources(manifest); } final SiblingType sibling = ( - info: InvidiousSourceInfo( + info: TrackSourceInfo( id: item.id, - artist: item.channelName, - artistUrl: "https://www.youtube.com/${item.channelId}", + artists: item.channelName, pageUrl: "https://www.youtube.com/watch?v=${item.id}", thumbnail: item.thumbnailUrl, title: item.title, - duration: item.duration, - album: null, + durationMs: item.duration.inMilliseconds, ), source: sourceMap, ); @@ -174,20 +136,20 @@ class InvidiousSourcedTrack extends SourcedTrack { } static Future> fetchSiblings({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) async { final invidiousClient = ref.read(invidiousProvider); final preference = ref.read(userPreferencesProvider); - final query = SourcedTrack.getSearchTerm(track); + final searchQuery = SourcedTrack.getSearchTerm(query); final searchResults = await invidiousClient.search.list( - query, + searchQuery, type: InvidiousSearchType.video, ); - if (ServiceUtils.onlyContainsEnglish(query)) { + if (ServiceUtils.onlyContainsEnglish(searchQuery)) { return await Future.wait( searchResults .whereType() @@ -211,7 +173,7 @@ class InvidiousSourcedTrack extends SourcedTrack { ), ) .toList(), - track, + query, ); return await Future.wait( @@ -224,23 +186,24 @@ class InvidiousSourcedTrack extends SourcedTrack { if (siblings.isNotEmpty) { return this; } - final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); return InvidiousSourcedTrack( ref: ref, siblings: fetchedSiblings - .where((s) => s.info.id != sourceInfo.id) + .where((s) => s.info.id != info.id) .map((s) => s.info) .toList(), source: source, - sourceInfo: sourceInfo, - track: this, + info: info, + query: query, + sources: sources, ); } @override - Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id) { + Future swapWithSibling(TrackSourceInfo sibling) async { + if (sibling.id == info.id) { return null; } @@ -251,7 +214,7 @@ class InvidiousSourcedTrack extends SourcedTrack { ? sibling : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, sourceInfo); + ..insert(0, info); final pipedClient = ref.read(invidiousProvider); @@ -261,7 +224,7 @@ class InvidiousSourcedTrack extends SourcedTrack { final database = ref.read(databaseProvider); await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: id!, + trackId: query.id, sourceId: newSourceInfo.id, sourceType: const Value(SourceType.youtube), // Because we're sorting by createdAt in the query @@ -274,9 +237,25 @@ class InvidiousSourcedTrack extends SourcedTrack { return InvidiousSourcedTrack( ref: ref, siblings: newSiblings, - source: toSourceMap(manifest), - sourceInfo: newSourceInfo, - track: this, + sources: toSources(manifest), + info: newSourceInfo, + query: query, + source: source, + ); + } + + @override + Future 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, ); } } diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index 1434e4f7..4b67e717 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -1,49 +1,35 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.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/services/sourced_track/enums.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:jiosaavn/jiosaavn.dart'; import 'package:spotube/extensions/string.dart'; 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 { JioSaavnSourcedTrack({ required super.ref, required super.source, required super.siblings, - required super.sourceInfo, - required super.track, + required super.info, + required super.query, + required super.sources, }); static Future fetchFromTrack({ - required Track track, + required TrackSourceQuery query, required Ref ref, bool weakMatch = false, }) async { final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!)) + ..where((s) => s.trackId.equals(query.id)) ..limit(1) ..orderBy([ (s) => @@ -54,15 +40,15 @@ class JioSaavnSourcedTrack extends SourcedTrack { if (cachedSource == null || cachedSource.sourceType != SourceType.jiosaavn) { final siblings = - await fetchSiblings(ref: ref, track: track, weakMatch: weakMatch); + await fetchSiblings(ref: ref, query: query, weakMatch: weakMatch); if (siblings.isEmpty) { - throw TrackNotFoundError(track); + throw TrackNotFoundError(query); } await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: track.id!, + trackId: query.id, sourceId: siblings.first.info.id, sourceType: const Value(SourceType.jiosaavn), ), @@ -71,9 +57,10 @@ class JioSaavnSourcedTrack extends SourcedTrack { return JioSaavnSourcedTrack( ref: ref, siblings: siblings.map((s) => s.info).skip(1).toList(), - source: siblings.first.source!, - sourceInfo: siblings.first.info, - track: track, + sources: siblings.first.source!, + info: siblings.first.info, + query: query, + source: AudioSource.jiosaavn, ); } @@ -85,80 +72,77 @@ class JioSaavnSourcedTrack extends SourcedTrack { return JioSaavnSourcedTrack( ref: ref, siblings: [], - source: source!, - sourceInfo: info, - track: track, + sources: source!, + query: query, + info: info, + source: AudioSource.jiosaavn, ); } static SiblingType toSiblingType(SongResponse result) { final SiblingType sibling = ( - info: JioSaavnSourceInfo( - artist: [ + info: TrackSourceInfo( + artists: [ result.primaryArtists, if (result.featuredArtists.isNotEmpty) ", ", result.featuredArtists ].join("").unescapeHtml(), - artistUrl: - "https://www.jiosaavn.com/artist/${result.primaryArtistsId.split(",").firstOrNull ?? ""}", - duration: Duration(seconds: int.parse(result.duration)), + durationMs: + Duration(seconds: int.parse(result.duration)).inMilliseconds, id: result.id, pageUrl: result.url, thumbnail: result.image?.last.link ?? "", 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; } static Future> fetchSiblings({ - required Track track, + required TrackSourceQuery query, required Ref ref, bool weakMatch = false, }) async { - final query = SourcedTrack.getSearchTerm(track); + final searchQuery = SourcedTrack.getSearchTerm(query); 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 .where( (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 = [ s.primaryArtists, if (s.featuredArtists.isNotEmpty) ", ", s.featuredArtists ].join("").unescapeHtml(); final sameArtists = artistNames.split(", ").any( - (artist) => - trackArtistNames?.any((ar) => artist == ar) ?? false, + (artist) => trackArtistNames.any((ar) => artist == ar), ); if (weakMatch) { final containsName = - s.name?.unescapeHtml().contains(track.name!) ?? false; + s.name?.unescapeHtml().contains(query.title) ?? false; final containsPrimaryArtist = s.primaryArtists .unescapeHtml() - .contains(trackArtistNames?.first ?? ""); + .contains(trackArtistNames.first); return containsName && containsPrimaryArtist; } @@ -181,23 +165,24 @@ class JioSaavnSourcedTrack extends SourcedTrack { if (siblings.isNotEmpty) { return this; } - final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); return JioSaavnSourcedTrack( ref: ref, siblings: fetchedSiblings - .where((s) => s.info.id != sourceInfo.id) + .where((s) => s.info.id != info.id) .map((s) => s.info) .toList(), source: source, - sourceInfo: sourceInfo, - track: this, + info: info, + query: query, + sources: sources, ); } @override - Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id) { + Future swapWithSibling(TrackSourceInfo sibling) async { + if (sibling.id == this.info.id) { return null; } @@ -208,7 +193,7 @@ class JioSaavnSourcedTrack extends SourcedTrack { ? sibling : siblings.firstWhere((s) => s.id == sibling.id); 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]); @@ -217,7 +202,7 @@ class JioSaavnSourcedTrack extends SourcedTrack { final database = ref.read(databaseProvider); await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: id!, + trackId: query.id, sourceId: info.id, sourceType: const Value(SourceType.jiosaavn), // Because we're sorting by createdAt in the query @@ -230,9 +215,16 @@ class JioSaavnSourcedTrack extends SourcedTrack { return JioSaavnSourcedTrack( ref: ref, siblings: newSiblings, - source: source!, - sourceInfo: info, - track: this, + sources: source!, + info: info, + query: query, + source: AudioSource.jiosaavn, ); } + + @override + Future refreshStream() async { + // There's no need to refresh the stream for JioSaavnSourcedTrack + return this; + } } diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 7ab9df44..65d613a6 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -2,15 +2,13 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; -import 'package:spotify/spotify.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/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/sourced_track/enums.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/sourced_track.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; @@ -24,48 +22,24 @@ final pipedProvider = Provider( }, ); -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 { PipedSourcedTrack({ required super.ref, required super.source, required super.siblings, - required super.sourceInfo, - required super.track, + required super.info, + required super.query, + required super.sources, }); static Future fetchFromTrack({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) async { - // Means it wants a refresh of the stream - 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 audioSource = ref.read(userPreferencesProvider).audioSource; final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!)) + ..where((s) => s.trackId.equals(query.id)) ..limit(1) ..orderBy([ (s) => @@ -76,14 +50,14 @@ class PipedSourcedTrack extends SourcedTrack { final pipedClient = ref.read(pipedProvider); if (cachedSource == null) { - final siblings = await fetchSiblings(ref: ref, track: track); + final siblings = await fetchSiblings(ref: ref, query: query); if (siblings.isEmpty) { - throw TrackNotFoundError(track); + throw TrackNotFoundError(query); } await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: track.id!, + trackId: query.id, sourceId: siblings.first.info.id, sourceType: Value( preferences.searchMode == SearchMode.youtube @@ -96,9 +70,10 @@ class PipedSourcedTrack extends SourcedTrack { return PipedSourcedTrack( ref: ref, siblings: siblings.map((s) => s.info).skip(1).toList(), - source: siblings.first.source as SourceMap, - sourceInfo: siblings.first.info, - track: track, + source: audioSource, + info: siblings.first.info, + query: query, + sources: siblings.first.source!, ); } else { final manifest = await pipedClient.streams(cachedSource.sourceId); @@ -106,44 +81,36 @@ class PipedSourcedTrack extends SourcedTrack { return PipedSourcedTrack( ref: ref, siblings: [], - source: toSourceMap(manifest), - sourceInfo: PipedSourceInfo( + sources: toSources(manifest), + info: TrackSourceInfo( id: manifest.id, - artist: manifest.uploader, - artistUrl: manifest.uploaderUrl, + artists: manifest.uploader, pageUrl: "https://www.youtube.com/watch?v=${manifest.id}", thumbnail: manifest.thumbnailUrl, title: manifest.title, - duration: manifest.duration, - album: null, + durationMs: manifest.duration.inMilliseconds, ), - track: track, + query: query, + source: audioSource, ); } } - static SourceMap toSourceMap(PipedStreamResponse manifest) { - final m4a = manifest.audioStreams - .where((audio) => audio.format == PipedAudioStreamFormat.m4a) - .sorted((a, b) => a.bitrate.compareTo(b.bitrate)); - - final weba = manifest.audioStreams - .where((audio) => audio.format == PipedAudioStreamFormat.webm) - .sorted((a, b) => a.bitrate.compareTo(b.bitrate)); - - return SourceMap( - m4a: SourceQualityMap( - high: m4a.first.url.toString(), - medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(), - low: m4a.last.url.toString(), - ), - weba: SourceQualityMap( - high: weba.first.url.toString(), - medium: - (weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(), - low: weba.last.url.toString(), - ), - ); + static List toSources(PipedStreamResponse manifest) { + return manifest.audioStreams.map((audio) { + return TrackSource( + url: audio.url.toString(), + quality: switch (audio.quality) { + "high" => SourceQualities.high, + "medium" => SourceQualities.medium, + _ => SourceQualities.low, + }, + codec: audio.format == PipedAudioStreamFormat.m4a + ? SourceCodecs.m4a + : SourceCodecs.weba, + bitrate: audio.bitrate.toString(), + ); + }).toList(); } static Future toSiblingType( @@ -151,40 +118,38 @@ class PipedSourcedTrack extends SourcedTrack { YoutubeVideoInfo item, PipedClient pipedClient, ) async { - SourceMap? sourceMap; + List? sources; if (index == 0) { final manifest = await pipedClient.streams(item.id); - sourceMap = toSourceMap(manifest); + sources = toSources(manifest); } final SiblingType sibling = ( - info: PipedSourceInfo( + info: TrackSourceInfo( id: item.id, - artist: item.channelName, - artistUrl: "https://www.youtube.com/${item.channelId}", + artists: item.channelName, pageUrl: "https://www.youtube.com/watch?v=${item.id}", thumbnail: item.thumbnailUrl, title: item.title, - duration: item.duration, - album: null, + durationMs: item.duration.inMilliseconds, ), - source: sourceMap, + source: sources, ); return sibling; } static Future> fetchSiblings({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) async { final pipedClient = ref.read(pipedProvider); final preference = ref.read(userPreferencesProvider); - final query = SourcedTrack.getSearchTerm(track); + final searchQuery = SourcedTrack.getSearchTerm(query); final PipedSearchResult(items: searchResults) = await pipedClient.search( - query, + searchQuery, preference.searchMode == SearchMode.youtube ? PipedFilter.videos : PipedFilter.musicSongs, @@ -196,8 +161,7 @@ class PipedSourcedTrack extends SourcedTrack { : preference.searchMode == SearchMode.youtubeMusic; if (isYouTubeMusic) { - final artists = - (track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList(); + final artists = query.artists; return await Future.wait( searchResults @@ -218,7 +182,7 @@ class PipedSourcedTrack extends SourcedTrack { ); } - if (ServiceUtils.onlyContainsEnglish(query)) { + if (ServiceUtils.onlyContainsEnglish(searchQuery)) { return await Future.wait( searchResults .whereType() @@ -241,7 +205,7 @@ class PipedSourcedTrack extends SourcedTrack { ), ) .toList(), - track, + query, ); return await Future.wait( @@ -254,23 +218,24 @@ class PipedSourcedTrack extends SourcedTrack { if (siblings.isNotEmpty) { return this; } - final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); return PipedSourcedTrack( ref: ref, siblings: fetchedSiblings - .where((s) => s.info.id != sourceInfo.id) + .where((s) => s.info.id != info.id) .map((s) => s.info) .toList(), source: source, - sourceInfo: sourceInfo, - track: this, + info: info, + query: query, + sources: sources, ); } @override - Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id) { + Future swapWithSibling(TrackSourceInfo sibling) async { + if (sibling.id == info.id) { return null; } @@ -281,7 +246,7 @@ class PipedSourcedTrack extends SourcedTrack { ? sibling : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, sourceInfo); + ..insert(0, info); final pipedClient = ref.read(pipedProvider); @@ -290,7 +255,7 @@ class PipedSourcedTrack extends SourcedTrack { final database = ref.read(databaseProvider); await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: id!, + trackId: query.id, sourceId: newSourceInfo.id, sourceType: const Value(SourceType.youtube), // Because we're sorting by createdAt in the query @@ -303,9 +268,23 @@ class PipedSourcedTrack extends SourcedTrack { return PipedSourcedTrack( ref: ref, siblings: newSiblings, - source: toSourceMap(manifest), - sourceInfo: newSourceInfo, - track: this, + sources: toSources(manifest), + info: info, + query: query, + source: source, + ); + } + + @override + Future 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), ); } } diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 193bdc0d..f55e4337 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -1,16 +1,15 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.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/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/song_link/song_link.dart'; import 'package:spotube/services/sourced_track/enums.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/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -21,54 +20,24 @@ final officialMusicRegex = RegExp( 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 { YoutubeSourcedTrack({ required super.source, required super.siblings, - required super.sourceInfo, - required super.track, + required super.info, + required super.query, + required super.sources, required super.ref, }); static Future fetchFromTrack({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) async { - // Indicates the track is requesting a stream refresh - 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 audioSource = ref.read(userPreferencesProvider).audioSource; final database = ref.read(databaseProvider); final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(track.id!)) + ..where((s) => s.trackId.equals(query.id)) ..limit(1) ..orderBy([ (s) => @@ -78,14 +47,14 @@ class YoutubeSourcedTrack extends SourcedTrack { .then((s) => s.firstOrNull); 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) { - throw TrackNotFoundError(track); + throw TrackNotFoundError(query); } await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: track.id!, + trackId: query.id, sourceId: siblings.first.info.id, sourceType: const Value(SourceType.youtube), ), @@ -94,9 +63,10 @@ class YoutubeSourcedTrack extends SourcedTrack { return YoutubeSourcedTrack( ref: ref, siblings: siblings.map((s) => s.info).skip(1).toList(), - source: siblings.first.source as SourceMap, - sourceInfo: siblings.first.info, - track: track, + info: siblings.first.info, + source: audioSource, + sources: siblings.first.source ?? [], + query: query, ); } final (item, manifest) = await ref @@ -106,26 +76,25 @@ class YoutubeSourcedTrack extends SourcedTrack { final sourcedTrack = YoutubeSourcedTrack( ref: ref, siblings: [], - source: toSourceMap(manifest), - sourceInfo: YoutubeSourceInfo( + sources: toTrackSources(manifest), + info: TrackSourceInfo( id: item.id.value, - artist: item.author, - artistUrl: "https://www.youtube.com/channel/${item.channelId}", + artists: item.author, pageUrl: item.url, thumbnail: item.thumbnails.highResUrl, title: item.title, - duration: item.duration ?? Duration.zero, - album: null, + durationMs: item.duration?.inMilliseconds ?? 0, ), - track: track, + query: query, + source: audioSource, ); - AppLogger.log.i("${track.name}: ${sourcedTrack.url}"); + AppLogger.log.i("${query.title}: ${sourcedTrack.url}"); return sourcedTrack; } - static SourceMap toSourceMap(StreamManifest manifest) { + static List toTrackSources(StreamManifest manifest) { var m4a = manifest.audioOnly .where((audio) => audio.codec.mimeType == "audio/mp4") .sortByBitrate(); @@ -137,19 +106,20 @@ class YoutubeSourcedTrack extends SourcedTrack { m4a = m4a.isEmpty ? weba.toList() : m4a; weba = weba.isEmpty ? m4a.toList() : weba; - return SourceMap( - m4a: SourceQualityMap( - high: m4a.first.url.toString(), - medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(), - low: m4a.last.url.toString(), - ), - weba: SourceQualityMap( - high: weba.first.url.toString(), - medium: - (weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(), - low: weba.last.url.toString(), - ), - ); + return manifest.audioOnly.map((streamInfo) { + return TrackSource( + url: streamInfo.url.toString(), + quality: streamInfo.qualityLabel == "AUDIO_QUALITY_HIGH" + ? SourceQualities.high + : streamInfo.qualityLabel == "AUDIO_QUALITY_MEDIUM" + ? SourceQualities.medium + : SourceQualities.low, + codec: streamInfo.codec.mimeType == "audio/mp4" + ? SourceCodecs.m4a + : SourceCodecs.weba, + bitrate: streamInfo.bitrate.bitsPerSecond.toString(), + ); + }).toList(); } static Future toSiblingType( @@ -158,23 +128,21 @@ class YoutubeSourcedTrack extends SourcedTrack { dynamic ref, ) async { assert(ref is WidgetRef || ref is Ref, "Invalid ref type"); - SourceMap? sourceMap; + List? sourceMap; if (index == 0) { final manifest = await ref.read(youtubeEngineProvider).getStreamManifest(item.id); - sourceMap = toSourceMap(manifest); + sourceMap = toTrackSources(manifest); } final SiblingType sibling = ( - info: YoutubeSourceInfo( + info: TrackSourceInfo( id: item.id, - artist: item.channelName, - artistUrl: "https://www.youtube.com/channel/${item.channelId}", + artists: item.channelName, pageUrl: "https://www.youtube.com/watch?v=${item.id}", thumbnail: item.thumbnailUrl, title: item.title, - duration: item.duration, - album: null, + durationMs: item.duration.inMilliseconds, ), source: sourceMap, ); @@ -183,16 +151,13 @@ class YoutubeSourcedTrack extends SourcedTrack { } static List rankResults( - List results, Track track) { - final artists = - (track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList(); - + List results, TrackSourceQuery track) { return results .sorted((a, b) => b.views.compareTo(a.views)) .map((sibling) { int score = 0; - for (final artist in artists) { + for (final artist in track.artists) { final isSameChannelArtist = sibling.channelName.toLowerCase() == artist.toLowerCase(); final channelContainsArtist = sibling.channelName @@ -212,7 +177,7 @@ class YoutubeSourcedTrack extends SourcedTrack { } final titleContainsTrackName = - sibling.title.toLowerCase().contains(track.name!.toLowerCase()); + sibling.title.toLowerCase().contains(track.title.toLowerCase()); final hasOfficialFlag = officialMusicRegex.hasMatch(sibling.title.toLowerCase()); @@ -237,12 +202,12 @@ class YoutubeSourcedTrack extends SourcedTrack { } static Future> fetchFromIsrc({ - required Track track, + required TrackSourceQuery track, required Ref ref, }) async { final isrcResults = []; - final isrc = track.externalIds?.isrc; - if (isrc != null && isrc.isNotEmpty) { + final isrc = track.isrc; + if (isrc.isNotEmpty) { final searchedVideos = await ref.read(youtubeEngineProvider).searchVideos(isrc.toString()); if (searchedVideos.isNotEmpty) { @@ -254,15 +219,18 @@ class YoutubeSourcedTrack extends SourcedTrack { .replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '') .split(RegExp(r'\p{Z}+', unicode: true)) .where((item) => item.isNotEmpty); - final spWords = track.name! + final spWords = track.title .toLowerCase() .replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '') .split(RegExp(r'\p{Z}+', unicode: true)) .where((item) => item.isNotEmpty); // Single word and duration match with 3 second tolerance if (ytWords.any((word) => spWords.contains(word)) && - (videoInfo.duration - track.duration!) - .abs().inMilliseconds <= 3000) { + (videoInfo.duration - + Duration(milliseconds: track.durationMs)) + .abs() + .inMilliseconds <= + 3000) { return videoInfo; } return null; @@ -275,21 +243,21 @@ class YoutubeSourcedTrack extends SourcedTrack { } static Future> fetchSiblings({ - required Track track, + required TrackSourceQuery query, required Ref ref, }) async { final videoResults = []; - if (track is! SourcedTrack) { + if (query is! SourcedTrack) { final isrcResults = await fetchFromIsrc( - track: track, + track: query, ref: ref, ); videoResults.addAll(isrcResults); if (isrcResults.isEmpty) { - final links = await SongLinkService.links(track.id!); + final links = await SongLinkService.links(query.id); final ytLink = links.firstWhereOrNull( (link) => link.platform == "youtube", ); @@ -308,18 +276,18 @@ class YoutubeSourcedTrack extends SourcedTrack { } } - final query = SourcedTrack.getSearchTerm(track); + final searchQuery = SourcedTrack.getSearchTerm(query); final searchResults = - await ref.read(youtubeEngineProvider).searchVideos(query); + await ref.read(youtubeEngineProvider).searchVideos(searchQuery); - if (ServiceUtils.onlyContainsEnglish(query)) { + if (ServiceUtils.onlyContainsEnglish(searchQuery)) { videoResults .addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList()); } else { videoResults.addAll(rankResults( searchResults.map(YoutubeVideoInfo.fromVideo).toList(), - track, + query, )); } @@ -338,8 +306,8 @@ class YoutubeSourcedTrack extends SourcedTrack { } @override - Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id) { + Future swapWithSibling(TrackSourceInfo sibling) async { + if (sibling.id == info.id) { return null; } @@ -350,7 +318,7 @@ class YoutubeSourcedTrack extends SourcedTrack { ? sibling : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, sourceInfo); + ..insert(0, info); final manifest = await ref .read(youtubeEngineProvider) @@ -360,7 +328,7 @@ class YoutubeSourcedTrack extends SourcedTrack { await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( - trackId: id!, + trackId: query.id, sourceId: newSourceInfo.id, sourceType: const Value(SourceType.youtube), // Because we're sorting by createdAt in the query @@ -372,10 +340,11 @@ class YoutubeSourcedTrack extends SourcedTrack { return YoutubeSourcedTrack( ref: ref, + source: source, siblings: newSiblings, - source: toSourceMap(manifest), - sourceInfo: newSourceInfo, - track: this, + sources: toTrackSources(manifest), + info: info, + query: query, ); } @@ -384,17 +353,37 @@ class YoutubeSourcedTrack extends SourcedTrack { if (siblings.isNotEmpty) { return this; } - final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); return YoutubeSourcedTrack( ref: ref, siblings: fetchedSiblings - .where((s) => s.info.id != sourceInfo.id) + .where((s) => s.info.id != info.id) .map((s) => s.info) .toList(), source: source, - sourceInfo: sourceInfo, - track: this, + sources: sources, + info: info, + query: query, ); } + + @override + Future 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; + } } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 72a98a0b..ccff62b0 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -195,10 +195,9 @@ abstract class ServiceUtils { @Deprecated("In favor spotify lyrics api, this isn't needed anymore") static Future getTimedLyrics(SourcedTrack track) async { - final artistNames = - track.artists?.map((artist) => artist.name!).toList() ?? []; + final artistNames = track.query.artists; final query = getTitle( - track.name!, + track.query.title, artists: artistNames, ); @@ -217,13 +216,11 @@ abstract class ServiceUtils { final rateSortedResults = results.map((result) { final title = result.text.trim().toLowerCase(); int points = 0; - final hasAllArtists = track.artists - ?.map((artist) => artist.name!) - .every((artist) => title.contains(artist.toLowerCase())) ?? - false; - final hasTrackName = title.contains(track.name!.toLowerCase()); + final hasAllArtists = track.query.artists + .every((artist) => title.contains(artist.toLowerCase())); + final hasTrackName = title.contains(track.query.title.toLowerCase()); final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live"); - final exactYtMatch = title == track.sourceInfo.title.toLowerCase(); + final exactYtMatch = title == track.info.title.toLowerCase(); if (exactYtMatch) points = 7; for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) { if (criteria) points++;