Skip to content

Commit

Permalink
Add basic oauth client for existing web API use
Browse files Browse the repository at this point in the history
  • Loading branch information
adamcik committed Apr 28, 2017
1 parent 5b39827 commit f09b4b8
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 34 deletions.
3 changes: 3 additions & 0 deletions mopidy_spotify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def get_config_schema(self):

schema['toplist_countries'] = config.List(optional=True)

schema['client_id'] = config.String()
schema['client_secret'] = config.Secret()

return schema

def setup(self, registry):
Expand Down
9 changes: 8 additions & 1 deletion mopidy_spotify/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import spotify

from mopidy_spotify import Extension, library, playback, playlists
from mopidy_spotify import Extension, library, playback, playlists, web


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -38,6 +38,7 @@ def __init__(self, config, audio):
self._session = None
self._event_loop = None
self._bitrate = None
self._web_client = None

self.library = library.SpotifyLibraryProvider(backend=self)
self.playback = playback.SpotifyPlaybackProvider(
Expand All @@ -59,6 +60,12 @@ def on_start(self):
self._config['spotify']['username'],
self._config['spotify']['password'])

self._web_client = web.OAuthClient(
refresh_url='https://auth.mopidy.com/spotify/token',
client_id=self._config['spotify']['client_id'],
client_secret=self._config['spotify']['client_secret'],
proxy_config=self._config['proxy'])

def on_stop(self):
logger.debug('Logging out of Spotify')
self._session.logout()
Expand Down
30 changes: 15 additions & 15 deletions mopidy_spotify/distinct.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,34 @@
logger = logging.getLogger(__name__)


def get_distinct(config, session, requests_session, field, query=None):
def get_distinct(config, session, web_client, field, query=None):
# To make the returned data as interesting as possible, we limit
# ourselves to data extracted from the user's playlists when no search
# query is included.

if field == 'artist':
result = _get_distinct_artists(
config, session, requests_session, query)
config, session, web_client, query)
elif field == 'albumartist':
result = _get_distinct_albumartists(
config, session, requests_session, query)
config, session, web_client, query)
elif field == 'album':
result = _get_distinct_albums(
config, session, requests_session, query)
config, session, web_client, query)
elif field == 'date':
result = _get_distinct_dates(
config, session, requests_session, query)
config, session, web_client, query)
else:
result = set()

return result - {None}


def _get_distinct_artists(config, session, requests_session, query):
def _get_distinct_artists(config, session, web_client, query):
logger.debug('Getting distinct artists: %s', query)
if query:
search_result = _get_search(
config, session, requests_session, query, artist=True)
config, session, web_client, query, artist=True)
return {artist.name for artist in search_result.artists}
else:
return {
Expand All @@ -46,12 +46,12 @@ def _get_distinct_artists(config, session, requests_session, query):
for artist in track.artists}


def _get_distinct_albumartists(config, session, requests_session, query):
def _get_distinct_albumartists(config, session, web_client, query):
logger.debug(
'Getting distinct albumartists: %s', query)
if query:
search_result = _get_search(
config, session, requests_session, query, album=True)
config, session, web_client, query, album=True)
return {
artist.name
for album in search_result.albums
Expand All @@ -64,11 +64,11 @@ def _get_distinct_albumartists(config, session, requests_session, query):
if track.album and track.album.artist}


def _get_distinct_albums(config, session, requests_session, query):
def _get_distinct_albums(config, session, web_client, query):
logger.debug('Getting distinct albums: %s', query)
if query:
search_result = _get_search(
config, session, requests_session, query, album=True)
config, session, web_client, query, album=True)
return {album.name for album in search_result.albums}
else:
return {
Expand All @@ -77,11 +77,11 @@ def _get_distinct_albums(config, session, requests_session, query):
if track.album}


def _get_distinct_dates(config, session, requests_session, query):
def _get_distinct_dates(config, session, web_client, query):
logger.debug('Getting distinct album years: %s', query)
if query:
search_result = _get_search(
config, session, requests_session, query, album=True)
config, session, web_client, query, album=True)
return {
album.date
for album in search_result.albums
Expand All @@ -94,7 +94,7 @@ def _get_distinct_dates(config, session, requests_session, query):


def _get_search(
config, session, requests_session, query,
config, session, web_client, query,
album=False, artist=False, track=False):

types = []
Expand All @@ -106,7 +106,7 @@ def _get_search(
types.append('track')

return search.search(
config, session, requests_session, query, types=types)
config, session, web_client, query, types=types)


def _get_playlist_tracks(config, session):
Expand Down
2 changes: 2 additions & 0 deletions mopidy_spotify/ext.conf
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ search_album_count = 20
search_artist_count = 10
search_track_count = 50
toplist_countries =
client_id =
client_secret =
10 changes: 5 additions & 5 deletions mopidy_spotify/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
logger = logging.getLogger(__name__)


def get_images(requests_session, uris):
def get_images(web_client, uris):
result = {}
uri_type_getter = operator.itemgetter('type')
uris = sorted((_parse_uri(u) for u in uris), key=uri_type_getter)
Expand All @@ -36,9 +36,9 @@ def get_images(requests_session, uris):
batch.append(uri)
if len(batch) >= _API_MAX_IDS_PER_REQUEST:
result.update(
_process_uris(requests_session, uri_type, batch))
_process_uris(web_client, uri_type, batch))
batch = []
result.update(_process_uris(requests_session, uri_type, batch))
result.update(_process_uris(web_client, uri_type, batch))
return result


Expand All @@ -59,7 +59,7 @@ def _parse_uri(uri):
raise ValueError('Could not parse %r as a Spotify URI' % uri)


def _process_uris(requests_session, uri_type, uris):
def _process_uris(web_client, uri_type, uris):
result = {}
ids = [u['id'] for u in uris]
ids_to_uris = {u['id']: u for u in uris}
Expand All @@ -70,7 +70,7 @@ def _process_uris(requests_session, uri_type, uris):
lookup_uri = _API_BASE_URI % (uri_type, ','.join(ids))

try:
response = requests_session.get(lookup_uri)
response = web_client.get(lookup_uri)
except requests.RequestException as exc:
logger.debug('Fetching %s failed: %s', lookup_uri, exc)
return result
Expand Down
14 changes: 4 additions & 10 deletions mopidy_spotify/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

from mopidy import backend

# Workaround https://github.com/public/flake8-import-order/issues/49:
from mopidy_spotify import Extension
from mopidy_spotify import (
__version__, browse, distinct, images, lookup, search, utils)
from mopidy_spotify import browse, distinct, images, lookup, search


logger = logging.getLogger(__name__)
Expand All @@ -19,25 +16,22 @@ class SpotifyLibraryProvider(backend.LibraryProvider):
def __init__(self, backend):
self._backend = backend
self._config = backend._config['spotify']
self._requests_session = utils.get_requests_session(
proxy_config=backend._config['proxy'],
user_agent='%s/%s' % (Extension.dist_name, __version__))

def browse(self, uri):
return browse.browse(self._config, self._backend._session, uri)

def get_distinct(self, field, query=None):
return distinct.get_distinct(
self._config, self._backend._session, self._requests_session,
self._config, self._backend._session, self._backend._web_client,
field, query)

def get_images(self, uris):
return images.get_images(self._requests_session, uris)
return images.get_images(self._backend._web_client, uris)

def lookup(self, uri):
return lookup.lookup(self._config, self._backend._session, uri)

def search(self, query=None, uris=None, exact=False):
return search.search(
self._config, self._backend._session, self._requests_session,
self._config, self._backend._session, self._backend._web_client,
query, uris, exact)
4 changes: 2 additions & 2 deletions mopidy_spotify/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
logger = logging.getLogger(__name__)


def search(config, session, requests_session,
def search(config, session, web_client,
query=None, uris=None, exact=False, types=_SEARCH_TYPES):
# TODO Respect `uris` argument
# TODO Support `exact` search
Expand Down Expand Up @@ -56,7 +56,7 @@ def search(config, session, requests_session,
search_count = 50

try:
response = requests_session.get(_API_BASE_URI, params={
response = web_client.get(_API_BASE_URI, params={
'q': sp_query,
'limit': search_count,
'type': ','.join(types)})
Expand Down
5 changes: 4 additions & 1 deletion mopidy_spotify/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

import requests

from mopidy_spotify import Extension, __version__


logger = logging.getLogger(__name__)
TRACE = logging.getLevelName('TRACE')


def get_requests_session(proxy_config, user_agent):
def get_requests_session(proxy_config):
user_agent = '%s/%s' % (Extension.dist_name, __version__)
proxy = httpclient.format_proxy(proxy_config)
full_user_agent = httpclient.format_user_agent(user_agent)

Expand Down
81 changes: 81 additions & 0 deletions mopidy_spotify/web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from __future__ import unicode_literals

import logging
import time

import requests

from mopidy_spotify import utils

logger = logging.getLogger(__name__)


class Error(Exception):
pass


class OAuthClient(object):

def __init__(self, refresh_url, client_id=None, client_secret=None,
proxy_config=None, expiry_margin=60):

if client_id and client_secret:
self._auth = (client_id, client_secret)
else:
self._auth = None

self._refresh_url = refresh_url

self._margin = expiry_margin
self._expires = 0

self._headers = {'Content-Type': 'application/json'}
self._session = utils.get_requests_session(proxy_config or {})

def _request(self, method, url, **kwargs):
try:
resp = self._session.request(method=method, url=url, **kwargs)
resp.raise_for_status()
return resp
except requests.RequestException as e:
raise Error('Fetching %s failed: %s' % (url, e))

def _decode(self, resp):
try:
return resp.json()
except ValueError as e:
raise Error('JSON decoding %s failed: %s' % (resp.request.url, e))

def _should_refresh_token(self):
return not self._auth or time.time() > self._expires - self._margin

This comment has been minimized.

Copy link
@kingosticks

kingosticks May 30, 2017

Member

If either of the client_id and client_secret are None this will keep pointlessly trying to refresh the token. Shouldn't this be return self._auth and time.time() > self._expires - self._margin ?

This comment has been minimized.

Copy link
@kingosticks

kingosticks May 30, 2017

Member

Scrap that, they should always be set.


def _refresh_token(self):
logger.debug('Fetching OAuth token from %s', self._refresh_url)

resp = self._request('POST', self._refresh_url, auth=self._auth,
data={'grant_type': 'client_credentials'})
data = self._decode(resp)

if data.get('error'):
raise Error('OAuth response returned error: %s %s' % (
data['error'], data.get('error_description', '')))
elif not data.get('access_token'):
raise Error('OAuth response missing access_token.' %

This comment has been minimized.

Copy link
@kingosticks

kingosticks May 30, 2017

Member

This needs a %s placeholder somewhere

self._refresh_url)
elif data.get('token_type') != 'Bearer':
raise Error('OAuth response from %s has wrong token_type: %s' % (
self._refresh_url, data.get('token_type')))

self._headers['Authorization'] = 'Bearer %s' % data['access_token']
self._expires = time.time() + data.get('expires_in', float('Inf'))

if data.get('expires_in'):
logger.debug('Token expires in %s seconds.', data['expires_in'])
if data.get('scope'):
logger.debug('Token scopes: %s', data['scope'])

def get(self, url, **kwargs):
if self._should_refresh_token():
self._refresh_token()
kwargs.setdefault('headers', {}).update(self._headers)
return self._request('GET', url, **kwargs)

0 comments on commit f09b4b8

Please sign in to comment.