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.
Every app that use libspotify needs its own libspotify application key. Application keys can be obtained automatically and free of charge from Spotify.
- Go to the Spotify developer pages and login using your Spotify account.
- Find the libspotify application keys management page and request an application key for your application.
- Once the key is issued, download the “binary” version. The “C code” version of the key will not work with pyspotify.
- 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_keyin 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
>>> 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)
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.
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
>>> 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>
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_IN. If libspotify hasn’t cached any
information about your Spotify user account, the connection state will
probably go directly from
LOGGED_IN. Your application
should be prepared for this.
For more details, see the
We only called
process_events() twice, which may not be
enough to get to the
state. A more robust solution is to call it repeatedly until the
CONNECTION_STATE_UPDATED event is emitted on the
Session object and
>>> 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.
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
spotify to get any log output from pyspotify. The defaults
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: 188.8.131.52: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')
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'
>>> 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>
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)
If you don’t have the URI to a Spotify object, another way to get started is
>>> 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.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
>>> search2 = search.more().load() >>> len(search2.artists) 0 >>> len(search2.tracks) 20 >>> search.track_offset 0 >>> search.tracks Track(u'spotify:track:67Hna13dNDkZvBpTXRIaOJ') >>> search2.track_offset 20 >>> search2.tracks 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.load().name u'X-Mas Allstars' >>> search = session.search('mas', search_type=spotify.SearchType.SUGGEST).load() Search(u'spotify:search:mas') >>> search.artists.load().name u'Massive Attack'
>>> len(session.playlist_container) 53 >>> playlist = session.playlist_container >>> playlist.load() Playlist(u'spotify:user:jodal:playlist:5hBcGwxKlnzNnSrREQ4aUe') >>> playlist.name u'The Glitch Mob - Love Death Immortality'
>>> del session.playlist_container >>> len(session.playlist_container) 52 >>> session.playlist_container.insert(0, playlist) >>> len(session.playlist_container) 53
Playlist objects let you add, remove and move tracks in
a playlist, as well as turning on things like syncing of the playlist for
>>> 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
Music data is delivered to the
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.
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
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.