feat: playlist generation all parameters support

This commit is contained in:
Kingkor Roy Tirtho 2023-06-08 12:49:08 +06:00
parent d57aad5612
commit 9877d5f517
8 changed files with 666 additions and 60 deletions

View File

@ -1,7 +1,11 @@
{
"cmake.configureOnOpen": false,
"cSpell.words": [
"acousticness",
"danceability",
"instrumentalness",
"Mpris",
"speechiness",
"Spotube",
"winget"
]

View File

@ -220,6 +220,7 @@ class _MultiSelectDialog<T> extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
autofocus: true,
controller: searchController,
decoration: InputDecoration(
hintText: context.l10n.search,

View File

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
typedef RecommendationAttribute = ({double min, double target, double max});
RecommendationAttribute lowValues(double base) =>
(min: 1 * base, target: 0.3 * base, max: 0.3 * base);
RecommendationAttribute moderateValues(double base) =>
(min: 0.5 * base, target: 1 * base, max: 0.5 * base);
RecommendationAttribute highValues(double base) =>
(min: 0.3 * base, target: 0.3 * base, max: 1 * base);
class RecommendationAttributeDials extends HookWidget {
final Widget title;
final RecommendationAttribute values;
final ValueChanged<RecommendationAttribute> onChanged;
final double base;
const RecommendationAttributeDials({
Key? key,
required this.values,
required this.onChanged,
required this.title,
this.base = 1,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final animation = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w500,
);
final minSlider = Row(
children: [
Text(context.l10n.min, style: labelStyle),
Expanded(
child: Slider(
value: values.min / base,
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: value * base,
target: values.target,
max: values.max,
)),
),
),
],
);
final targetSlider = Row(
children: [
Text(context.l10n.target, style: labelStyle),
Expanded(
child: Slider(
value: values.target / base,
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: values.min,
target: value * base,
max: values.max,
)),
),
),
],
);
final maxSlider = Row(
children: [
Text(context.l10n.max, style: labelStyle),
Expanded(
child: Slider(
value: values.max / base,
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: values.min,
target: values.target,
max: value * base,
)),
),
),
],
);
return LayoutBuilder(builder: (context, constrain) {
return Card(
child: ExpansionTile(
title: DefaultTextStyle(
style: Theme.of(context).textTheme.titleMedium!,
child: title,
),
shape: const Border(),
leading: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.rotate(
angle: (animation.value * 3.14) / 2,
child: child,
);
},
child: const Icon(Icons.chevron_right),
),
trailing: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ToggleButtons(
borderRadius: BorderRadius.circular(8),
textStyle: labelStyle,
isSelected: [
values == lowValues(base),
values == moderateValues(base),
values == highValues(base),
],
onPressed: (index) {
RecommendationAttribute newValues = zeroValues;
switch (index) {
case 0:
newValues = lowValues(base);
break;
case 1:
newValues = moderateValues(base);
break;
case 2:
newValues = highValues(base);
break;
}
if (newValues == values) {
onChanged(zeroValues);
} else {
onChanged(newValues);
}
},
children: [
Text(context.l10n.low),
Text(" ${context.l10n.moderate} "),
Text(context.l10n.high),
],
),
),
onExpansionChanged: (value) {
if (value) {
animation.forward();
} else {
animation.reverse();
}
},
children: [
if (constrain.mdAndUp)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 16),
Expanded(child: minSlider),
Expanded(child: targetSlider),
Expanded(child: maxSlider),
],
)
else
Padding(
padding: const EdgeInsets.only(left: 16),
child: Column(
children: [
minSlider,
targetSlider,
maxSlider,
],
),
),
],
),
);
});
}
}

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
class RecommendationAttributeFields extends HookWidget {
final Widget title;
final RecommendationAttribute values;
final ValueChanged<RecommendationAttribute> onChanged;
final Map<String, RecommendationAttribute>? presets;
const RecommendationAttributeFields({
Key? key,
required this.values,
required this.onChanged,
required this.title,
this.presets,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final animation = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w500,
);
final minController = useTextEditingController(text: values.min.toString());
final targetController =
useTextEditingController(text: values.target.toString());
final maxController = useTextEditingController(text: values.max.toString());
useEffect(() {
listener() {
onChanged((
min: double.tryParse(minController.text) ?? 0,
target: double.tryParse(targetController.text) ?? 0,
max: double.tryParse(maxController.text) ?? 0,
));
}
minController.addListener(listener);
targetController.addListener(listener);
maxController.addListener(listener);
return () {
minController.removeListener(listener);
targetController.removeListener(listener);
maxController.removeListener(listener);
};
}, [values]);
final minField = TextField(
controller: minController,
decoration: InputDecoration(
labelText: context.l10n.min,
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
);
final targetField = TextField(
controller: targetController,
decoration: InputDecoration(
labelText: context.l10n.target,
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
);
final maxField = TextField(
controller: maxController,
decoration: InputDecoration(
labelText: context.l10n.max,
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
);
return LayoutBuilder(builder: (context, constrain) {
return Card(
child: ExpansionTile(
title: DefaultTextStyle(
style: Theme.of(context).textTheme.titleMedium!,
child: title,
),
shape: const Border(),
leading: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.rotate(
angle: (animation.value * 3.14) / 2,
child: child,
);
},
child: const Icon(Icons.chevron_right),
),
trailing: presets == null
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ToggleButtons(
borderRadius: BorderRadius.circular(8),
textStyle: labelStyle,
isSelected: presets!.values
.map((value) => value == values)
.toList(),
onPressed: (index) {
RecommendationAttribute newValues =
presets!.values.elementAt(index);
if (newValues == values) {
onChanged(zeroValues);
minController.text = zeroValues.min.toString();
targetController.text = zeroValues.target.toString();
maxController.text = zeroValues.max.toString();
} else {
onChanged(newValues);
minController.text = newValues.min.toString();
targetController.text = newValues.target.toString();
maxController.text = newValues.max.toString();
}
},
children: presets!.keys.map((key) => Text(key)).toList(),
),
),
onExpansionChanged: (value) {
if (value) {
animation.forward();
} else {
animation.reverse();
}
},
children: [
const SizedBox(height: 8),
if (constrain.mdAndUp)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 16),
Expanded(child: minField),
const SizedBox(width: 16),
Expanded(child: targetField),
const SizedBox(width: 16),
Expanded(child: maxField),
const SizedBox(width: 16),
],
)
else
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
minField,
const SizedBox(height: 16),
targetField,
const SizedBox(height: 16),
maxField,
],
),
),
const SizedBox(height: 8),
],
),
);
});
}
}

View File

@ -191,5 +191,31 @@
"skip_download_tracks": "Skip downloading all downloaded tracks",
"do_you_want_to_replace": "Do you want to replace the existing track??",
"replace": "Replace",
"skip": "Skip"
"skip": "Skip",
"select_up_to_count_type": "Select up to {count} {type}",
"select_genres": "Select Genres",
"add_genres": "Add Genres",
"country": "Country",
"number_of_tracks_generate": "Number of tracks to generate",
"acousticness": "Acousticness",
"danceability": "Danceability",
"energy": "Energy",
"instrumentalness": "Instrumentalness",
"liveness": "Liveness",
"loudness": "Loudness",
"speechiness": "Speechiness",
"valence": "Valence",
"popularity": "Popularity",
"key": "Key",
"duration": "Duration (s)",
"tempo": "Tempo (BPM)",
"mode": "Mode",
"time_signature": "Time Signature",
"short": "Short",
"medium": "Medium",
"long": "Long",
"min": "Min",
"max": "Max",
"target": "Target",
"moderate": "Moderate"
}

View File

@ -7,6 +7,8 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/library/playlist_generate/multi_select_field.dart';
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_fields.dart';
import 'package:spotube/components/library/playlist_generate/seeds_multi_autocomplete.dart';
import 'package:spotube/components/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
@ -19,6 +21,8 @@ import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
class PlaylistGeneratorPage extends HookConsumerWidget {
const PlaylistGeneratorPage({Key? key}) : super(key: key);
@ -45,13 +49,34 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final leftSeedCount =
5 - genres.value.length - artists.value.length - tracks.value.length;
// Dial (int 0-1) attributes
final acousticness = useState<RecommendationAttribute>(zeroValues);
final danceability = useState<RecommendationAttribute>(zeroValues);
final energy = useState<RecommendationAttribute>(zeroValues);
final instrumentalness = useState<RecommendationAttribute>(zeroValues);
final key = useState<RecommendationAttribute>(zeroValues);
final liveness = useState<RecommendationAttribute>(zeroValues);
final loudness = useState<RecommendationAttribute>(zeroValues);
final popularity = useState<RecommendationAttribute>(zeroValues);
final speechiness = useState<RecommendationAttribute>(zeroValues);
final valence = useState<RecommendationAttribute>(zeroValues);
// Field editable attributes
final tempo = useState<RecommendationAttribute>(zeroValues);
final durationMs = useState<RecommendationAttribute>(zeroValues);
final mode = useState<RecommendationAttribute>(zeroValues);
final timeSignature = useState<RecommendationAttribute>(zeroValues);
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
seeds: artists,
enabled: enabled,
inputDecoration: InputDecoration(
labelText: "Artists",
labelText: context.l10n.artists,
labelStyle: textTheme.titleMedium,
helperText: "Select up to $leftSeedCount artists",
helperText: context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.artists,
),
),
fetchSeeds: (textEditingValue) => spotify.search
.get(
@ -125,9 +150,12 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
enabled: enabled,
selectedItemDisplayType: SelectedItemDisplayType.list,
inputDecoration: InputDecoration(
labelText: "Tracks",
labelText: context.l10n.tracks,
labelStyle: textTheme.titleMedium,
helperText: "Select up to $leftSeedCount tracks",
helperText: context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.tracks,
),
),
fetchSeeds: (textEditingValue) => spotify.search
.get(
@ -181,9 +209,12 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
onSelected: (value) {
genres.value = value;
},
dialogTitle: const Text("Select genres"),
label: const Text("Add genres"),
helperText: "Select up to $leftSeedCount genres",
dialogTitle: Text(context.l10n.select_genres),
label: Text(context.l10n.add_genres),
helperText: context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.genre,
),
enabled: enabled,
);
final countrySelector = ValueListenableBuilder(
@ -191,7 +222,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
builder: (context, value, _) {
return DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: "Country",
labelText: context.l10n.country,
labelStyle: textTheme.titleMedium,
),
isExpanded: true,
@ -229,7 +260,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Number of tracks to generate",
context.l10n.number_of_tracks_generate,
style: textTheme.titleMedium,
),
Row(
@ -305,10 +336,124 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
const SizedBox(height: 16),
tracksAutocomplete,
],
const SizedBox(height: 16),
RecommendationAttributeDials(
title: Text(context.l10n.acousticness),
values: acousticness.value,
onChanged: (value) {
acousticness.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.danceability),
values: danceability.value,
onChanged: (value) {
danceability.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.energy),
values: energy.value,
onChanged: (value) {
energy.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.instrumentalness),
values: instrumentalness.value,
onChanged: (value) {
instrumentalness.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.liveness),
values: liveness.value,
onChanged: (value) {
liveness.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.loudness),
values: loudness.value,
onChanged: (value) {
loudness.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.speechiness),
values: speechiness.value,
onChanged: (value) {
speechiness.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.valence),
values: valence.value,
onChanged: (value) {
valence.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.popularity),
values: popularity.value,
base: 100,
onChanged: (value) {
popularity.value = value;
},
),
RecommendationAttributeDials(
title: Text(context.l10n.key),
values: key.value,
base: 11,
onChanged: (value) {
key.value = value;
},
),
RecommendationAttributeFields(
title: Text(context.l10n.duration),
values: (
max: durationMs.value.max / 1000,
target: durationMs.value.target / 1000,
min: durationMs.value.min / 1000,
),
onChanged: (value) {
durationMs.value = (
max: value.max * 1000,
target: value.target * 1000,
min: value.min * 1000,
);
},
presets: {
context.l10n.short: (min: 50, target: 90, max: 120),
context.l10n.medium: (min: 120, target: 180, max: 200),
context.l10n.long: (min: 480, target: 560, max: 640)
},
),
RecommendationAttributeFields(
title: Text(context.l10n.tempo),
values: tempo.value,
onChanged: (value) {
tempo.value = value;
},
),
RecommendationAttributeFields(
title: Text(context.l10n.mode),
values: mode.value,
onChanged: (value) {
mode.value = value;
},
),
RecommendationAttributeFields(
title: Text(context.l10n.time_signature),
values: timeSignature.value,
onChanged: (value) {
timeSignature.value = value;
},
),
const SizedBox(height: 20),
FilledButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text("Generate"),
label: Text(context.l10n.generate_playlist),
onPressed: () {
final PlaylistGenerateResultRouteState routeState = (
seeds: (
@ -318,9 +463,22 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
),
market: market.value,
limit: limit.value,
max: null,
min: null,
target: null,
parameters: (
acousticness: acousticness.value,
danceability: danceability.value,
energy: energy.value,
instrumentalness: instrumentalness.value,
liveness: liveness.value,
loudness: loudness.value,
speechiness: speechiness.value,
valence: valence.value,
popularity: popularity.value,
key: key.value,
duration_ms: durationMs.value,
tempo: tempo.value,
mode: mode.value,
time_signature: timeSignature.value,
)
);
GoRouter.of(context).push(
"/library/generate/result",

View File

@ -12,9 +12,7 @@ import 'package:spotube/services/queries/queries.dart';
typedef PlaylistGenerateResultRouteState = ({
({List<String> tracks, List<String> artists, List<String> genres})? seeds,
RecommendationParameters? min,
RecommendationParameters? max,
RecommendationParameters? target,
RecommendationParameters? parameters,
int limit,
String? market,
});
@ -30,15 +28,13 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final (:seeds, :min, :max, :target, :limit, :market) = state;
final (:seeds, :parameters, :limit, :market) = state;
final queryClient = useQueryClient();
final generatedPlaylist = useQueries.playlist.generate(
ref,
seeds: seeds,
min: min,
max: max,
target: target,
parameters: parameters,
limit: limit,
market: market,
);

View File

@ -1,54 +1,113 @@
import 'dart:convert';
import 'package:catcher/catcher.dart';
import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/extensions/map.dart';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/hooks/use_spotify_infinite_query.dart';
import 'package:spotube/hooks/use_spotify_query.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
typedef RecommendationParameters = ({
double acousticness,
double danceability,
double duration_ms,
double energy,
double instrumentalness,
double key,
double liveness,
double loudness,
double mode,
double popularity,
double speechiness,
double tempo,
double time_signature,
double valence,
RecommendationAttribute acousticness,
RecommendationAttribute danceability,
RecommendationAttribute duration_ms,
RecommendationAttribute energy,
RecommendationAttribute instrumentalness,
RecommendationAttribute key,
RecommendationAttribute liveness,
RecommendationAttribute loudness,
RecommendationAttribute mode,
RecommendationAttribute popularity,
RecommendationAttribute speechiness,
RecommendationAttribute tempo,
RecommendationAttribute time_signature,
RecommendationAttribute valence,
});
Map<String, num> recommendationParametersToMap(
RecommendationParameters params) =>
{
"acousticness": params.acousticness,
"danceability": params.danceability,
"duration_ms": params.duration_ms,
"energy": params.energy,
"instrumentalness": params.instrumentalness,
"key": params.key,
"liveness": params.liveness,
"loudness": params.loudness,
"mode": params.mode,
"popularity": params.popularity,
"speechiness": params.speechiness,
"tempo": params.tempo,
"time_signature": params.time_signature,
"valence": params.valence,
Map<String, num> recommendationAttributeToMap(RecommendationAttribute attr) => {
"min": attr.min,
"target": attr.target,
"max": attr.max,
};
({Map<String, num> min, Map<String, num> target, Map<String, num> max})
recommendationParametersToMap(RecommendationParameters params) {
final maxMap = <String, num>{
if (params.acousticness != zeroValues)
"acousticness": params.acousticness.max,
if (params.danceability != zeroValues)
"danceability": params.danceability.max,
if (params.duration_ms != zeroValues) "duration_ms": params.duration_ms.max,
if (params.energy != zeroValues) "energy": params.energy.max,
if (params.instrumentalness != zeroValues)
"instrumentalness": params.instrumentalness.max,
if (params.key != zeroValues) "key": params.key.max,
if (params.liveness != zeroValues) "liveness": params.liveness.max,
if (params.loudness != zeroValues) "loudness": params.loudness.max,
if (params.mode != zeroValues) "mode": params.mode.max,
if (params.popularity != zeroValues) "popularity": params.popularity.max,
if (params.speechiness != zeroValues) "speechiness": params.speechiness.max,
if (params.tempo != zeroValues) "tempo": params.tempo.max,
if (params.time_signature != zeroValues)
"time_signature": params.time_signature.max,
if (params.valence != zeroValues) "valence": params.valence.max,
};
final minMap = <String, num>{
if (params.acousticness != zeroValues)
"acousticness": params.acousticness.min,
if (params.danceability != zeroValues)
"danceability": params.danceability.min,
if (params.duration_ms != zeroValues) "duration_ms": params.duration_ms.min,
if (params.energy != zeroValues) "energy": params.energy.min,
if (params.instrumentalness != zeroValues)
"instrumentalness": params.instrumentalness.min,
if (params.key != zeroValues) "key": params.key.min,
if (params.liveness != zeroValues) "liveness": params.liveness.min,
if (params.loudness != zeroValues) "loudness": params.loudness.min,
if (params.mode != zeroValues) "mode": params.mode.min,
if (params.popularity != zeroValues) "popularity": params.popularity.min,
if (params.speechiness != zeroValues) "speechiness": params.speechiness.min,
if (params.tempo != zeroValues) "tempo": params.tempo.min,
if (params.time_signature != zeroValues)
"time_signature": params.time_signature.min,
if (params.valence != zeroValues) "valence": params.valence.min,
};
final targetMap = <String, num>{
if (params.acousticness != zeroValues)
"acousticness": params.acousticness.target,
if (params.danceability != zeroValues)
"danceability": params.danceability.target,
if (params.duration_ms != zeroValues)
"duration_ms": params.duration_ms.target,
if (params.energy != zeroValues) "energy": params.energy.target,
if (params.instrumentalness != zeroValues)
"instrumentalness": params.instrumentalness.target,
if (params.key != zeroValues) "key": params.key.target,
if (params.liveness != zeroValues) "liveness": params.liveness.target,
if (params.loudness != zeroValues) "loudness": params.loudness.target,
if (params.mode != zeroValues) "mode": params.mode.target,
if (params.popularity != zeroValues) "popularity": params.popularity.target,
if (params.speechiness != zeroValues)
"speechiness": params.speechiness.target,
if (params.tempo != zeroValues) "tempo": params.tempo.target,
if (params.time_signature != zeroValues)
"time_signature": params.time_signature.target,
if (params.valence != zeroValues) "valence": params.valence.target,
};
return (
max: maxMap,
min: minMap,
target: targetMap,
);
}
class PlaylistQueries {
const PlaylistQueries();
@ -140,9 +199,7 @@ class PlaylistQueries {
Query<List<Track>, dynamic> generate(
WidgetRef ref, {
({List<String> tracks, List<String> artists, List<String> genres})? seeds,
RecommendationParameters? min,
RecommendationParameters? max,
RecommendationParameters? target,
RecommendationParameters? parameters,
int limit = 20,
String? market,
}) {
@ -151,15 +208,18 @@ class PlaylistQueries {
);
final customSpotify = ref.watch(customSpotifyEndpointProvider);
final parametersMap =
parameters == null ? null : recommendationParametersToMap(parameters);
final query = useQuery<List<Track>, dynamic>(
"generate-playlist",
() async {
final tracks = await customSpotify.getRecommendations(
limit: limit,
market: market ?? marketOfPreference,
max: max != null ? recommendationParametersToMap(max) : null,
min: min != null ? recommendationParametersToMap(min) : null,
target: target != null ? recommendationParametersToMap(target) : null,
max: parametersMap?.max,
min: parametersMap?.min,
target: parametersMap?.target,
seedArtists: seeds?.artists,
seedGenres: seeds?.genres,
seedTracks: seeds?.tracks,