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> getView( String view, { int limit = 20, int contentLimit = 10, List 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> listGenreSeeds() async { final res = await _client.getUri( Uri.parse('$_baseUrl/recommendations/available-genre-seeds'), ); if (res.statusCode == 200) { final body = res.data; return List.from(body['genres'] ?? []); } else { throw Exception( '[CustomSpotifyEndpoints.listGenreSeeds]: Failed to get genre seeds' '\nStatus code: ${res.statusCode}' '\nBody: ${res.data}', ); } } Future 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 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; } }