mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: personal playlist recommendations
This commit is contained in:
parent
ec11af53a1
commit
ae820a22f2
@ -12,7 +12,6 @@ import 'package:spotube/extensions/context.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class GenrePage extends HookConsumerWidget {
|
||||
const GenrePage({Key? key}) : super(key: key);
|
||||
@ -39,13 +38,13 @@ class GenrePage extends HookConsumerWidget {
|
||||
return categories;
|
||||
}
|
||||
return categories
|
||||
.map((e) => Tuple2(
|
||||
.map((e) => (
|
||||
weightedRatio(e.name!, searchText.value),
|
||||
e,
|
||||
))
|
||||
.sorted((a, b) => b.item1.compareTo(a.item1))
|
||||
.where((e) => e.item1 > 50)
|
||||
.map((e) => e.item2)
|
||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||
.where((e) => e.$1 > 50)
|
||||
.map((e) => e.$2)
|
||||
.toList();
|
||||
},
|
||||
[categoriesQuery.pages, searchText.value],
|
||||
|
@ -18,13 +18,16 @@ class HomePage extends HookConsumerWidget {
|
||||
centerTitle: true,
|
||||
leadingWidth: double.infinity,
|
||||
leading: ThemedButtonsTabBar(
|
||||
tabs: [context.l10n.genre, context.l10n.personalized],
|
||||
tabs: [
|
||||
context.l10n.personalized,
|
||||
context.l10n.genre,
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
GenrePage(),
|
||||
PersonalizedPage(),
|
||||
GenrePage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -102,6 +102,8 @@ class PersonalizedPage extends HookConsumerWidget {
|
||||
[featuredPlaylistsQuery.pages],
|
||||
);
|
||||
|
||||
final madeForUser = useQueries.views.get(ref, "made-for-x-hub");
|
||||
|
||||
final newReleases = useQueries.album.newReleases(ref);
|
||||
final userArtists = useQueries.artist
|
||||
.followedByMeAll(ref)
|
||||
@ -136,6 +138,21 @@ class PersonalizedPage extends HookConsumerWidget {
|
||||
hasNextPage: newReleases.hasNextPage,
|
||||
onFetchMore: newReleases.fetchNext,
|
||||
),
|
||||
...?madeForUser.data?["content"]?["items"]?.map((item) {
|
||||
final playlists = item["content"]?["items"]
|
||||
?.where((itemL2) => itemL2["type"] == "playlist")
|
||||
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
||||
.toList()
|
||||
.cast<PlaylistSimple>() ??
|
||||
<PlaylistSimple>[];
|
||||
if (playlists.isEmpty) return const SizedBox.shrink();
|
||||
return PersonalizedItemCard(
|
||||
playlists: playlists,
|
||||
title: item["name"] ?? "",
|
||||
hasNextPage: false,
|
||||
onFetchMore: () {},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
|
8
lib/provider/custom_spotify_endpoint_provider.dart
Normal file
8
lib/provider/custom_spotify_endpoint_provider.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart';
|
||||
|
||||
final customSpotifyEndpointProvider = Provider<CustomSpotifyEndpoints>((ref) {
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
return CustomSpotifyEndpoints(auth?.accessToken ?? "");
|
||||
});
|
83
lib/services/custom_spotify_endpoints/spotify_endpoints.dart
Normal file
83
lib/services/custom_spotify_endpoints/spotify_endpoints.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class CustomSpotifyEndpoints {
|
||||
static const _baseUrl = 'https://api.spotify.com/v1';
|
||||
final String accessToken;
|
||||
final http.Client _client;
|
||||
|
||||
CustomSpotifyEndpoints(this.accessToken) : _client = http.Client();
|
||||
|
||||
// views API
|
||||
|
||||
/// Get a single view of given genre
|
||||
///
|
||||
/// Currently known genres are:
|
||||
/// - new-releases-page
|
||||
/// - made-for-x-hub (it requires authentication)
|
||||
/// - my-mix-genres (it requires authentication)
|
||||
/// - artist-seed-mixes (it requires authentication)
|
||||
/// - my-mix-decades (it requires authentication)
|
||||
/// - my-mix-moods (it requires authentication)
|
||||
/// - podcasts-and-more (it requires authentication)
|
||||
/// - uniquely-yours-in-hub (it requires authentication)
|
||||
/// - made-for-x-dailymix (it requires authentication)
|
||||
/// - made-for-x-discovery (it requires authentication)
|
||||
Future<Map<String, dynamic>> getView(
|
||||
String view, {
|
||||
int limit = 20,
|
||||
int contentLimit = 10,
|
||||
List<String> types = const [
|
||||
"album",
|
||||
"playlist",
|
||||
"artist",
|
||||
"show",
|
||||
"station",
|
||||
"episode",
|
||||
"merch",
|
||||
"artist_concerts",
|
||||
"uri_link"
|
||||
],
|
||||
String imageStyle = "gradient_overlay",
|
||||
String includeExternal = "audio",
|
||||
String? locale,
|
||||
String? market,
|
||||
String? country,
|
||||
}) async {
|
||||
if (accessToken.isEmpty) {
|
||||
throw Exception('[CustomSpotifyEndpoints.getView]: accessToken is empty');
|
||||
}
|
||||
|
||||
final queryParams = {
|
||||
'limit': limit.toString(),
|
||||
'content_limit': contentLimit.toString(),
|
||||
'types': types.join(','),
|
||||
'image_style': imageStyle,
|
||||
'include_external': includeExternal,
|
||||
'timestamp': DateTime.now().toUtc().toIso8601String(),
|
||||
if (locale != null) 'locale': locale,
|
||||
if (market != null) 'market': market,
|
||||
if (country != null) 'country': country,
|
||||
}.entries.map((e) => '${e.key}=${e.value}').join('&');
|
||||
|
||||
final res = await _client.get(
|
||||
Uri.parse('$_baseUrl/views/$view?$queryParams'),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"authorization": "Bearer $accessToken",
|
||||
"accept": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return jsonDecode(res.body);
|
||||
} else {
|
||||
throw Exception(
|
||||
'[CustomSpotifyEndpoints.getView]: Failed to get view'
|
||||
'\nStatus code: ${res.statusCode}'
|
||||
'\nBody: ${res.body}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import 'package:spotube/services/queries/lyrics.dart';
|
||||
import 'package:spotube/services/queries/playlist.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
import 'package:spotube/services/queries/user.dart';
|
||||
import 'package:spotube/services/queries/views.dart';
|
||||
|
||||
class Queries {
|
||||
const Queries._();
|
||||
@ -15,6 +16,7 @@ class Queries {
|
||||
final playlist = const PlaylistQueries();
|
||||
final search = const SearchQueries();
|
||||
final user = const UserQueries();
|
||||
final views = const ViewsQueries();
|
||||
}
|
||||
|
||||
const useQueries = Queries._();
|
||||
|
25
lib/services/queries/views.dart
Normal file
25
lib/services/queries/views.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
|
||||
class ViewsQueries {
|
||||
const ViewsQueries();
|
||||
|
||||
Query<Map<String, dynamic>?, dynamic> get(
|
||||
WidgetRef ref,
|
||||
String view,
|
||||
) {
|
||||
final customSpotify = ref.watch(customSpotifyEndpointProvider);
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final market = ref
|
||||
.watch(userPreferencesProvider.select((s) => s.recommendationMarket));
|
||||
|
||||
return useQuery<Map<String, dynamic>?, dynamic>("views/$view", () {
|
||||
if (auth == null) return null;
|
||||
return customSpotify.getView(view, market: market, country: market);
|
||||
});
|
||||
}
|
||||
}
|
@ -1306,9 +1306,11 @@ packages:
|
||||
piped_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../piped_client"
|
||||
relative: true
|
||||
source: path
|
||||
path: "."
|
||||
ref: eaade37d0938d31dbfa35bb5152caca4e284bda6
|
||||
resolved-ref: eaade37d0938d31dbfa35bb5152caca4e284bda6
|
||||
url: "https://github.com/KRTirtho/piped_client"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
|
Loading…
Reference in New Issue
Block a user