Skip to content

Commit

Permalink
CB-261: Authenticate requests to the Spotify Web API
Browse files Browse the repository at this point in the history
  • Loading branch information
ferbncode authored and gentlecat committed Jul 10, 2017
1 parent 26f1cc0 commit b9c1f81
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 11 deletions.
7 changes: 7 additions & 0 deletions critiquebrainz/frontend/external/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ExternalAPIException(Exception):
"""Base exception for this package"""
pass

class SpotifyWebAPIException(ExternalAPIException):
"""Exception related to errors dealing with Spotify API."""
pass
55 changes: 50 additions & 5 deletions critiquebrainz/frontend/external/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,57 @@
import requests
import urllib.parse
from brainzutils import cache
from base64 import b64encode
from flask import current_app as app
from critiquebrainz.frontend.external.exceptions import SpotifyWebAPIException

DEFAULT_CACHE_EXPIRATION = 12 * 60 * 60 # seconds (12 hours)
ACCESS_TOKEN_EXPIRATION = 60 * 60 # seconds (1 hour)

BASE_URL = "https://api.spotify.com/v1"


def _fetch_access_token():
"""Get an access token from the oauth credentials."""

key = cache.gen_key("spotify_access_token")
namespace = "spotify_access_token"
access_token = cache.get(key, namespace)

client_id = app.config.get("SPOTIFY_CLIENT_ID")
client_secret = app.config.get("SPOTIFY_CLIENT_SECRET")
auth_value = b64encode(bytes(f"{client_id}:{client_secret}", "utf-8")).decode("utf-8")

if not access_token:
access_token = requests.post(
"https://accounts.spotify.com/api/token",
data={"grant_type": "client_credentials"},
headers={"Authorization": f"Basic {auth_value}"},
).json()
access_token = access_token.get('access_token')
if not access_token:
raise SpotifyWebAPIException("Could not fetch access token for Spotify API")
cache.set(key=key, namespace=namespace, val=access_token, time=ACCESS_TOKEN_EXPIRATION)
return access_token


def _get_spotify(query):
"""Make a GET request to Spotify Web API.
Args:
query (str): Query to the Web API.
Returns:
Dictionary containing the information.
"""
access_token = _fetch_access_token()
url = BASE_URL + query
headers = {"Authorization": f"Bearer {access_token}"}

result = requests.get(f"{url}", headers=headers)
return result.json()


def search(query, type, limit=20, offset=0):
"""Get Spotify catalog information about artists, albums, or tracks that
match a keyword string.
Expand All @@ -22,9 +67,9 @@ def search(query, type, limit=20, offset=0):
namespace = "spotify_search"
result = cache.get(key, namespace)
if not result:
result = requests.get("%s/search?q=%s&type=%s&limit=%s&offset=%s" %
(BASE_URL, urllib.parse.quote(query.encode('utf8')),
type, str(limit), str(offset))).json()
result = _get_spotify("/search?q=%s&type=%s&limit=%s&offset=%s" %
(urllib.parse.quote(query.encode('utf8')),
type, str(limit), str(offset)))
cache.set(key=key, namespace=namespace, val=result,
time=DEFAULT_CACHE_EXPIRATION)
return result
Expand All @@ -40,7 +85,7 @@ def get_album(spotify_id):
namespace = "spotify_album"
album = cache.get(spotify_id, namespace)
if not album:
album = requests.get("%s/albums/%s" % (BASE_URL, spotify_id)).json()
album = _get_spotify("/albums/%s" % (spotify_id))
cache.set(key=spotify_id, namespace=namespace, val=album,
time=DEFAULT_CACHE_EXPIRATION)
return album
Expand All @@ -63,7 +108,7 @@ def get_multiple_albums(spotify_ids):
spotify_ids.remove(album_id)

if len(spotify_ids) > 0:
resp = requests.get("%s/albums?ids=%s" % (BASE_URL, ','.join(spotify_ids))).json()['albums']
resp = _get_spotify("/albums?ids=%s" % (','.join(spotify_ids)))["albums"]

received_albums = {}
for album in resp:
Expand Down
7 changes: 4 additions & 3 deletions critiquebrainz/frontend/external/spotify_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from critiquebrainz.frontend.testing import FrontendTestCase
from critiquebrainz.frontend.external import spotify

BASE_URL = "https://api.spotify.com/v1"

class FakeSpotifyResponse():
def __init__(self, url):
self.url = url
def __init__(self, query):
self.url = BASE_URL + query

def json(self):
return dict(url=self.url)
Expand All @@ -14,7 +15,7 @@ class SpotifyTestCase(FrontendTestCase):

def setUp(self):
super(SpotifyTestCase, self).setUp()
spotify.requests.get = lambda url: FakeSpotifyResponse(url)
spotify._get_spotify = lambda query: FakeSpotifyResponse(query).json()
spotify.cache.get = lambda key, namespace=None: None

def test_search(self):
Expand Down
3 changes: 3 additions & 0 deletions custom_config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ MUSICBRAINZ_CLIENT_SECRET = ""
#MBSPOTIFY_BASE_URI = "https://mbspotify.musicbrainz.org/"
#MBSPOTIFY_ACCESS_KEY = ""

# Spotify
#SPOTIFY_CLIENT_ID = ""
#SPOTIFY_CLIENT_SECRET = ""

# OTHER STUFF

Expand Down
4 changes: 4 additions & 0 deletions default_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
MUSICBRAINZ_CLIENT_ID = ""
MUSICBRAINZ_CLIENT_SECRET = ""

# Spotify
SPOTIFY_CLIENT_ID = ""
SPOTIFY_CLIENT_SECRET = ""

# mbspotify
# https://github.com/metabrainz/mbspotify
MBSPOTIFY_BASE_URI = "https://mbspotify.musicbrainz.org/"
Expand Down
20 changes: 17 additions & 3 deletions docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ First, you need to create custom configuration file. Copy the skeleton configura

$ cp custom_config.py.example custom_config.py

Then, open ``critiquebrainz/config.py`` in your favourite text editor and update
Then, open ``critiquebrainz/custom_config.py`` in your favourite text editor and update
any variables, as needed.

Configuring MusicBrainz login
'''''''''''''''''''''''''''''

Before you begin using authentication with MusicBrainz accounts,
you need to set ``MUSICBRAINZ_CLIENT_ID`` and ``MUSICBRAINZ_CLIENT_SECRET`` values.
To obtain these keys, you need to register your instance of CrituqeBrainz on MusicBrainz.
To obtain these keys, you need to register your instance of CritiqueBrainz on MusicBrainz.

**Note** ``<your domain>`` field in the urls listed below should probably be set
to ``localhost``, if you plan to run your CritiqueBrainz instance locally
Expand All @@ -35,9 +35,23 @@ In ``Callback URL`` field type::
http://<your domain>/login/musicbrainz/post

Finally, save the obtained ``OAuth Client ID`` and ``OAuth Client Secret`` fields
in your ``config.py`` fields ``MUSICBRAINZ_CLIENT_ID`` and ``MUSICBRAINZ_CLIENT_SECRET``
in your ``custom_config.py`` fields ``MUSICBRAINZ_CLIENT_ID`` and ``MUSICBRAINZ_CLIENT_SECRET``
respectively.

Configuring Spotfiy API authentication
''''''''''''''''''''''''''''''''''''''

To use the Spotify Web API in an instance of CritiqueBrainz, you need to set the
``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_CLIENT_SECRET`` values. To obtain the keys,
register your instance of CritiqueBrainz on Spotify.

After registering and logging into your Spotify account, head to
https://developer.spotify.com/my-applications/ and then register your application following
instructions on https://developer.spotify.com/web-api/tutorial/#registering-your-application.

Finally, save the obtained ``Client ID`` and ``Client Secret`` fields in your ``custom_config.py``
fields ``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_CLIENT_SECRET`` respectively.

Startup
^^^^^^^
Then you can build all the services::
Expand Down

0 comments on commit b9c1f81

Please sign in to comment.