Querying Spotify's web API
So far, we have only prepared the terrain and now things start to get a bit more interesting. In this section, we are going to create the basic functions to send requests to Spotify's Web API; more specifically, we want to be able to search for an artist, get an artist's list of albums, get a list of tracks in that album, and finally we want to send a request to actually play a given track in Spotify's client that is currently active. It can be the browser, a mobile phone, Spotify's client, or even video game consoles. So, let's dive right into it!
To start off, we are going to create a file called request_type.py in the musicterminal/pytify/core directory with the following contents:
from enum import Enum, auto
class RequestType(Enum):
GET = auto()
PUT = auto()
We have gone through enumerations before, so we won't be going into so much detail. It suffices to say that we create an enumeration with GET and PUT properties. This will be used to notify the function that performs the requests for us that we want to do a GET request or a PUT request.
Then, we can create another file named request.py in the same musicterminal/pytify/core directory, and we start by adding a few import statements and defining a function called execute_request:
import requests
import json
from .exceptions import BadRequestError
from .config import read_config
from .request_type import RequestType
def execute_request(
url_template,
auth,
params,
request_type=RequestType.GET,
payload=()):
This function gets a few arguments:
- url_template: This is the template that will be used to build the URL to perform the request; it will use another argument called params to build the URL
- auth: Is the Authorization object
- params: It is a dict containing all the parameters that will be placed into the URL that we are going to perform the request on
- request: This is the request type; it can be GET or PUT
- payload: This is the data that may be sent together with the request
As we continue to implement the same function, we can add:
conf = read_config()
params['base_url'] = conf.base_url
url = url_template.format(**params)
headers = {
'Authorization': f'Bearer {auth.access_token}'
}
We read the configuration and add the base URL to the params so it is replaced in the url_template string. We add Authorization in the request headers, together with the authentication access token:
if request_type is RequestType.GET:
response = requests.get(url, headers=headers)
else:
response = requests.put(url, headers=headers, data=json.dumps(payload))
if not response.text:
return response.text
result = json.loads(response.text)
Here, we check if the request type is GET. If so, we execute the get function from requests; otherwise, we execute the put function. The function calls are very similar; the only thing that differs here is the data argument. If the response returned is empty, we just return the empty string; otherwise, we parse the JSON data into the result variable:
if not response.ok:
error = result['error']
raise BadRequestError(
f'{error["message"]} (HTTP {error["status"]})')
return result
After parsing the JSON result, we test whether the status of the request is not 200 (OK); in that case, we raise a BadRequestError. If it is a successful response, we return the results.
We also need some functions to help us prepare the parameters that we are going to pass to the Web API endpoints. Let's go ahead and create a file called parameter.py in the musicterminal/pytify/core folder with the following contents:
from urllib.parse import urlencode
def validate_params(params, required=None):
if required is None:
return
partial = {x: x in params.keys() for x in required}
not_supplied = [x for x in partial.keys() if not partial[x]]
if not_supplied:
msg = f'The parameter(s) `{", ".join(not_supplied)}` are
required'
raise AttributeError(msg)
def prepare_params(params, required=None):
if params is None and required is not None:
msg = f'The parameter(s) `{", ".join(required)}` are
required'
raise ValueErrorAttributeError(msg)
elif params is None and required is None:
return ''
else:
validate_params(params, required)
query = urlencode(
'&'.join([f'{key}={value}' for key, value in
params.items()])
)
return f'?{query}'
We have two functions here, prepare_params and validate_params. The validate_params function is used to identify whether there are parameters that are required for a certain operation, but they haven't been supplied. The prepare_params function first calls validate_params to make sure that all the parameters have been supplied and to also join all the parameters together so they can be easily appended to the URL query string.
Now, let's add an enumeration with the types of searches that can be performed. Create a file called search_type.py in the musicterminal/pytify/core directory with the following contents:
from enum import Enum
class SearchType(Enum):
ARTIST = 1
ALBUM = 2
PLAYLIST = 3
TRACK = 4
This is just a simple enumeration with the four search options.
Now, we are ready to create the function to perform the search. Create a file called search.py in the musicterminal/pytify/core directory:
import requests
import json
from urllib.parse import urlencode
from .search_type import SearchType
from pytify.core import read_config
def _search(criteria, auth, search_type):
conf = read_config()
if not criteria:
raise AttributeError('Parameter `criteria` is required.')
q_type = search_type.name.lower()
url = urlencode(f'{conf.base_url}/search?q={criteria}&type=
{q_type}')
headers = {'Authorization': f'Bearer {auth.access_token}'}
response = requests.get(url, headers=headers)
return json.loads(response.text)
def search_artist(criteria, auth):
return _search(criteria, auth, SearchType.ARTIST)
def search_album(criteria, auth):
return _search(criteria, auth, SearchType.ALBUM)
def search_playlist(criteria, auth):
return _search(criteria, auth, SearchType.PLAYLIST)
def search_track(criteria, auth):
return _search(criteria, auth, SearchType.TRACK)
We start by explaining the _search function. This function gets three criteria parameters (what we want to search for), the Authorization object, and lastly the search type, which is a value in the enumeration that we just created.
The function is quite simple; we start by validating the parameters, then we build the URL to make the request, we set the Authorization head using our access token, and lastly, we perform the request and return the parsed response.
The other functions search_artist, search_album, search_playlist, and search_track simply get the same arguments, the criteria and the Authorization object, and pass it to the _search function, but they pass different search types.
Now that we can search for an artist, we have to get a list of albums. Add a file called artist.py in the musicterminal/pytify/core directory with the following contents:
from .parameter import prepare_params
from .request import execute_request
def get_artist_albums(artist_id, auth, params=None):
if artist_id is None or artist_id is "":
raise AttributeError(
'Parameter `artist_id` cannot be `None` or empty.')
url_template = '{base_url}/{area}/{artistid}/{postfix}{query}'
url_params = {
'query': prepare_params(params),
'area': 'artists',
'artistid': artist_id,
'postfix': 'albums',
}
return execute_request(url_template, auth, url_params)
So, given an artist_id, we just define the URL template and parameters that we want to make the request and run the execute_request function which will take care of building the URL, getting and parsing the results for us.
Now, we want to get a list of the tracks for a given album. Add a file called album.py in the musicterminal/pytify/core directory with the following contents:
from .parameters import prepare_params
from .request import execute_request
def get_album_tracks(album_id, auth, params=None):
if album_id is None or album_id is '':
raise AttributeError(
'Parameter `album_id` cannot be `None` or empty.')
url_template = '{base_url}/{area}/{albumid}/{postfix}{query}'
url_params = {
'query': prepare_params(params),
'area': 'albums',
'albumid': album_id,
'postfix': 'tracks',
}
return execute_request(url_template, auth, url_params)
The get_album_tracks function is very similar to the get_artist_albums function that we just implemented.
Finally, we want to be able to send an instruction to Spotify's Web API, telling it to play a track that we selected. Add a file called player.py in the musicterminal/pytify/core directory, and add the following contents:
from .parameter import prepare_params
from .request import execute_request
from .request_type import RequestType
def play(track_uri, auth, params=None):
if track_uri is None or track_uri is '':
raise AttributeError(
'Parameter `track_uri` cannot be `None` or empty.')
url_template = '{base_url}/{area}/{postfix}'
url_params = {
'query': prepare_params(params),
'area': 'me',
'postfix': 'player/play',
}
payload = {
'uris': [track_uri],
'offset': {'uri': track_uri}
}
return execute_request(url_template,
auth,
url_params,
request_type=RequestType.PUT,
payload=payload)
This function is also very similar to the previous ones (get_artist_albums and get_album_tracks), except that it defines a payload. A payload is a dictionary containing two items: uris, which is a list of tracks that should be added to the playback queue, and offset, which contains another dictionary with the URIs of tracks that should be played first. Since we are interested in only playing one song at a time, uris and offset will contain the same track_uri.
The final touch here is to import the new function that we implemented. In the __init__.py file at the musicterminal/pytify/core directory, add the following code:
from .search_type import SearchType
from .search import search_album
from .search import search_artist
from .search import search_playlist
from .search import search_track
from .artist import get_artist_albums
from .album import get_album_tracks
from .player import play
Let's try the function to search artists in the python REPL to check whether everything is working properly:
Python 3.6.2 (default, Dec 22 2017, 15:38:46)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pytify.core import search_artist
>>> from pytify.core import read_config
>>> from pytify.auth import authenticate
>>> from pprint import pprint as pp
>>>
>>> config = read_config()
>>> auth = authenticate(config)
>>> results = search_artist('hot water music', auth)
>>> pp(results)
{'artists': {'href': 'https://api.spotify.com/v1/search?query=hot+water+music&type=artist&market=SE&offset=0&limit=20',
'items': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/4dmaYARGTCpChLhHBdr3ff'},
'followers': {'href': None, 'total': 56497},
'genres': ['alternative emo',
'emo',
'emo punk',
The rest of the output has been omitted because it was too long, but now we can see that everything is working just as expected.
Now, we are ready to start building the terminal player.