Quickstart

This guide will quickly introduce you to some of the core features of pyspotify. It assumes that you’ve already installed pyspotify. If you do not, check out Installation. For a complete reference of what pyspotify provides, refer to the API reference.

Application keys

Every app that use libspotify needs its own libspotify application key. Application keys can be obtained automatically and free of charge from Spotify.

  1. Go to the Spotify developer pages and login using your Spotify account.
  2. Find the libspotify application keys management page and request an application key for your application.
  3. Once the key is issued, download the “binary” version. The “C code” version of the key will not work with pyspotify.
  4. If you place the application key in the same directory as your application’s main Python file, pyspotify will automatically find it and use it. If you want to keep the application key in another location, you’ll need to set application_key in your session config or call load_application_key_file() to load the session key file correctly.

Creating a session

Once pyspotify is installed and the application key is in place, we can start writing some Python code. Almost everything in pyspotify requires a Session, so we’ll start with creating a session with the default config:

>>> import spotify
>>> session = spotify.Session()

All config must be done before the session is created. Thus, if you need to change any config to something else than the default, you must create a Config object first, and then pass it to the session:

>>> import spotify
>>> config = spotify.Config()
>>> config.user_agent = 'My awesome Spotify client'
>>> config.tracefile = b'/tmp/libspotify-trace.log'
>>> session = spotify.Session(config)

Text encoding

libspotify encodes all text as UTF-8. pyspotify converts the UTF-8 bytestrings to Unicode strings before returning them to you, so you don’t have to be worried about text encoding.

Similarly, pyspotify will convert any string you give it from Unicode to UTF-8 encoded bytestrings before passing them on to libspotify. The only exception is file system paths, like tracefile above, which is passed directly to libspotify. This is in case you have a file system which doesn’t use UTF-8 encoding for file names.

Login and event processing

With a session we can do a few things, like creating objects from Spotify URIs:

>>> import spotify
>>> session = spotify.Session()
>>> album = session.get_album('spotify:album:0XHpO9qTpqJJQwa2zFxAAE')
>>> album
Album(u'spotify:album:0XHpO9qTpqJJQwa2zFxAAE')
>>> album.link
Link(u'spotify:album:0XHpO9qTpqJJQwa2zFxAAE')
>>> album.link.uri
u'spotify:album:0XHpO9qTpqJJQwa2zFxAAE'

But that’s mostly how far you get with a fresh session. To do more, you need to login to the Spotify service using a Spotify account with the Premium subscription.

Warning

pyspotify and all other libspotify applications required a Spotify Premium subscription.

The Free Spotify subscription, or the old Unlimited subscription, will not work with pyspotify or any other applications using libspotify.

>>> import spotify
>>> session = spotify.Session()
>>> session.login('alice', 's3cretpassword')

For alternative ways to login, refer to the login() documentation.

The login() method is asynchronous, so we must ask the session to process_events() until the login has succeeded or failed:

>>> session.connection.state
<ConnectionState.LOGGED_OUT: 0>
>>> session.process_events()
>>> session.connection.state
<ConnectionState.OFFLINE: 1>
>>> session.process_events()
>>> session.connection.state
<ConnectionState.LOGGED_IN: 1>

Note

The connection state is a representation of both your authentication state and your offline mode. If libspotify has cached your user object from a previous session, it may authenticate you without a connection to Spotify’s servers. Thus, you may very well be logged in, but still offline.

The connection state in the above example goes from the LOGGED_OUT state, to OFFLINE, to LOGGED_IN. If libspotify hasn’t cached any information about your Spotify user account, the connection state will probably go directly from LOGGED_OUT to LOGGED_IN. Your application should be prepared for this.

For more details, see the session.connection.state documentation.

We only called process_events() twice, which may not be enough to get to the LOGGED_IN connection state. A more robust solution is to call it repeatedly until the CONNECTION_STATE_UPDATED event is emitted on the Session object and session.connection.state is LOGGED_IN:

>>> import threading
>>> logged_in_event = threading.Event()
>>> def connection_state_listener(session):
...     if session.connection.state is spotify.ConnectionState.LOGGED_IN:
...         logged_in_event.set()
...
>>> session = spotify.Session()
>>> session.on(
...     spotify.SessionEvent.CONNECTION_STATE_UPDATED,
...     connection_state_listener)
...
>>> session.login('alice', 's3cretpassword')
>>> session.connection.state
<ConnectionState.LOGGED_OUT: 0>
>>> while not logged_in_event.wait(0.1):
...     session.process_events()
...
>>> session.connection.state
<ConnectionState.LOGGED_IN: 1>
>>> session.user
User(u'spotify:user:alice')

This solution works properly, but is a bit tedious. pyspotify provides an EventLoop helper thread that can make the process_events() calls in the background. With it running, we can simplify the login process:

>>> import threading
>>> logged_in_event = threading.Event()
>>> def connection_state_listener(session):
...     if session.connection.state is spotify.ConnectionState.LOGGED_IN:
...         logged_in_event.set()
...
>>> session = spotify.Session()
>>> loop = spotify.EventLoop(session)
>>> loop.start()
>>> session.on(
...     spotify.SessionEvent.CONNECTION_STATE_UPDATED,
...     connection_state_listener)
...
>>> session.connection.state
<ConnectionState.LOGGED_OUT: 0>
>>> session.login('alice', 's3cretpassword')
>>> session.connection.state
<ConnectionState.OFFLINE: 4>
>>> logged_in_event.wait()
>>> session.connection.state
<ConnectionState.LOGGED_IN: 1>
>>> session.user
User(u'spotify:user:alice')

Note that when using EventLoop, your event listener functions are called from the EventLoop thread, and not from your main thread. You may need to add synchronization primitives to protect your application code from threading issues.

Logging

pyspotify uses Python’s standard logging module for logging. All log records emitted by pyspotify are issued to the logger named spotify, or a sublogger of it.

Out of the box, pyspotify is set up with logging.NullHandler as the only log record handler. This is the recommended approach for logging in libraries, so that the application developer using the library will have full control over how the log records from the library will be exposed to the application’s users. In other words, if you want to see the log records from pyspotify anywhere, you need to add a useful handler to the root logger or the logger named spotify to get any log output from pyspotify. The defaults provided by logging.basicConfig() is enough to get debug log statements out of pyspotify:

import logging
logging.basicConfig(level=logging.DEBUG)

If your application is already using logging, and you want debug log output from your own application, but not from pyspotify, you can ignore debug log messages from pyspotify by increasing the threshold on the “spotify” logger to “info” level or higher:

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('spotify').setLevel(logging.INFO)

For more details on how to use logging, please refer to the Python standard library documentation.

If we turn on logging, the login process is a bit more informative:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> import spotify
>>> session = spotify.Session()
>>> session.login('alice', 's3cretpassword')
DEBUG:spotify.session:Notify main thread
DEBUG:spotify.session:Log message from Spotify: 19:15:54.829 I [ap:1752] Connecting to AP ap.spotify.com:4070
DEBUG:spotify.session:Log message from Spotify: 19:15:54.862 I [ap:1226] Connected to AP: 78.31.12.11:4070
>>> session.process_events()
DEBUG:spotify.session:Notify main thread
DEBUG:spotify.session:Log message from Spotify: 19:17:27.972 E [session:926] Not all tracks cached
INFO:spotify.session:Logged in
DEBUG:spotify.session:Credentials blob updated: 'NfFEO...'
DEBUG:spotify.session:Connection state updated
43
>>> session.user
User(u'spotify:user:alice')

Browsing metadata

When we’re logged in, the objects we created from Spotify URIs becomes a lot more interesting:

>>> album = session.get_album('spotify:album:0XHpO9qTpqJJQwa2zFxAAE')

If the object isn’t loaded, you can call load() to block until the object is loaded with data:

>>> album.is_loaded
False
>>> album.name is None
True
>>> album.load()
Album('spotify:album:0XHpO9qTpqJJQwa2zFxAAE')
>>> album.name
u'Reach For Glory'
>>> album.artist
Artist(u'spotify:artist:4kjWnaLfIRcLJ1Dy4Wr6tY')
>>> album.artist.load().name
u'Blackmill'

The Album object give you the most basic information about an album. For more metadata, you can call browse() to get an AlbumBrowser:

>>> browser = album.browse()

The browser also needs to load data, but once its loaded, most related objects are in place with data as well:

>>> browser.load()
AlbumBrowser(u'spotify:album:0XHpO9qTpqJJQwa2zFxAAE')
>>> browser.copyrights
[u'2011 Blackmill']
>>> browser.tracks
[Track(u'spotify:track:4FXj4ZKMO2dSkqiAhV7L8t'),
 Track(u'spotify:track:1sYClIlZZsL6dVMVTxCYRm'),
 Track(u'spotify:track:1uY4O332HuqLIcSSJlg4NX'),
 Track(u'spotify:track:58qbTrCRGyjF9tnjvHDqAD'),
 Track(u'spotify:track:3RZzg8yZs5HaRjQiDiBIsV'),
 Track(u'spotify:track:4jIzCryeLdBgE671gdQ6QD'),
 Track(u'spotify:track:4JNpKcFjVFYIzt1D95dmi0'),
 Track(u'spotify:track:7wAtUSgh6wN5ZmuPRRXHyL'),
 Track(u'spotify:track:7HYOVVLd5XnfY4yyV5Neke'),
 Track(u'spotify:track:2YfVXi6dTux0x8KkWeZdd3'),
 Track(u'spotify:track:6HPKugiH3p0pUJBNgUQoou')]
>>> [(t.index, t.name, t.duration // 1000) for t in browser.tracks]
[(1, u'Evil Beauty', 228),
 (2, u'City Lights', 299),
 (3, u'A Reach For Glory', 254),
 (4, u'Relentless', 194),
 (5, u'In The Night Of Wilderness', 327),
 (6, u"Journey's End", 296),
 (7, u'Oh Miah', 333),
 (8, u'Flesh and Bones', 276),
 (9, u'Sacred River', 266),
 (10, u'Rain', 359),
 (11, u'As Time Goes By', 97)]

Downloading cover art

While we’re at it, let’s do something a bit more impressive; getting cover art:

>>> cover = album.cover(spotify.ImageSize.LARGE)
>>> cover.load()
Image(u'spotify:image:16eaba4959d5d97e8c0ca04289e0b1baaefae55f')

Currently, all covers are in JPEG format:

>>> cover.format
<ImageFormat.JPEG: 0>

The Image object gives access to the raw JPEG data:

>>> len(cover.data)
37204
>>> cover.data[:20]
'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00'

For convenience, it also provides the same data encoded as a data: URI for easy embedding into HTML documents:

>>> len(cover.data_uri)
49631
>>> cover.data_uri[:60]
u'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCA'

If you’re following along, you can try writing the image data out to files and inspect the result yourself:

>>> open('/tmp/cover.jpg', 'w+').write(cover.data)
>>> open('/tmp/cover.html', 'w+').write('<img src="%s">' % cover.data_uri)

Searching

If you don’t have the URI to a Spotify object, another way to get started is to search():

>>> search = session.search('massive attack')
>>> search.load()
Search(u'spotify:search:massive+attack')

A search returns lists of matching artists, albums, tracks, and playlists:

>>> (search.artist_total, search.album_total, search.track_total, track.playlist_total)
(5, 50, 564, 125)
>>> search.artists[0].load().name
u'Massive Attack'
>>> [a.load().name for a in search.artists[:3]]
[u'Massive Attack',
 u'Kwanzaa Posse feat. Massive Attack',
 u'Massive Attack Vs. Mad Professor']

Only the first 20 items in each list are returned by default:

>>> len(search.artists)
5
>>> len(search.tracks)
20

The Search object can help you with getting more() results from the same query:

>>> search2 = search.more().load()
>>> len(search2.artists)
0
>>> len(search2.tracks)
20
>>> search.track_offset
0
>>> search.tracks[0]
Track(u'spotify:track:67Hna13dNDkZvBpTXRIaOJ')
>>> search2.track_offset
20
>>> search2.tracks[0]
Track(u'spotify:track:3kKVqFF4pv4EXeQe428zl2')

You can also do searches where Spotify tries to figure out what you mean based on popularity, etc. instead of exact token matches:

>>> search = session.search('mas').load()
Search(u'spotify:search:mas')
>>> search.artists[0].load().name
u'X-Mas Allstars'

>>> search = session.search('mas', search_type=spotify.SearchType.SUGGEST).load()
Search(u'spotify:search:mas')
>>> search.artists[0].load().name
u'Massive Attack'

Playlist management

Another way to find some music is to use your Spotify Playlist, which can be found in playlist_container:

>>> len(session.playlist_container)
53
>>> playlist = session.playlist_container[0]
>>> playlist.load()
Playlist(u'spotify:user:jodal:playlist:5hBcGwxKlnzNnSrREQ4aUe')
>>> playlist.name
u'The Glitch Mob - Love Death Immortality'

The PlaylistContainer object lets you add, remove, move and rename playlists as well as playlist folders. See the API docs for PlaylistContainer for more examples.

>>> del session.playlist_container[0]
>>> len(session.playlist_container)
52
>>> session.playlist_container.insert(0, playlist)
>>> len(session.playlist_container)
53

The Playlist objects let you add, remove and move tracks in a playlist, as well as turning on things like syncing of the playlist for offline playback:

>>> playlist.offline_status
<PlaylistOfflineStatus.NO: 0>
>>> playlist.set_offline_mode(True)
>>> playlist.offline_status
<PlaylistOfflineStatus.WAITING: 3>
>>> session.process_events()
# Probably needed multiple times, before syncing begins
>>> playlist.offline_status
<PlaylistOfflineStatus.DOWNLOADING: 2>
>>> playlist.offline_download_completed
20
# More process_events()
>>> playlist.offline_status
<PlaylistOfflineStatus.YES: 1>

For more details, see the API docs for Playlist.

Playing music

Music data is delivered to the MUSIC_DELIVERY event listener as PCM frames. If you want to have full control of audio playback, you can deliver these audio frames to your operating systems’ audio subsystem yourself. If you want some help on the road, pyspotify comes with audio sinks for some select audio subsystems.

For details, have a look at the spotify.AlsaSink and spotify.PortAudioSink documentation, and the examples/play_track.py and examples/shell.py examples.

Thread safety

If you’ve read the libspotify documentation, you may have noticed that libspotify itself isn’t thread safe. This means that you must take care to never call libspotify functions from two threads at the same time, and to finish your work with any pointers, e.g. strings, returned by libspotify functions before calling the next libspotify function. In summary, you’ll need to use a single thread for all your use of libspotify, or protect all libspotify function calls with a single lock.

pyspotify, on the other hand, improves on this so that you can use pyspotify from multiple threads. pyspotify has a single global lock. This lock is acquired during all calls to libspotify, for as long as we’re working with pointers returned from libspotify functions, and during all access to pyspotify’s own internal state, like for example the collections of event listeners. In other words, pyspotify should be safe to use from multiple threads simultaneously.

Even though pyspotify itself is thread safe, you cannot disregard threading issues entirely when using pyspotify. There’s two things to watch out for. First, event listeners for a number of the events listed in SessionEvent will be called from internal threads in libspotify itself. This is clearly marked in the documentation for the relevant events. Second, if you use the EventLoop helper thread, listeners for all other events—that is, events not emitted from internal threads in libspotify—will be called from the EventLoop thread. This shouldn’t be an issue if you just use pyspotify itself from within the event listeners, but the moment you start working with your application’s state from inside event listeners, you’ll need to apply the proper thread synchronization primitives to avoid getting into trouble.