From 997e5fad0d2b51d55b0ba923c8a2a9bbcdec1a9d Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 8 Dec 2022 15:45:37 +0000 Subject: [PATCH] new data model: add Spotify --- catalog/common/__init__.py | 2 +- catalog/common/downloaders.py | 9 +- catalog/common/models.py | 5 +- catalog/music/models.py | 6 +- catalog/music/tests.py | 27 ++ catalog/sites/__init__.py | 1 + catalog/sites/spotify.py | 153 +++++++++ catalog/tests.py | 1 + ...otify_com_v1_albums_65KwtzkJXw7oT819NFWmEP | 303 ++++++++++++++++++ 9 files changed, 501 insertions(+), 6 deletions(-) create mode 100644 catalog/music/tests.py create mode 100644 catalog/sites/spotify.py create mode 100644 test_data/https___api_spotify_com_v1_albums_65KwtzkJXw7oT819NFWmEP diff --git a/catalog/common/__init__.py b/catalog/common/__init__.py index 53d4ba7d..b51c2629 100644 --- a/catalog/common/__init__.py +++ b/catalog/common/__init__.py @@ -5,4 +5,4 @@ from .scrapers import * from . import jsondata -__all__ = ('IdType', 'Item', 'ExternalPage', 'PageData', 'ParseError', 'ScraperMixin', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'setMockMode', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP') +__all__ = ('IdType', 'Item', 'ExternalPage', 'PageData', 'ParseError', 'ScraperMixin', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'setMockMode', 'getMockMode', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP') diff --git a/catalog/common/downloaders.py b/catalog/common/downloaders.py index 6d2e63e8..15186760 100644 --- a/catalog/common/downloaders.py +++ b/catalog/common/downloaders.py @@ -34,6 +34,11 @@ def setMockMode(enabled): MockMode = enabled +def getMockMode(): + global MockMode + return MockMode + + class DownloadError(Exception): def __init__(self, downloader): self.url = downloader.url @@ -63,10 +68,12 @@ class BasicDownloader: 'Cache-Control': 'no-cache', } - def __init__(self, url): + def __init__(self, url, headers=None): self.url = url self.response_type = RESPONSE_OK self.logs = [] + if headers: + self.headers = headers def get_timeout(self): return settings.SCRAPING_TIMEOUT diff --git a/catalog/common/models.py b/catalog/common/models.py index 412f2d3c..e5b19bfb 100644 --- a/catalog/common/models.py +++ b/catalog/common/models.py @@ -18,7 +18,7 @@ class IdType(models.TextChoices): ISSN = 'issn', _('ISSN') CUBN = 'cubn', _('统一书号') ISRC = 'isrc', _('ISRC') # only for songs - UPC = 'upc', _('GTIN UPC EAN码') + GTIN = 'gtin', _('GTIN UPC EAN码') # ISBN is separate Feed = 'feed', _('Feed URL') IMDB = 'imdb', _('IMDb') TMDB_TV = 'tmdb_tv', _('TMDB剧集') @@ -37,6 +37,9 @@ class IdType(models.TextChoices): Bandcamp = 'bandcamp', _('Bandcamp') Spotify_Album = 'spotify_album', _('Spotify专辑') Spotify_Show = 'spotify_show', _('Spotify播客') + Discogs_Release = 'discogs_release', ('Discogs Release') + Discogs_Master = 'discogs_master', ('Discogs Master') + MusicBrainz = 'musicbrainz', ('MusicBrainz ID') DoubanBook_Author = 'doubanbook_author', _('豆瓣读书作者') DoubanCelebrity = 'doubanmovie_celebrity', _('豆瓣电影影人') Goodreads_Author = 'goodreads_author', _('Goodreads作者') diff --git a/catalog/music/models.py b/catalog/music/models.py index 0f6c0136..11d008ba 100644 --- a/catalog/music/models.py +++ b/catalog/music/models.py @@ -2,9 +2,9 @@ from catalog.common import * class Album(Item): - upc = LookupIdDescriptor(IdType.UPC) - douban_music = LookupIdDescriptor(IdType.DoubanMusic) - spotify_album = LookupIdDescriptor(IdType.Spotify_Album) + barcode = PrimaryLookupIdDescriptor(IdType.GTIN) + douban_music = PrimaryLookupIdDescriptor(IdType.DoubanMusic) + spotify_album = PrimaryLookupIdDescriptor(IdType.Spotify_Album) class Meta: proxy = True diff --git a/catalog/music/tests.py b/catalog/music/tests.py new file mode 100644 index 00000000..1c6eeb31 --- /dev/null +++ b/catalog/music/tests.py @@ -0,0 +1,27 @@ +from django.test import TestCase +from catalog.common import * +from catalog.models import * + + +class SpotifyTestCase(TestCase): + def test_parse(self): + t_id_type = IdType.Spotify_Album + t_id_value = '65KwtzkJXw7oT819NFWmEP' + t_url = 'https://open.spotify.com/album/65KwtzkJXw7oT819NFWmEP' + site = SiteList.get_site_by_id_type(t_id_type) + self.assertIsNotNone(site) + self.assertEqual(site.validate_url(t_url), True) + site = SiteList.get_site_by_url(t_url) + self.assertEqual(site.url, t_url) + self.assertEqual(site.id_value, t_id_value) + + @use_local_response + def test_scrape(self): + t_url = 'https://open.spotify.com/album/65KwtzkJXw7oT819NFWmEP' + site = SiteList.get_site_by_url(t_url) + self.assertEqual(site.ready, False) + site.get_page_ready() + self.assertEqual(site.ready, True) + self.assertEqual(site.page.metadata['title'], 'The Race For Space') + self.assertIsInstance(site.page.item, Album) + self.assertEqual(site.page.item.barcode, '3610159662676') diff --git a/catalog/sites/__init__.py b/catalog/sites/__init__.py index 45b11419..1ca1ed46 100644 --- a/catalog/sites/__init__.py +++ b/catalog/sites/__init__.py @@ -6,3 +6,4 @@ from .douban_drama import DoubanDrama from .goodreads import Goodreads from .tmdb import TMDB_Movie from .imdb import IMDB +from .spotify import Spotify diff --git a/catalog/sites/spotify.py b/catalog/sites/spotify.py new file mode 100644 index 00000000..52a90d92 --- /dev/null +++ b/catalog/sites/spotify.py @@ -0,0 +1,153 @@ +""" +Spotify +""" +from django.conf import settings +from catalog.common import * +from catalog.models import * +from .douban import * +import time +import datetime +import requests +import dateparser +import logging + + +_logger = logging.getLogger(__name__) + + +spotify_token = None +spotify_token_expire_time = time.time() + + +@SiteList.register +class Spotify(AbstractSite): + ID_TYPE = IdType.Spotify_Album + URL_PATTERNS = [r'\w+://open\.spotify\.com/album/([a-zA-Z0-9]+)'] + WIKI_PROPERTY_ID = '?' + DEFAULT_MODEL = Album + + @classmethod + def id_to_url(self, id_value): + return f"https://open.spotify.com/album/{id_value}" + + def scrape(self): + api_url = "https://api.spotify.com/v1/albums/" + self.id_value + headers = { + 'Authorization': f"Bearer {get_spotify_token()}" + } + res_data = BasicDownloader(api_url, headers=headers).download().json() + artist = [] + for artist_dict in res_data['artists']: + artist.append(artist_dict['name']) + + title = res_data['name'] + + genre = ', '.join(res_data['genres']) + + company = [] + for com in res_data['copyrights']: + company.append(com['text']) + + duration = 0 + track_list = [] + track_urls = [] + for track in res_data['tracks']['items']: + track_urls.append(track['external_urls']['spotify']) + duration += track['duration_ms'] + if res_data['tracks']['items'][-1]['disc_number'] > 1: + # more than one disc + track_list.append(str( + track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name']) + else: + track_list.append(str(track['track_number']) + '. ' + track['name']) + track_list = '\n'.join(track_list) + + release_date = parse_date(res_data['release_date']).strftime('%Y-%m-%d') + + gtin = None + if res_data['external_ids'].get('upc'): + gtin = res_data['external_ids'].get('upc') + if res_data['external_ids'].get('ean'): + gtin = res_data['external_ids'].get('ean') + # isrc = None + # if res_data['external_ids'].get('isrc'): + # isrc = res_data['external_ids'].get('isrc') + # _logger.error('isrc for album? this should not happen') + + pd = PageData(metadata={ + 'title': title, + 'artist': artist, + 'genre': genre, + 'track_list': track_list, + 'release_date': release_date, + 'duration': duration, + 'company': company, + 'brief': None, + 'cover_image_url': res_data['images'][0]['url'] + }) + if gtin: + pd.lookup_ids[IdType.GTIN] = gtin + if pd.metadata["cover_image_url"]: + imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url) + try: + pd.cover_image = imgdl.download().content + pd.cover_image_extention = imgdl.extention + except Exception: + _logger.debug(f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}') + return pd + + +def get_spotify_token(): + global spotify_token, spotify_token_expire_time + if getMockMode(): + return 'mocked' + if spotify_token is None or is_spotify_token_expired(): + invoke_spotify_token() + return spotify_token + + +def is_spotify_token_expired(): + global spotify_token_expire_time + return True if spotify_token_expire_time <= time.time() else False + + +def invoke_spotify_token(): + global spotify_token, spotify_token_expire_time + r = requests.post( + "https://accounts.spotify.com/api/token", + data={ + "grant_type": "client_credentials" + }, + headers={ + "Authorization": f"Basic {settings.SPOTIFY_CREDENTIAL}" + } + ) + data = r.json() + if r.status_code == 401: + # token expired, try one more time + # this maybe caused by external operations, + # for example debugging using a http client + r = requests.post( + "https://accounts.spotify.com/api/token", + data={ + "grant_type": "client_credentials" + }, + headers={ + "Authorization": f"Basic {settings.SPOTIFY_CREDENTIAL}" + } + ) + data = r.json() + elif r.status_code != 200: + raise Exception(f"Request to spotify API fails. Reason: {r.reason}") + # minus 2 for execution time error + spotify_token_expire_time = int(data['expires_in']) + time.time() - 2 + spotify_token = data['access_token'] + + +def parse_date(raw_str): + return dateparser.parse( + raw_str, + settings={ + "RELATIVE_BASE": datetime.datetime(1900, 1, 1) + } + ) diff --git a/catalog/tests.py b/catalog/tests.py index a50f1ffc..5f0aa779 100644 --- a/catalog/tests.py +++ b/catalog/tests.py @@ -2,6 +2,7 @@ from django.test import TestCase from catalog.book.tests import * from catalog.movie.tests import * from catalog.tv.tests import * +from catalog.music.tests import * from catalog.podcast.tests import * from catalog.performance.tests import * diff --git a/test_data/https___api_spotify_com_v1_albums_65KwtzkJXw7oT819NFWmEP b/test_data/https___api_spotify_com_v1_albums_65KwtzkJXw7oT819NFWmEP new file mode 100644 index 00000000..98d31c58 --- /dev/null +++ b/test_data/https___api_spotify_com_v1_albums_65KwtzkJXw7oT819NFWmEP @@ -0,0 +1,303 @@ +{ + "album_type" : "album", + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "copyrights" : [ { + "text" : "Test Card Recordings", + "type" : "C" + }, { + "text" : "Test Card Recordings", + "type" : "P" + } ], + "external_ids" : { + "upc" : "3610159662676" + }, + "external_urls" : { + "spotify" : "https://open.spotify.com/album/65KwtzkJXw7oT819NFWmEP" + }, + "genres" : [ ], + "href" : "https://api.spotify.com/v1/albums/65KwtzkJXw7oT819NFWmEP", + "id" : "65KwtzkJXw7oT819NFWmEP", + "images" : [ { + "height" : 640, + "url" : "https://i.scdn.co/image/ab67616d0000b273123ebfc7ca99a9bb6342cd36", + "width" : 640 + }, { + "height" : 300, + "url" : "https://i.scdn.co/image/ab67616d00001e02123ebfc7ca99a9bb6342cd36", + "width" : 300 + }, { + "height" : 64, + "url" : "https://i.scdn.co/image/ab67616d00004851123ebfc7ca99a9bb6342cd36", + "width" : 64 + } ], + "label" : "Test Card Recordings", + "name" : "The Race For Space", + "popularity" : 44, + "release_date" : "2014", + "release_date_precision" : "year", + "total_tracks" : 9, + "tracks" : { + "href" : "https://api.spotify.com/v1/albums/65KwtzkJXw7oT819NFWmEP/tracks?offset=0&limit=50", + "items" : [ { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 159859, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/3982V8R7oW3xyV8zASbCGG" + }, + "href" : "https://api.spotify.com/v1/tracks/3982V8R7oW3xyV8zASbCGG", + "id" : "3982V8R7oW3xyV8zASbCGG", + "is_local" : false, + "name" : "The Race For Space", + "preview_url" : "https://p.scdn.co/mp3-preview/cc69663d5b6a7982e5f162e625f1b319b26956ec?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 1, + "type" : "track", + "uri" : "spotify:track:3982V8R7oW3xyV8zASbCGG" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 429374, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/4EhQrGzqi8k24qWIJuG5CH" + }, + "href" : "https://api.spotify.com/v1/tracks/4EhQrGzqi8k24qWIJuG5CH", + "id" : "4EhQrGzqi8k24qWIJuG5CH", + "is_local" : false, + "name" : "Sputnik", + "preview_url" : "https://p.scdn.co/mp3-preview/32ccf0b8f7ef1251c35e97acb405e4e7cc2660d2?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 2, + "type" : "track", + "uri" : "spotify:track:4EhQrGzqi8k24qWIJuG5CH" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 228623, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/4IaRxPHdzLJ78tm7lxg9M8" + }, + "href" : "https://api.spotify.com/v1/tracks/4IaRxPHdzLJ78tm7lxg9M8", + "id" : "4IaRxPHdzLJ78tm7lxg9M8", + "is_local" : false, + "name" : "Gagarin", + "preview_url" : "https://p.scdn.co/mp3-preview/1d91010dc50a73caa3831c4617f3d658ae279339?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 3, + "type" : "track", + "uri" : "spotify:track:4IaRxPHdzLJ78tm7lxg9M8" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 181621, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/6SONXH9dJQgDY9vCjdkZfK" + }, + "href" : "https://api.spotify.com/v1/tracks/6SONXH9dJQgDY9vCjdkZfK", + "id" : "6SONXH9dJQgDY9vCjdkZfK", + "is_local" : false, + "name" : "Fire in the Cockpit", + "preview_url" : "https://p.scdn.co/mp3-preview/a2180cec25187fa80ddc80dcbe36edda1cc169cc?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 4, + "type" : "track", + "uri" : "spotify:track:6SONXH9dJQgDY9vCjdkZfK" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 255606, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/52KMWPHDL84oo2Ncj3O6RX" + }, + "href" : "https://api.spotify.com/v1/tracks/52KMWPHDL84oo2Ncj3O6RX", + "id" : "52KMWPHDL84oo2Ncj3O6RX", + "is_local" : false, + "name" : "E.V.A.", + "preview_url" : "https://p.scdn.co/mp3-preview/732171a4a5e27540b6709602b4af9662fda98595?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 5, + "type" : "track", + "uri" : "spotify:track:52KMWPHDL84oo2Ncj3O6RX" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 379931, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/3jjMyq44OIjNgmpXLhpw7W" + }, + "href" : "https://api.spotify.com/v1/tracks/3jjMyq44OIjNgmpXLhpw7W", + "id" : "3jjMyq44OIjNgmpXLhpw7W", + "is_local" : false, + "name" : "The Other Side", + "preview_url" : "https://p.scdn.co/mp3-preview/5eda4958044595b36842f2362799d91f080a7357?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 6, + "type" : "track", + "uri" : "spotify:track:3jjMyq44OIjNgmpXLhpw7W" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + }, { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/7wbZFLV3wwTqyrKNCJ8Y8D" + }, + "href" : "https://api.spotify.com/v1/artists/7wbZFLV3wwTqyrKNCJ8Y8D", + "id" : "7wbZFLV3wwTqyrKNCJ8Y8D", + "name" : "Smoke Fairies", + "type" : "artist", + "uri" : "spotify:artist:7wbZFLV3wwTqyrKNCJ8Y8D" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 269376, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/5Um9ghqMlKALp9AcRMIk7B" + }, + "href" : "https://api.spotify.com/v1/tracks/5Um9ghqMlKALp9AcRMIk7B", + "id" : "5Um9ghqMlKALp9AcRMIk7B", + "is_local" : false, + "name" : "Valentina", + "preview_url" : "https://p.scdn.co/mp3-preview/9e812bde9e2944d22f1eae78eab2adb89ce1f1cd?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 7, + "type" : "track", + "uri" : "spotify:track:5Um9ghqMlKALp9AcRMIk7B" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 252720, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/5xYZXIgVAND5sWjN8G0hID" + }, + "href" : "https://api.spotify.com/v1/tracks/5xYZXIgVAND5sWjN8G0hID", + "id" : "5xYZXIgVAND5sWjN8G0hID", + "is_local" : false, + "name" : "Go!", + "preview_url" : "https://p.scdn.co/mp3-preview/a7f4e9d98224dea630ee6604938848c3fd0c2842?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 8, + "type" : "track", + "uri" : "spotify:track:5xYZXIgVAND5sWjN8G0hID" + }, { + "artists" : [ { + "external_urls" : { + "spotify" : "https://open.spotify.com/artist/6VsiDFMZJlJ053P1uO4A6h" + }, + "href" : "https://api.spotify.com/v1/artists/6VsiDFMZJlJ053P1uO4A6h", + "id" : "6VsiDFMZJlJ053P1uO4A6h", + "name" : "Public Service Broadcasting", + "type" : "artist", + "uri" : "spotify:artist:6VsiDFMZJlJ053P1uO4A6h" + } ], + "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "KE", "KG", "KH", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RW", "SA", "SC", "SE", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TR", "TT", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "ZA", "ZM", "ZW" ], + "disc_number" : 1, + "duration_ms" : 442359, + "explicit" : false, + "external_urls" : { + "spotify" : "https://open.spotify.com/track/5ERrJuNLnmHj525ooOKyqJ" + }, + "href" : "https://api.spotify.com/v1/tracks/5ERrJuNLnmHj525ooOKyqJ", + "id" : "5ERrJuNLnmHj525ooOKyqJ", + "is_local" : false, + "name" : "Tomorrow", + "preview_url" : "https://p.scdn.co/mp3-preview/779a285aca862b886613815a0c1d1817446b550e?cid=4b150d8d6d374d1e8dbb85f4f11a2ad9", + "track_number" : 9, + "type" : "track", + "uri" : "spotify:track:5ERrJuNLnmHj525ooOKyqJ" + } ], + "limit" : 50, + "next" : null, + "offset" : 0, + "previous" : null, + "total" : 9 + }, + "type" : "album", + "uri" : "spotify:album:65KwtzkJXw7oT819NFWmEP" +} \ No newline at end of file