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, "cmake.configureOnOpen": false,
"cSpell.words": [ "cSpell.words": [
"acousticness",
"danceability",
"instrumentalness",
"Mpris", "Mpris",
"speechiness",
"Spotube", "Spotube",
"winget" "winget"
] ]

View File

@ -220,6 +220,7 @@ class _MultiSelectDialog<T> extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
TextField( TextField(
autofocus: true,
controller: searchController, controller: searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.search, 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", "skip_download_tracks": "Skip downloading all downloaded tracks",
"do_you_want_to_replace": "Do you want to replace the existing track??", "do_you_want_to_replace": "Do you want to replace the existing track??",
"replace": "Replace", "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/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.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/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/seeds_multi_autocomplete.dart';
import 'package:spotube/components/library/playlist_generate/simple_track_tile.dart'; import 'package:spotube/components/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/components/shared/image/universal_image.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/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
class PlaylistGeneratorPage extends HookConsumerWidget { class PlaylistGeneratorPage extends HookConsumerWidget {
const PlaylistGeneratorPage({Key? key}) : super(key: key); const PlaylistGeneratorPage({Key? key}) : super(key: key);
@ -45,13 +49,34 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final leftSeedCount = final leftSeedCount =
5 - genres.value.length - artists.value.length - tracks.value.length; 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>( final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
seeds: artists, seeds: artists,
enabled: enabled, enabled: enabled,
inputDecoration: InputDecoration( inputDecoration: InputDecoration(
labelText: "Artists", labelText: context.l10n.artists,
labelStyle: textTheme.titleMedium, 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 fetchSeeds: (textEditingValue) => spotify.search
.get( .get(
@ -125,9 +150,12 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
enabled: enabled, enabled: enabled,
selectedItemDisplayType: SelectedItemDisplayType.list, selectedItemDisplayType: SelectedItemDisplayType.list,
inputDecoration: InputDecoration( inputDecoration: InputDecoration(
labelText: "Tracks", labelText: context.l10n.tracks,
labelStyle: textTheme.titleMedium, 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 fetchSeeds: (textEditingValue) => spotify.search
.get( .get(
@ -181,9 +209,12 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
onSelected: (value) { onSelected: (value) {
genres.value = value; genres.value = value;
}, },
dialogTitle: const Text("Select genres"), dialogTitle: Text(context.l10n.select_genres),
label: const Text("Add genres"), label: Text(context.l10n.add_genres),
helperText: "Select up to $leftSeedCount genres", helperText: context.l10n.select_up_to_count_type(
leftSeedCount,
context.l10n.genre,
),
enabled: enabled, enabled: enabled,
); );
final countrySelector = ValueListenableBuilder( final countrySelector = ValueListenableBuilder(
@ -191,7 +222,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
builder: (context, value, _) { builder: (context, value, _) {
return DropdownButtonFormField<String>( return DropdownButtonFormField<String>(
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Country", labelText: context.l10n.country,
labelStyle: textTheme.titleMedium, labelStyle: textTheme.titleMedium,
), ),
isExpanded: true, isExpanded: true,
@ -229,7 +260,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Number of tracks to generate", context.l10n.number_of_tracks_generate,
style: textTheme.titleMedium, style: textTheme.titleMedium,
), ),
Row( Row(
@ -305,10 +336,124 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
tracksAutocomplete, 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), const SizedBox(height: 20),
FilledButton.icon( FilledButton.icon(
icon: const Icon(SpotubeIcons.magic), icon: const Icon(SpotubeIcons.magic),
label: Text("Generate"), label: Text(context.l10n.generate_playlist),
onPressed: () { onPressed: () {
final PlaylistGenerateResultRouteState routeState = ( final PlaylistGenerateResultRouteState routeState = (
seeds: ( seeds: (
@ -318,9 +463,22 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
), ),
market: market.value, market: market.value,
limit: limit.value, limit: limit.value,
max: null, parameters: (
min: null, acousticness: acousticness.value,
target: null, 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( GoRouter.of(context).push(
"/library/generate/result", "/library/generate/result",

View File

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

View File

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