feat: add playlist related providers

This commit is contained in:
Kingkor Roy Tirtho 2024-03-15 21:38:43 +06:00
parent 1d580e8b6a
commit 91c6ddbc2d
11 changed files with 780 additions and 0 deletions

View File

@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recommendation_seeds.freezed.dart';
part 'recommendation_seeds.g.dart';
@freezed
class RecommendationSeeds with _$RecommendationSeeds {
factory RecommendationSeeds(
num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence,
) = _RecommendationSeeds;
factory RecommendationSeeds.fromJson(Map<String, dynamic> json) =>
_$RecommendationSeedsFromJson(json);
}

View File

@ -0,0 +1,446 @@
// 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 'recommendation_seeds.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
RecommendationSeeds _$RecommendationSeedsFromJson(Map<String, dynamic> json) {
return _RecommendationSeeds.fromJson(json);
}
/// @nodoc
mixin _$RecommendationSeeds {
num? get acousticness => throw _privateConstructorUsedError;
num? get danceability => throw _privateConstructorUsedError;
@JsonKey(name: "duration_ms")
num? get durationMs => throw _privateConstructorUsedError;
num? get energy => throw _privateConstructorUsedError;
num? get instrumentalness => throw _privateConstructorUsedError;
num? get key => throw _privateConstructorUsedError;
num? get liveness => throw _privateConstructorUsedError;
num? get loudness => throw _privateConstructorUsedError;
num? get mode => throw _privateConstructorUsedError;
num? get popularity => throw _privateConstructorUsedError;
num? get speechiness => throw _privateConstructorUsedError;
num? get tempo => throw _privateConstructorUsedError;
@JsonKey(name: "time_signature")
num? get timeSignature => throw _privateConstructorUsedError;
num? get valence => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RecommendationSeedsCopyWith<RecommendationSeeds> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RecommendationSeedsCopyWith<$Res> {
factory $RecommendationSeedsCopyWith(
RecommendationSeeds value, $Res Function(RecommendationSeeds) then) =
_$RecommendationSeedsCopyWithImpl<$Res, RecommendationSeeds>;
@useResult
$Res call(
{num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence});
}
/// @nodoc
class _$RecommendationSeedsCopyWithImpl<$Res, $Val extends RecommendationSeeds>
implements $RecommendationSeedsCopyWith<$Res> {
_$RecommendationSeedsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? acousticness = freezed,
Object? danceability = freezed,
Object? durationMs = freezed,
Object? energy = freezed,
Object? instrumentalness = freezed,
Object? key = freezed,
Object? liveness = freezed,
Object? loudness = freezed,
Object? mode = freezed,
Object? popularity = freezed,
Object? speechiness = freezed,
Object? tempo = freezed,
Object? timeSignature = freezed,
Object? valence = freezed,
}) {
return _then(_value.copyWith(
acousticness: freezed == acousticness
? _value.acousticness
: acousticness // ignore: cast_nullable_to_non_nullable
as num?,
danceability: freezed == danceability
? _value.danceability
: danceability // ignore: cast_nullable_to_non_nullable
as num?,
durationMs: freezed == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as num?,
energy: freezed == energy
? _value.energy
: energy // ignore: cast_nullable_to_non_nullable
as num?,
instrumentalness: freezed == instrumentalness
? _value.instrumentalness
: instrumentalness // ignore: cast_nullable_to_non_nullable
as num?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as num?,
liveness: freezed == liveness
? _value.liveness
: liveness // ignore: cast_nullable_to_non_nullable
as num?,
loudness: freezed == loudness
? _value.loudness
: loudness // ignore: cast_nullable_to_non_nullable
as num?,
mode: freezed == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as num?,
popularity: freezed == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as num?,
speechiness: freezed == speechiness
? _value.speechiness
: speechiness // ignore: cast_nullable_to_non_nullable
as num?,
tempo: freezed == tempo
? _value.tempo
: tempo // ignore: cast_nullable_to_non_nullable
as num?,
timeSignature: freezed == timeSignature
? _value.timeSignature
: timeSignature // ignore: cast_nullable_to_non_nullable
as num?,
valence: freezed == valence
? _value.valence
: valence // ignore: cast_nullable_to_non_nullable
as num?,
) as $Val);
}
}
/// @nodoc
abstract class _$$RecommendationSeedsImplCopyWith<$Res>
implements $RecommendationSeedsCopyWith<$Res> {
factory _$$RecommendationSeedsImplCopyWith(_$RecommendationSeedsImpl value,
$Res Function(_$RecommendationSeedsImpl) then) =
__$$RecommendationSeedsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence});
}
/// @nodoc
class __$$RecommendationSeedsImplCopyWithImpl<$Res>
extends _$RecommendationSeedsCopyWithImpl<$Res, _$RecommendationSeedsImpl>
implements _$$RecommendationSeedsImplCopyWith<$Res> {
__$$RecommendationSeedsImplCopyWithImpl(_$RecommendationSeedsImpl _value,
$Res Function(_$RecommendationSeedsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? acousticness = freezed,
Object? danceability = freezed,
Object? durationMs = freezed,
Object? energy = freezed,
Object? instrumentalness = freezed,
Object? key = freezed,
Object? liveness = freezed,
Object? loudness = freezed,
Object? mode = freezed,
Object? popularity = freezed,
Object? speechiness = freezed,
Object? tempo = freezed,
Object? timeSignature = freezed,
Object? valence = freezed,
}) {
return _then(_$RecommendationSeedsImpl(
freezed == acousticness
? _value.acousticness
: acousticness // ignore: cast_nullable_to_non_nullable
as num?,
freezed == danceability
? _value.danceability
: danceability // ignore: cast_nullable_to_non_nullable
as num?,
freezed == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as num?,
freezed == energy
? _value.energy
: energy // ignore: cast_nullable_to_non_nullable
as num?,
freezed == instrumentalness
? _value.instrumentalness
: instrumentalness // ignore: cast_nullable_to_non_nullable
as num?,
freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as num?,
freezed == liveness
? _value.liveness
: liveness // ignore: cast_nullable_to_non_nullable
as num?,
freezed == loudness
? _value.loudness
: loudness // ignore: cast_nullable_to_non_nullable
as num?,
freezed == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as num?,
freezed == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as num?,
freezed == speechiness
? _value.speechiness
: speechiness // ignore: cast_nullable_to_non_nullable
as num?,
freezed == tempo
? _value.tempo
: tempo // ignore: cast_nullable_to_non_nullable
as num?,
freezed == timeSignature
? _value.timeSignature
: timeSignature // ignore: cast_nullable_to_non_nullable
as num?,
freezed == valence
? _value.valence
: valence // ignore: cast_nullable_to_non_nullable
as num?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$RecommendationSeedsImpl implements _RecommendationSeeds {
_$RecommendationSeedsImpl(
this.acousticness,
this.danceability,
@JsonKey(name: "duration_ms") this.durationMs,
this.energy,
this.instrumentalness,
this.key,
this.liveness,
this.loudness,
this.mode,
this.popularity,
this.speechiness,
this.tempo,
@JsonKey(name: "time_signature") this.timeSignature,
this.valence);
factory _$RecommendationSeedsImpl.fromJson(Map<String, dynamic> json) =>
_$$RecommendationSeedsImplFromJson(json);
@override
final num? acousticness;
@override
final num? danceability;
@override
@JsonKey(name: "duration_ms")
final num? durationMs;
@override
final num? energy;
@override
final num? instrumentalness;
@override
final num? key;
@override
final num? liveness;
@override
final num? loudness;
@override
final num? mode;
@override
final num? popularity;
@override
final num? speechiness;
@override
final num? tempo;
@override
@JsonKey(name: "time_signature")
final num? timeSignature;
@override
final num? valence;
@override
String toString() {
return 'RecommendationSeeds(acousticness: $acousticness, danceability: $danceability, durationMs: $durationMs, energy: $energy, instrumentalness: $instrumentalness, key: $key, liveness: $liveness, loudness: $loudness, mode: $mode, popularity: $popularity, speechiness: $speechiness, tempo: $tempo, timeSignature: $timeSignature, valence: $valence)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RecommendationSeedsImpl &&
(identical(other.acousticness, acousticness) ||
other.acousticness == acousticness) &&
(identical(other.danceability, danceability) ||
other.danceability == danceability) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.energy, energy) || other.energy == energy) &&
(identical(other.instrumentalness, instrumentalness) ||
other.instrumentalness == instrumentalness) &&
(identical(other.key, key) || other.key == key) &&
(identical(other.liveness, liveness) ||
other.liveness == liveness) &&
(identical(other.loudness, loudness) ||
other.loudness == loudness) &&
(identical(other.mode, mode) || other.mode == mode) &&
(identical(other.popularity, popularity) ||
other.popularity == popularity) &&
(identical(other.speechiness, speechiness) ||
other.speechiness == speechiness) &&
(identical(other.tempo, tempo) || other.tempo == tempo) &&
(identical(other.timeSignature, timeSignature) ||
other.timeSignature == timeSignature) &&
(identical(other.valence, valence) || other.valence == valence));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
acousticness,
danceability,
durationMs,
energy,
instrumentalness,
key,
liveness,
loudness,
mode,
popularity,
speechiness,
tempo,
timeSignature,
valence);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
__$$RecommendationSeedsImplCopyWithImpl<_$RecommendationSeedsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$RecommendationSeedsImplToJson(
this,
);
}
}
abstract class _RecommendationSeeds implements RecommendationSeeds {
factory _RecommendationSeeds(
final num? acousticness,
final num? danceability,
@JsonKey(name: "duration_ms") final num? durationMs,
final num? energy,
final num? instrumentalness,
final num? key,
final num? liveness,
final num? loudness,
final num? mode,
final num? popularity,
final num? speechiness,
final num? tempo,
@JsonKey(name: "time_signature") final num? timeSignature,
final num? valence) = _$RecommendationSeedsImpl;
factory _RecommendationSeeds.fromJson(Map<String, dynamic> json) =
_$RecommendationSeedsImpl.fromJson;
@override
num? get acousticness;
@override
num? get danceability;
@override
@JsonKey(name: "duration_ms")
num? get durationMs;
@override
num? get energy;
@override
num? get instrumentalness;
@override
num? get key;
@override
num? get liveness;
@override
num? get loudness;
@override
num? get mode;
@override
num? get popularity;
@override
num? get speechiness;
@override
num? get tempo;
@override
@JsonKey(name: "time_signature")
num? get timeSignature;
@override
num? get valence;
@override
@JsonKey(ignore: true)
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recommendation_seeds.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(
Map<String, dynamic> json) =>
_$RecommendationSeedsImpl(
json['acousticness'] as num?,
json['danceability'] as num?,
json['duration_ms'] as num?,
json['energy'] as num?,
json['instrumentalness'] as num?,
json['key'] as num?,
json['liveness'] as num?,
json['loudness'] as num?,
json['mode'] as num?,
json['popularity'] as num?,
json['speechiness'] as num?,
json['tempo'] as num?,
json['time_signature'] as num?,
json['valence'] as num?,
);
Map<String, dynamic> _$$RecommendationSeedsImplToJson(
_$RecommendationSeedsImpl instance) =>
<String, dynamic>{
'acousticness': instance.acousticness,
'danceability': instance.danceability,
'duration_ms': instance.durationMs,
'energy': instance.energy,
'instrumentalness': instance.instrumentalness,
'key': instance.key,
'liveness': instance.liveness,
'loudness': instance.loudness,
'mode': instance.mode,
'popularity': instance.popularity,
'speechiness': instance.speechiness,
'tempo': instance.tempo,
'time_signature': instance.timeSignature,
'valence': instance.valence,
};

View File

@ -0,0 +1,81 @@
part of '../spotify.dart';
class FavoritePlaylistsState extends PaginatedState<PlaylistSimple> {
FavoritePlaylistsState({
required super.items,
required super.offset,
required super.limit,
required super.hasMore,
});
@override
FavoritePlaylistsState copyWith({
List<PlaylistSimple>? items,
int? offset,
int? limit,
bool? hasMore,
}) {
return FavoritePlaylistsState(
items: items ?? this.items,
offset: offset ?? this.offset,
limit: limit ?? this.limit,
hasMore: hasMore ?? this.hasMore,
);
}
}
class FavoritePlaylistsNotifier
extends PaginatedAsyncNotifier<PlaylistSimple, FavoritePlaylistsState> {
FavoritePlaylistsNotifier() : super();
@override
fetch(int offset, int limit) async {
final playlists = await spotify.playlists.me.getPage(
limit,
offset,
);
return playlists.items?.toList() ?? [];
}
@override
build() async {
ref.watch(spotifyProvider);
final playlists = await fetch(0, 20);
return FavoritePlaylistsState(
items: playlists,
offset: 0,
limit: 20,
hasMore: playlists.length == 20,
);
}
}
final favoritePlaylistsProvider =
AsyncNotifierProvider<FavoritePlaylistsNotifier, FavoritePlaylistsState>(
() => FavoritePlaylistsNotifier(),
);
final allFavoritePlaylistsProvider = FutureProvider<List<PlaylistSimple>>(
(ref) async {
final spotify = ref.watch(spotifyProvider);
return (await spotify.playlists.me.all()).toList();
},
);
final isFavoritePlaylistProvider = FutureProvider.family<bool, String>(
(ref, id) async {
final spotify = ref.watch(spotifyProvider);
final me = ref.watch(meProvider);
if (me.value == null) {
return false;
}
final follows =
await spotify.playlists.followedByUsers(id, [me.value!.id!]);
return follows[me.value!.id!] ?? false;
},
);

View File

@ -0,0 +1,58 @@
part of '../spotify.dart';
class FeaturedPlaylistsState extends PaginatedState<PlaylistSimple> {
FeaturedPlaylistsState({
required super.items,
required super.offset,
required super.limit,
required super.hasMore,
});
@override
FeaturedPlaylistsState copyWith({
List<PlaylistSimple>? items,
int? offset,
int? limit,
bool? hasMore,
}) {
return FeaturedPlaylistsState(
items: items ?? this.items,
offset: offset ?? this.offset,
limit: limit ?? this.limit,
hasMore: hasMore ?? this.hasMore,
);
}
}
class FeaturedPlaylistsNotifier
extends PaginatedAsyncNotifier<PlaylistSimple, FeaturedPlaylistsState> {
FeaturedPlaylistsNotifier() : super();
@override
fetch(int offset, int limit) async {
final playlists = await spotify.playlists.featured.getPage(
limit,
offset,
);
return playlists.items?.toList() ?? [];
}
@override
build() async {
ref.watch(spotifyProvider);
final playlists = await fetch(0, 20);
return FeaturedPlaylistsState(
items: playlists,
offset: 0,
limit: 20,
hasMore: playlists.length == 20,
);
}
}
final featuredPlaylistsProvider =
AsyncNotifierProvider<FeaturedPlaylistsNotifier, FeaturedPlaylistsState>(
() => FeaturedPlaylistsNotifier(),
);

View File

@ -0,0 +1,34 @@
part of '../spotify.dart';
typedef GeneratePlaylistProviderInput = ({
Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target,
});
final generatePlaylistProvider =
FutureProvider.family<List<TrackSimple>, GeneratePlaylistProviderInput>(
(ref, input) async {
final spotify = ref.watch(spotifyProvider);
final market = ref.watch(
userPreferencesProvider.select((s) => s.recommendationMarket),
);
final recommendation = await spotify.recommendations.get(
limit: input.limit,
seedArtists: input.seedArtists?.toList(),
seedGenres: input.seedGenres?.toList(),
seedTracks: input.seedTracks?.toList(),
market: market,
max: input.max?.toJson().cast<String, num>(),
min: input.min?.toJson().cast<String, num>(),
target: input.target?.toJson().cast<String, num>(),
);
return recommendation.tracks?.toList() ?? [];
},
);

View File

@ -0,0 +1,8 @@
part of '../spotify.dart';
final likedTracksProvider = FutureProvider<List<Track>>((ref) async {
final spotify = ref.watch(spotifyProvider);
final savedTracked = await spotify.tracks.me.saved.all();
return savedTracked.map((e) => e.track!).toList();
});

View File

@ -0,0 +1,8 @@
part of '../spotify.dart';
final playlistProvider = FutureProvider.family<Playlist, String>(
(ref, playlistId) async {
final spotify = ref.watch(spotifyProvider);
return spotify.playlists.get(playlistId);
},
);

View File

@ -0,0 +1,57 @@
part of '../spotify.dart';
class PlaylistTracksState extends PaginatedState<Track> {
PlaylistTracksState({
required super.items,
required super.offset,
required super.limit,
required super.hasMore,
});
@override
PlaylistTracksState copyWith({
List<Track>? items,
int? offset,
int? limit,
bool? hasMore,
}) {
return PlaylistTracksState(
items: items ?? this.items,
offset: offset ?? this.offset,
limit: limit ?? this.limit,
hasMore: hasMore ?? this.hasMore,
);
}
}
class PlaylistTracksNotifier
extends FamilyPaginatedAsyncNotifier<Track, PlaylistTracksState, String> {
PlaylistTracksNotifier() : super();
@override
fetch(arg, offset, limit) async {
final tracks = await spotify.playlists
.getTracksByPlaylistId(arg)
.getPage(limit, offset);
return tracks.items?.toList() ?? <Track>[];
}
@override
build(arg) async {
ref.watch(spotifyProvider);
final tracks = await fetch(arg, 0, 20);
return PlaylistTracksState(
items: tracks,
offset: 0,
limit: 20,
hasMore: tracks.length == 20,
);
}
}
final playlistTracksProvider = AsyncNotifierProviderFamily<
PlaylistTracksNotifier, PlaylistTracksState, String>(
() => PlaylistTracksNotifier(),
);

View File

@ -12,6 +12,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod/src/async_notifier.dart'; import 'package:riverpod/src/async_notifier.dart';
import 'package:spotube/extensions/map.dart'; import 'package:spotube/extensions/map.dart';
import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/lyrics.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -35,6 +36,15 @@ part 'category/playlists.dart';
part 'lyrics/synced.dart'; part 'lyrics/synced.dart';
part 'playlist/favorite.dart';
part 'playlist/playlist.dart';
part 'playlist/liked.dart';
part 'playlist/tracks.dart';
part 'playlist/featured.dart';
part 'playlist/generate.dart';
part 'user/me.dart';
part 'utils/mixin.dart'; part 'utils/mixin.dart';
part 'utils/state.dart'; part 'utils/state.dart';
part 'utils/provider.dart'; part 'utils/provider.dart';

View File

@ -0,0 +1,6 @@
part of '../spotify.dart';
final meProvider = FutureProvider<User>((ref) async {
final spotify = ref.watch(spotifyProvider);
return spotify.me.get();
});