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/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
class GenrePage extends HookConsumerWidget {
|
class GenrePage extends HookConsumerWidget {
|
||||||
const GenrePage({Key? key}) : super(key: key);
|
const GenrePage({Key? key}) : super(key: key);
|
||||||
@ -39,13 +38,13 @@ class GenrePage extends HookConsumerWidget {
|
|||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
return categories
|
return categories
|
||||||
.map((e) => Tuple2(
|
.map((e) => (
|
||||||
weightedRatio(e.name!, searchText.value),
|
weightedRatio(e.name!, searchText.value),
|
||||||
e,
|
e,
|
||||||
))
|
))
|
||||||
.sorted((a, b) => b.item1.compareTo(a.item1))
|
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||||
.where((e) => e.item1 > 50)
|
.where((e) => e.$1 > 50)
|
||||||
.map((e) => e.item2)
|
.map((e) => e.$2)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
[categoriesQuery.pages, searchText.value],
|
[categoriesQuery.pages, searchText.value],
|
||||||
|
@ -18,13 +18,16 @@ class HomePage extends HookConsumerWidget {
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leadingWidth: double.infinity,
|
leadingWidth: double.infinity,
|
||||||
leading: ThemedButtonsTabBar(
|
leading: ThemedButtonsTabBar(
|
||||||
tabs: [context.l10n.genre, context.l10n.personalized],
|
tabs: [
|
||||||
|
context.l10n.personalized,
|
||||||
|
context.l10n.genre,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: const TabBarView(
|
body: const TabBarView(
|
||||||
children: [
|
children: [
|
||||||
GenrePage(),
|
|
||||||
PersonalizedPage(),
|
PersonalizedPage(),
|
||||||
|
GenrePage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -102,6 +102,8 @@ class PersonalizedPage extends HookConsumerWidget {
|
|||||||
[featuredPlaylistsQuery.pages],
|
[featuredPlaylistsQuery.pages],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final madeForUser = useQueries.views.get(ref, "made-for-x-hub");
|
||||||
|
|
||||||
final newReleases = useQueries.album.newReleases(ref);
|
final newReleases = useQueries.album.newReleases(ref);
|
||||||
final userArtists = useQueries.artist
|
final userArtists = useQueries.artist
|
||||||
.followedByMeAll(ref)
|
.followedByMeAll(ref)
|
||||||
@ -136,6 +138,21 @@ class PersonalizedPage extends HookConsumerWidget {
|
|||||||
hasNextPage: newReleases.hasNextPage,
|
hasNextPage: newReleases.hasNextPage,
|
||||||
onFetchMore: newReleases.fetchNext,
|
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/playlist.dart';
|
||||||
import 'package:spotube/services/queries/search.dart';
|
import 'package:spotube/services/queries/search.dart';
|
||||||
import 'package:spotube/services/queries/user.dart';
|
import 'package:spotube/services/queries/user.dart';
|
||||||
|
import 'package:spotube/services/queries/views.dart';
|
||||||
|
|
||||||
class Queries {
|
class Queries {
|
||||||
const Queries._();
|
const Queries._();
|
||||||
@ -15,6 +16,7 @@ class Queries {
|
|||||||
final playlist = const PlaylistQueries();
|
final playlist = const PlaylistQueries();
|
||||||
final search = const SearchQueries();
|
final search = const SearchQueries();
|
||||||
final user = const UserQueries();
|
final user = const UserQueries();
|
||||||
|
final views = const ViewsQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
const useQueries = Queries._();
|
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:
|
piped_client:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "../piped_client"
|
path: "."
|
||||||
relative: true
|
ref: eaade37d0938d31dbfa35bb5152caca4e284bda6
|
||||||
source: path
|
resolved-ref: eaade37d0938d31dbfa35bb5152caca4e284bda6
|
||||||
|
url: "https://github.com/KRTirtho/piped_client"
|
||||||
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
Loading…
Reference in New Issue
Block a user