215 lines
6.2 KiB
Dart
215 lines
6.2 KiB
Dart
|
import 'dart:convert';
|
||
|
|
||
|
import 'package:dio/dio.dart';
|
||
|
import 'package:rhythm_box/services/spotify/spotify_feed.dart';
|
||
|
import 'package:spotify/spotify.dart';
|
||
|
import 'package:timezone/timezone.dart' as tz;
|
||
|
|
||
|
class CustomSpotifyEndpoints {
|
||
|
static const _baseUrl = 'https://api.spotify.com/v1';
|
||
|
final String accessToken;
|
||
|
final Dio _client;
|
||
|
|
||
|
CustomSpotifyEndpoints(this.accessToken)
|
||
|
: _client = Dio(
|
||
|
BaseOptions(
|
||
|
baseUrl: _baseUrl,
|
||
|
responseType: ResponseType.json,
|
||
|
headers: {
|
||
|
'content-type': 'application/json',
|
||
|
if (accessToken.isNotEmpty)
|
||
|
'authorization': 'Bearer $accessToken',
|
||
|
'accept': 'application/json',
|
||
|
},
|
||
|
),
|
||
|
);
|
||
|
|
||
|
// 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,
|
||
|
Market? market,
|
||
|
Market? 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.name,
|
||
|
if (country != null) 'country': country.name,
|
||
|
}.entries.map((e) => '${e.key}=${e.value}').join('&');
|
||
|
|
||
|
final res = await _client.getUri(
|
||
|
Uri.parse('$_baseUrl/views/$view?$queryParams'),
|
||
|
);
|
||
|
|
||
|
if (res.statusCode == 200) {
|
||
|
return res.data;
|
||
|
} else {
|
||
|
throw Exception(
|
||
|
'[CustomSpotifyEndpoints.getView]: Failed to get view'
|
||
|
'\nStatus code: ${res.statusCode}'
|
||
|
'\nBody: ${res.data}',
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Future<List<String>> listGenreSeeds() async {
|
||
|
final res = await _client.getUri(
|
||
|
Uri.parse('$_baseUrl/recommendations/available-genre-seeds'),
|
||
|
);
|
||
|
|
||
|
if (res.statusCode == 200) {
|
||
|
final body = res.data;
|
||
|
return List<String>.from(body['genres'] ?? []);
|
||
|
} else {
|
||
|
throw Exception(
|
||
|
'[CustomSpotifyEndpoints.listGenreSeeds]: Failed to get genre seeds'
|
||
|
'\nStatus code: ${res.statusCode}'
|
||
|
'\nBody: ${res.data}',
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Future<SpotifyHomeFeed> getHomeFeed({
|
||
|
required String spTCookie,
|
||
|
required Market country,
|
||
|
}) async {
|
||
|
final headers = {
|
||
|
'app-platform': 'WebPlayer',
|
||
|
'authorization': 'Bearer $accessToken',
|
||
|
'content-type': 'application/json;charset=UTF-8',
|
||
|
'dnt': '1',
|
||
|
'origin': 'https://open.spotify.com',
|
||
|
'referer': 'https://open.spotify.com/'
|
||
|
};
|
||
|
final response = await _client.getUri(
|
||
|
Uri(
|
||
|
scheme: 'https',
|
||
|
host: 'api-partner.spotify.com',
|
||
|
path: '/pathfinder/v1/query',
|
||
|
queryParameters: {
|
||
|
'operationName': 'home',
|
||
|
'variables': jsonEncode({
|
||
|
'timeZone': tz.local.name,
|
||
|
'sp_t': spTCookie,
|
||
|
'country': country.name,
|
||
|
'facet': null,
|
||
|
'sectionItemsLimit': 10
|
||
|
}),
|
||
|
'extensions': jsonEncode(
|
||
|
{
|
||
|
'persistedQuery': {
|
||
|
'version': 1,
|
||
|
|
||
|
/// GraphQL persisted Query hash
|
||
|
/// This can change overtime. We've to lookout for it
|
||
|
/// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/
|
||
|
'sha256Hash':
|
||
|
'eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be',
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
},
|
||
|
),
|
||
|
options: Options(headers: headers),
|
||
|
);
|
||
|
|
||
|
final data = SpotifyHomeFeed.fromJson(
|
||
|
transformHomeFeedJsonMap(response.data),
|
||
|
);
|
||
|
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
Future<SpotifyHomeFeedSection> getHomeFeedSection(
|
||
|
String sectionUri, {
|
||
|
required String spTCookie,
|
||
|
required Market country,
|
||
|
}) async {
|
||
|
final headers = {
|
||
|
'app-platform': 'WebPlayer',
|
||
|
'authorization': 'Bearer $accessToken',
|
||
|
'content-type': 'application/json;charset=UTF-8',
|
||
|
'dnt': '1',
|
||
|
'origin': 'https://open.spotify.com',
|
||
|
'referer': 'https://open.spotify.com/'
|
||
|
};
|
||
|
final response = await _client.getUri(
|
||
|
Uri(
|
||
|
scheme: 'https',
|
||
|
host: 'api-partner.spotify.com',
|
||
|
path: '/pathfinder/v1/query',
|
||
|
queryParameters: {
|
||
|
'operationName': 'homeSection',
|
||
|
'variables': jsonEncode({
|
||
|
'timeZone': tz.local.name,
|
||
|
'sp_t': spTCookie,
|
||
|
'country': country.name,
|
||
|
'uri': sectionUri
|
||
|
}),
|
||
|
'extensions': jsonEncode(
|
||
|
{
|
||
|
'persistedQuery': {
|
||
|
'version': 1,
|
||
|
|
||
|
/// GraphQL persisted Query hash
|
||
|
/// This can change overtime. We've to lookout for it
|
||
|
/// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/
|
||
|
'sha256Hash':
|
||
|
'eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be',
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
},
|
||
|
),
|
||
|
options: Options(headers: headers),
|
||
|
);
|
||
|
|
||
|
final data = SpotifyHomeFeedSection.fromJson(
|
||
|
transformSectionItemJsonMap(
|
||
|
response.data['data']['homeSections']['sections'][0],
|
||
|
),
|
||
|
);
|
||
|
|
||
|
return data;
|
||
|
}
|
||
|
}
|