Spotify API를 이용한 인증 및 음악데이터 조회

2020-05-09

.

Data_Engineering_TIL(20200509)

Study_Program : https://www.fastcampus.co.kr/data_online_engineering

[학습노트]

https://developer.spotify.com/documentation/general/guides/authorization-guide/ 의 Authorization 가이드를 참고한다.

먼저 client_id 및 client_secret를 이용해서 Access token 발급받아본다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"


def main():

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    ## 여기까지 중간체크
    print(r.status_code)
    print(r.text)
    sys.exit(0)


if __name__=='__main__':
    main()

image

아래에 r.text가 타입이 string인데 이거를 json 형태로 바꿔줘야 한다. 그래서 코드를 아래와 같이 작성해준다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)
    print(access_token)
    print(type(access_token))
    sys.exit(0)

if __name__=='__main__':
    main()

image

위에 제이슨형태의 데이터에서 access_token만 가져와서 headers를 만들어준다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"


def main():

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers


if __name__=='__main__':
    main()

액세스 토큰은 3600초가 되면 소멸되기 때문에 소멸될때마다 반복적인 요청을 해줘야 한다. 그래서 이부분을 메인함수에 두는게 아니라 아래 코드와 같이 따로 함수로 만들어준다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():
    headers = get_headers(client_id, client_secret)
    print(headers)
    sys.exit(0)

def get_headers(client_id, client_secret):

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers

if __name__=='__main__':
    main()

image

위와 같이 헤더가 연결이 되면 본격적으로 음악데이터를 아래와 같이 조회해본다. search 앤드포인트를 이용해서 태연을 검색해본다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():
    headers = get_headers(client_id, client_secret)

    params = {
        "q": "taeyeon",
        "type": "artist",
        "limit": "3"
    }

    r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)

    print(r.status_code)
    print(r.text)
    sys.exit(0)



def get_headers(client_id, client_secret):

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers

if __name__=='__main__':
    main()

image

상세 실행결과는 아래와 같다.

200
{
  "artists" : {
    "href" : "https://api.spotify.com/v1/search?query=taeyeon&type=artist&offset=0&limit=3",
    "items" : [ {
      "external_urls" : {
        "spotify" : "https://open.spotify.com/artist/3qNVuliS40BLgXGxhdBdqu"
      },
      "followers" : {
        "href" : null,
        "total" : 1325363
      },
      "genres" : [ "k-pop" ],
      "href" : "https://api.spotify.com/v1/artists/3qNVuliS40BLgXGxhdBdqu",
      "id" : "3qNVuliS40BLgXGxhdBdqu",
      "images" : [ {
        "height" : 640,
        "url" : "https://i.scdn.co/image/6200bf305c6e7b76cdada0a92cba625c94f03c5f",
        "width" : 640
      }, {
        "height" : 320,
        "url" : "https://i.scdn.co/image/872d64c314f6cd8f2651dea0556044abd20bbf23",
        "width" : 320
      }, {
        "height" : 160,
        "url" : "https://i.scdn.co/image/596b177d775c9271341e2c59cab3a173c2788686",
        "width" : 160
      } ],
      "name" : "TAEYEON",
      "popularity" : 71,
      "type" : "artist",
      "uri" : "spotify:artist:3qNVuliS40BLgXGxhdBdqu"
    }, {
      "external_urls" : {
        "spotify" : "https://open.spotify.com/artist/2CHWrLbBTQeRLAgVhBWUXX"
      },
      "followers" : {
        "href" : null,
        "total" : 23
      },
      "genres" : [ ],
      "href" : "https://api.spotify.com/v1/artists/2CHWrLbBTQeRLAgVhBWUXX",
      "id" : "2CHWrLbBTQeRLAgVhBWUXX",
      "images" : [ {
        "height" : 640,
        "url" : "https://i.scdn.co/image/ab67616d0000b2730145b65e6ef4801ce59cacc0",
        "width" : 640
      }, {
        "height" : 300,
        "url" : "https://i.scdn.co/image/ab67616d00001e020145b65e6ef4801ce59cacc0",
        "width" : 300
      }, {
        "height" : 64,
        "url" : "https://i.scdn.co/image/ab67616d000048510145b65e6ef4801ce59cacc0",
        "width" : 64
      } ],
      "name" : "Taeyeon (태연)",
      "popularity" : 2,
      "type" : "artist",
      "uri" : "spotify:artist:2CHWrLbBTQeRLAgVhBWUXX"
    }, {
      "external_urls" : {
        "spotify" : "https://open.spotify.com/artist/6qJGHDVwb6yfH4gaTki52I"
      },
      "followers" : {
        "href" : null,
        "total" : 511
      },
      "genres" : [ ],
      "href" : "https://api.spotify.com/v1/artists/6qJGHDVwb6yfH4gaTki52I",
      "id" : "6qJGHDVwb6yfH4gaTki52I",
      "images" : [ {
        "height" : 640,
        "url" : "https://i.scdn.co/image/ab67616d0000b27351b28836cb892c906a399d48",
        "width" : 640
      }, {
        "height" : 300,
        "url" : "https://i.scdn.co/image/ab67616d00001e0251b28836cb892c906a399d48",
        "width" : 300
      }, {
        "height" : 64,
        "url" : "https://i.scdn.co/image/ab67616d0000485151b28836cb892c906a399d48",
        "width" : 64
      } ],
      "name" : "Kim taeyeon",
      "popularity" : 0,
      "type" : "artist",
      "uri" : "spotify:artist:6qJGHDVwb6yfH4gaTki52I"
    } ],
    "limit" : 3,
    "next" : "https://api.spotify.com/v1/search?query=taeyeon&type=artist&offset=3&limit=3",
    "offset" : 0,
    "previous" : null,
    "total" : 6
  }
}

그러면 만든 프로그램을 활용하기 전에 애러상황에 대한 약간의 코드들도 추가해준다.

애러상황 1 : 액세스아이디나 토큰을 잘못집어넣었을때

아래와 같은 애러가 발생할 것이다.

image

get_headers 함수에서 정확히 어떤것에서 애러가 났는지 확인해보자. 아래와 같이 코드를 만들고 실행해본다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():
    headers = get_headers(client_id, client_secret)

    params = {
        "q": "taeyeon",
        "type": "artist",
        "limit": "3"
    }

    r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)



def get_headers(client_id, client_secret):

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)
    print(r.text)
    print(r.status_code)
    print(r.headers)
    sys.exit(0)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers

if __name__=='__main__':
    main()

아래와 같이 클라이언트 애러인것을 확인할 수 있다.

image

그러면 사용자가 접근을 잘못했을때나 사용자로서는 어쩔 수 없는 400번대 애러를 핸들링 할 수 있도록 코드를 짜본다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():
    headers = get_headers(client_id, client_secret)

    params = {
        "q": "taeyeon",
        "type": "artist",
        "limit": "3"
    }

    try:
        r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)

    except:
        ## 애러메세지를 로깅처리
        logging.error(r.text)

        if r.status_code != 200:
            logging.error(r.text)

            ## 사용자 차원에서 해결할 수 있는 애러일 경우
            ## 429 애러는 too many requests일 경우 발생
            if r.status_code == 429:

                retry_after = json.loads(r.headers)['Retry-After']
                time.sleep(int(retry_after))

                r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)

            ## 401 애러는 Authorization에 문제가 있을때 발생하며 대부분은 Access token이 만료가 된 경우
            elif r.status_code == 401:

                headers = get_headers(client_id, client_secret)
                r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)

            ## 사용자 차원에서 해결할 수 없는 애러일 경우
            else:
                sys.exit(1)



def get_headers(client_id, client_secret):

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers

if __name__=='__main__':
    main()

그러면 태연의 앨범데이터를 가져와보자.

https://developer.spotify.com/documentation/web-api/reference/artists/get-artists-albums/ 에서 get-artists-albums 도큐먼트를 참고한다.

도큐먼트에 가면 예시가 나와 있는데 특정 아티스트에 대해 앨범을 가져오는 것에 대한 GET API 명령어와 결과이다. limit을 2로 줬기 때문에 아래 결과와 같이 리스트 안에 두개의 딕셔너리 형태로 아이템도 2개가 들어온다. limit을 두개로 줬기 때문에 어떤 URL을 통해서 요청을해야 다음 정보를 불러올 수 있는지에 대한 것도 있다. offset은 위치값이다. 0에서부터 시작하기 때문에 0번과 1번 아이템 두개를 가져온것이다. 그리고 해당 아티스트의 토탈앨범수는 308개라는 것도 나와있다.

왜 페이지네이션을 하냐면 사용자가 spotify에 요청을 주면 어떤 아티스트는 308개이고 어떤 아티스트는 1000개이상 이렇게 될 경우에는 한번에 데이터를 가져오면 데이터양이 너무 커지기 때문에 서버에 부하를 줄 수 있어서 페이지를 나누어서 데이터를 제공한다.

curl -X GET "https://api.spotify.com/v1/artists/1vCWHaC5f2uS3yhpwWbIA6/albums?market=ES&include_groups=appears_on&limit=2" -H "Authorization: Bearer {your access token}"
{
  "href": "https://api.spotify.com/v1/artists/1vCWHaC5f2uS3yhpwWbIA6/albums?offset=0&limit=2&include_groups=appears_on&market=ES",
  "items": [
    {
      "album_group": "appears_on",
      "album_type": "album",
      "artists": [
        {
          "external_urls": {
            "spotify": "https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of"
          },
          "href": "https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of",
          "id": "0LyfQWJT6nXafLPZqxe9Of",
          "name": "Various Artists",
          "type": "artist",
          "uri": "spotify:artist:0LyfQWJT6nXafLPZqxe9Of"
        }
      ],
      "available_markets": ["AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IS", "IT", "JP", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SK", "SV", "TH", "TR", "TW", "UY", "VN", "ZA"],
      "external_urls": {
        "spotify": "https://open.spotify.com/album/43977e0YlJeMXG77uCCSMX"
      },
      "href": "https://api.spotify.com/v1/albums/43977e0YlJeMXG77uCCSMX",
      "id": "43977e0YlJeMXG77uCCSMX",
      "images": [
        {
          "height": 640,
          "url": "https://i.scdn.co/image/0da79956d0440a55b20ea4e8e38877bce43275cd",
          "width": 640
        },
        {
          "height": 300,
          "url": "https://i.scdn.co/image/29368267cc6b1eab2600e6e42485d3774621e7d4",
          "width": 300
        },
        {
          "height": 64,
          "url": "https://i.scdn.co/image/779dd6d6a0e124e03a5143d2be729ee4bab3f15f",
          "width": 64
        }
      ],
      "name": "Shut Up Lets Dance (Vol. II)",
      "release_date": "2018-02-09",
      "release_date_precision": "day",
      "type": "album",
      "uri": "spotify:album:43977e0YlJeMXG77uCCSMX"
    },
    {
      "album_group": "appears_on",
      "album_type": "compilation",
      "artists": [
        {
          "external_urls": {
            "spotify": "https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of"
          },
          "href": "https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of",
          "id": "0LyfQWJT6nXafLPZqxe9Of",
          "name": "Various Artists",
          "type": "artist",
          "uri": "spotify:artist:0LyfQWJT6nXafLPZqxe9Of"
        }
      ],
      "available_markets": ["AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IS", "IT", "JP", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SK", "SV", "TH", "TR", "TW", "US", "UY", "VN", "ZA"],
      "external_urls": {
        "spotify": "https://open.spotify.com/album/189ngoT3WxR5mZSYkAGOLF"
      },
      "href": "https://api.spotify.com/v1/albums/189ngoT3WxR5mZSYkAGOLF",
      "id": "189ngoT3WxR5mZSYkAGOLF",
      "images": [
        {
          "height": 600,
          "url": "https://i.scdn.co/image/42f4dbe7e54d52efa14f058fab74d8a0505ef26d",
          "width": 600
        },
        {
          "height": 300,
          "url": "https://i.scdn.co/image/b221fb6d689f84f8e09b493776520194a6f4ef88",
          "width": 300
        },
        {
          "height": 64,
          "url": "https://i.scdn.co/image/0fc4a3cb2ee5b14fdefeb8f20afd84b7fbae7707",
          "width": 64
        }
      ],
      "name": "Classic Club Monsters (25 Floor Killers)",
      "release_date": "2018-02-02",
      "release_date_precision": "day",
      "type": "album",
      "uri": "spotify:album:189ngoT3WxR5mZSYkAGOLF"
    }
  ],
  "limit": 2,
  "next": "https://api.spotify.com/v1/artists/1vCWHaC5f2uS3yhpwWbIA6/albums?offset=2&limit=2&include_groups=appears_on&market=ES",
  "offset": 0,
  "previous": null,
  "total": 308
}

페이지네이션 핸들링을 아래와 같이 적용해본다.

spotify의 태연 URL의 id를 미리 확인하여

3qNVuliS40BLgXGxhdBdqu를 아래 r = requests.get(“https://api.spotify.com/v1/artists/[태연ID]/albums”,headers=headers)으로 삽입해준다.

또는 위에 코딩한 것처럼 search API에서 태연의 url을 추출해서 넣어주는 방법도 있다.

import sys
import requests
import base64
import json
import logging

client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():
    headers = get_headers(client_id, client_secret)

    r = requests.get("https://api.spotify.com/v1/artists/3qNVuliS40BLgXGxhdBdqu/albums",headers=headers)

    raw = json.loads(r.text)

    total = raw['total']
    offset = raw['offset']
    limit = raw['limit']
    next = raw['next']

    print(total)
    print(offset)
    print(limit)
    print(next)

    return None


def get_headers(client_id, client_secret):

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers

if __name__=='__main__':
    main()

위의 코드 실행결과는 아래와 같다.

image

최종적으로 그래서 우리가 의도하고자하는 사용자 인증 및 음악데이터 조회기능을 아래와 같이 spotify_api.py로 구현할 수 있다.

import sys
import requests
import base64
import json
import logging

# 아래 세줄의 코드가 없으면 이 전체 코드의 실행결과 시
# UnicodeEncodeError: 'cp949' codec can't encode character '\u2013' in position 33:
# illegal multibyte sequence 애러발생하기 때문에 추가한 것임
import io
sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding = 'utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.detach(), encoding = 'utf-8')



client_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
client_secret = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

def main():
    headers = get_headers(client_id, client_secret)

    try:
        r = requests.get("https://api.spotify.com/v1/artists/3qNVuliS40BLgXGxhdBdqu/albums",headers=headers)
        raw = json.loads(r.text)

        total = raw['total']
        offset = raw['offset']
        limit = raw['limit']
        next = raw['next']

        albums = []

        ## 태연 앨범전부를 다가져와라
        ## 또는 전체 페이지에서 일부만 가져올 수 있도록 조건을 걸 수도 있다.
        while next:
            r = requests.get(raw['next'], headers=headers)
            raw = json.loads(r.text)
            next = raw['next']
            albums.extend(raw['items'])

        print(albums)

    except:
        ## 애러메세지를 로깅처리
        logging.error(r.text)

        if r.status_code != 200:
            logging.error(r.text)

            ## 사용자 차원에서 해결할 수 있는 애러일 경우
            ## 429 애러는 too many requests일 경우 발생
            if r.status_code == 429:

                retry_after = json.loads(r.headers)['Retry-After']
                time.sleep(int(retry_after))

                r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)

            ## 401 애러는 Authorization에 문제가 있을때 발생하며 대부분은 Access token이 만료가 된 경우
            elif r.status_code == 401:

                headers = get_headers(client_id, client_secret)
                r = requests.get("https://api.spotify.com/v1/search", params=params, headers=headers)

            ## 사용자 차원에서 해결할 수 없는 애러일 경우
            else:
                sys.exit(1)

    return None


def get_headers(client_id, client_secret):

    endpoint = "https://accounts.spotify.com/api/token"
    encoded = base64.b64encode("{}:{}".format(client_id, client_secret).encode('utf-8')).decode('ascii')

    headers = {"Authorization": "Basic {}".format(encoded)}

    payload = {"grant_type": "client_credentials"}

    r = requests.post(endpoint, data=payload, headers=headers)

    access_token = json.loads(r.text)['access_token']

    headers = {"Authorization": "Bearer {}".format(access_token)}

    return headers

if __name__=='__main__':
    main()

실행하면 태연의 앨범정보를 아래와 같이 확인할 수 있다.

image