Slide 1

Slide 1 text

July 4, 2013 EuroPython 2013 Michael Fötsch – @mfoetsch Adding Music to Python

Slide 2

Slide 2 text

2 Section name Libspotify Echo Nest Last.fm Music APIs

Slide 3

Slide 3 text

Libspotify

Slide 4

Slide 4 text

Section name 4 What is Spotify? The right music for every moment. ● Over 6 million paying subscribers ● Over 24 million active users ● Over 20 million songs ● Over 20,000 songs added per day ● Over 1 billion playlists created ● Available in 28 countries

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

6 developer.spotify.com

Slide 7

Slide 7 text

7

Slide 8

Slide 8 text

8 Section name Can be used to build a full-featured Spotify client ● Multi-platform C/C++ API ● High-quality streaming ● Playlist management ● Searching ● Artist, album, track metadata ● Facebook / Last.fm scrobbling Libspotify Features

Slide 9

Slide 9 text

Section name 9 Limitations/ Shortcomings ● C/C++ API ● No support for in-client apps ● No support for latest social and Discovery features ● No Radio support

Slide 10

Slide 10 text

10 Premium Account + App Key

Slide 11

Slide 11 text

11 Windows, OS X, iOS, Linux (x86, x86_64, ARM)

Slide 12

Slide 12 text

12 github.com/mopidy/pyspotify

Slide 13

Slide 13 text

13 SpotifySessionManager import spotify.manager class SessionManager(spotify.manager.SpotifySessionManager): def logged_in(self, session, error): print “User”, session.display_name(), “has been logged in” sess = SessionManager(username, password) sess.connect()

Slide 14

Slide 14 text

14 SpotifySessionManager import spotify.manager class SessionManager(spotify.manager.SpotifySessionManager): def logged_in(self, session, error): print “User”, session.display_name(), “has been logged in” sess = SessionManager(username, password) sess.connect() ß Does not return until logged out. Log out by calling disconnect().

Slide 15

Slide 15 text

15 Section name ● Logged in / logged out ● Metadata updates ● Connection errors ● Music delivery Session Manager Callbacks Section name

Slide 16

Slide 16 text

16 Example: Loading User’s Playlists class SessionManager(spotify.manager.SpotifySessionManager, spotify.manager.SpotifyContainerManager): def logged_in(self, session, error): self.watch(session.playlist_container()) def container_loaded(self, playlist_container, userdata): for item in playlist_container: if item.type() == “playlist” and item.is_loaded(): print item.name()

Slide 17

Slide 17 text

17 if playlist.is_loaded() ● Asynchronous loading ● Callback-based notifications

Slide 18

Slide 18 text

18 Example: Loading User’s Playlists def container_loaded(self, playlist_container, userdata): … if item.type() == “playlist” and not item.is_loaded(): item.add_playlist_state_changed_callback( self.playlist_state_changed) def playlist_state_changed(self, playlist, userdata): if playlist.is_loaded(): print playlist.name(), “(%d tracks)” % len(playlist) for track in playlist: print track.name(), “-”, track.artists()[0].name()

Slide 19

Slide 19 text

19 Playlist Folders def container_loaded(self, playlist_container, userdata): for item in playlist_container: if item.type() == “playlist”: … elif item.type() == “folder_start”: # Playlists between now and “folder_end” go in this folder … elif item.type() == “folder_end”: # Back in parent folder …

Slide 20

Slide 20 text

20

Slide 21

Slide 21 text

21 Example: Searching ● Paged results (offset / limit) ● Artists, albums, tracks, playlists ● Search term suggestions (“Did you mean …?”)

Slide 22

Slide 22 text

22 Example: Searching class SessionManager(spotify.manager.SpotifySessionManager): def logged_in(self, session, error): session.search(“ABBA”, self.results_loaded, track_offset=0, track_count=5, album_offset=0, album_count=5, artist_offset=0, artist_count=5) def results_loaded(self, results, userdata): print results.total_albums(), “album results in total” print “First”, len(results.albums()), “albums:” for album in results.albums(): print album.name()

Slide 23

Slide 23 text

23

Slide 24

Slide 24 text

24 Example: Playback ● music_delivery callback delivers PCM audio samples ● spotify.audiosink wraps ALSA, OSS, PortAudio, Gstreamer

Slide 25

Slide 25 text

25 Example: Playback from spotify.audiosink import import_audio_sink AudioSink = import_audio_sink() class SessionManager(spotify.manager.SpotifySessionManager): def load_track(self, track): self.session.load(track) self.audio = AudioSink(backend=self) self.audio.start() self.session.play(1) def music_delivery_safe(self, *args, **kwargs): self.audio.music_delivery(*args, **kwargs)

Slide 26

Slide 26 text

26 Example: Playback from spotify.audiosink import import_audio_sink AudioSink = import_audio_sink() class SessionManager(spotify.manager.SpotifySessionManager): def load_track(self, track): self.session.load(track) ß track must be loaded self.audio = AudioSink(backend=self) self.audio.start() self.session.play(1) def music_delivery_safe(self, *args, **kwargs): self.audio.music_delivery(*args, **kwargs)

Slide 27

Slide 27 text

27 Example: Loading a track def load_track(self, track): while not track.is_loaded(): time.sleep(0.1) if track.is_autolinked(): track = self.load_track(track.playable()) if track.availability() != 1: print "Track not available (%s)" % track.availability() # track is loaded self.session.load(track) self.audio.start() self.session.play(1)

Slide 28

Slide 28 text

Section name 28 Playback Controls ● Play / pause ● Seek ● Bitrate (96 / 160 / 320 kbps)

Slide 29

Slide 29 text

29 Example: Spotify URIs

Slide 30

Slide 30 text

30 Example: Spotify URIs import spotify … uri = “spotify:track:7FgmnYKW9O87uvMoE2ouTF” track_link = spotify.Link.from_string(uri) if track_link.type() == spotify.LINK_TRACK: assert uri == str(spotify.Link.from_track(track)) track = track_link.as_track()

Slide 31

Slide 31 text

Echo Nest

Slide 32

Slide 32 text

32 The Echo Nest •  “The world’s leading music intelligence company” •  Over a trillion data points on over 30 million songs

Slide 33

Slide 33 text

Section name 33 Echo Nest API ● HTTP + JSON/XML ● Artist profiles, bios, reviews, etc. ● Similar songs, artists ● Song tempo, mood, genre, etc. ● …

Slide 34

Slide 34 text

34 developer.echonest.com github.com/echonest/pyechonest Requires an API key

Slide 35

Slide 35 text

35 Example: Searching Songs from pyechonest import song songs = song.search(min_tempo=120, max_tempo=160, artist_start_year_after=1980, artist_end_year_before=2000, min_danceability=0.6, artist_min_familiarity=0.5, buckets=[“id:spotify-WW”, “tracks”]) for s in songs: print s.title, "by", s.artist_name for track in s.get_tracks("spotify-WW"): uri = track["foreign_id"].replace("spotify-WW", "spotify") print " ", uri

Slide 36

Slide 36 text

36 Example: Searching Songs from pyechonest import song songs = song.search(min_tempo=120, max_tempo=160, artist_start_year_after=1980, artist_end_year_before=2000, min_danceability=0.6, artist_min_familiarity=0.5, buckets=[“id:spotify-WW”, “tracks”]) ß speedup for s in songs: print s.title, "by", s.artist_name for track in s.get_tracks("spotify-WW"): uri = track["foreign_id"].replace("spotify-WW", "spotify") print " ", uri

Slide 37

Slide 37 text

37

Slide 38

Slide 38 text

38 Song Attributes ● Title ● Artist ● Genre ● Mood ● Live / Studio ● Tempo ● Duration ● Loudness ● Artist familiarity ● Time period ● Region ● Danceability ● Energy ● Speechiness ● Acousticness ● Musical key (c, c-sharp, d, e-flat, …)

Slide 39

Slide 39 text

39 Example: Artist biographies and reviews from pyechonest import artist a = artist.Artist("spotify-WW:artist:0LcJLqbBmaGUft1e9Mm8HV") for bio in a.biographies: print bio["text"] print "-", bio["site"], "<%s>" % bio["url”] for review in a.reviews: print "Review:", review["name"] print review["summary"] print "-", review["url”]

Slide 40

Slide 40 text

40

Slide 41

Slide 41 text

41 Example: Similar artists from pyechonest import artist artists = [artist.Artist("spotify-WW:artist:2SHhfs4BiDxGQ3oxqf0UHY"), artist.Artist("spotify-WW:artist:0LcJLqbBmaGUft1e9Mm8HV")] print "Artists similar to", artists[0].name, “and”, artists[1].name similars = artist.similar(ids=[a.id for a in artists], buckets=["id:spotify-WW"]) for a in similars: uri = a.get_foreign_id("spotify-WW").replace("spotify-WW", "spotify”) print "-", a.name, "(%s)" % uri

Slide 42

Slide 42 text

42

Slide 43

Slide 43 text

43 Collection of songs or artists to personalize recommendations ● Similar tracks/artists radio stations ● Event recommendations ● Thumbs up/down and ratings Example: Taste Profiles a.k.a. Catalogs

Slide 44

Slide 44 text

44 Section name 1.  Create a catalog (up to 1,000 per API key) 2.  Add songs/artists 3.  Use catalog as a seed In queries 4.  Refine catalog (update, favorite, ban) Catalogs: Steps Section name

Slide 45

Slide 45 text

45 Example: Create a catalog from pyechonest import catalog, track cat = catalog.Catalog(“My taste profile”, "song") uris = [“spotify-WW:track:5xtwX31B89apd0farwIgir”, …] update_items = [] for uri in uris: t = track.track_from_id() update_items.append( {"action": "update", "item": {"item_id": uri.split(":") [-1], "song_id”: t.song_id}}) ticket = cat.update(update_items) status = cat.status(ticket)["ticket_status"] # wait for status to be “complete”

Slide 46

Slide 46 text

46 Example: Create a catalog radio from pyechonest import playlist pl = playlist.static(type="catalog-radio", seed_catalog=cat, buckets=["id:spotify-WW", "tracks"]) for s in pl: print s.title, “by”, s.artist_name for t in s.get_tracks("spotify-WW"): print " ", t["foreign_id"].replace("spotify-WW", "spotify") break # print only the first Spotify track URI

Slide 47

Slide 47 text

47

Slide 48

Slide 48 text

48 Example: Refine catalog # items must be in the catalog already items = [“spotify-WW:track:5xtwX31B89apd0farwIgir”] # When an item is played… cat.play(items, plays=1) # When an item is skipped… cat.skip(items, skips=1) # When an item is favorited… cat.favorite(items, favorite=True) # When an item is banned… cat.ban(items, ban=True) # When an item is rated… cat.rate(items, rate=9)

Slide 49

Slide 49 text

49 ● Tempo, bars, beats ● Fade-in time/fade-out time ● … * Not available for all tracks Audio analysis*

Slide 50

Slide 50 text

50 Example: Audio analysis from pyechonest import track t = track.track_from_id(“spotify:track:2uYupCnY6uQwyNuIVvgKsX”) t.get_analysis() print t.duration print t.end_of_fade_in print t.start_of_fade_out print t.bars[:10] print t.beats[:10] print t.tempo print t.danceability …

Slide 51

Slide 51 text

51 Echo Nest Remix

Slide 52

Slide 52 text

52

Slide 53

Slide 53 text

53

Slide 54

Slide 54 text

Last.fm

Slide 55

Slide 55 text

55 ● Scrobbling ● User profiles ● Recommendations Last.fm

Slide 56

Slide 56 text

56 last.fm/api code.google.com/p/pylast/

Slide 57

Slide 57 text

57 Example: “Now Playing” import pylast password_hash = pylast.md5(password) network = pylast.LastFMNetwork(api_key=API_KEY, api_secret=API_SECRET, username=username, password_hash=password_hash) network.update_now_playing(artist_name, track_title) network.scrobble(artist_name, track_title, time.time())

Slide 58

Slide 58 text

58 Example: “Now Playing”

Slide 59

Slide 59 text

59 Example: Mapping Spotify URIs to Last.fm tracks import pyechonest.song import pyechonest.track … t = pyechonest.track.track_from_id(spotify_track_uri) s = pyechonest_song.Song(t.song_id, buckets=["id:musicbrainz", "tracks"]) for t in s.get_tracks("musicbrainz"): foreign_id = t["foreign_id"].replace("musicbrainz:track:", "") try: lastfm_track = lastfm_network.get_track_by_mbid(foreign_id) except pylast.WSError: pass else: return lastfm_track else: return pylast.Track(s.artist_name, s.title, lastfm_network)

Slide 60

Slide 60 text

60 Example: User’s library

Slide 61

Slide 61 text

61 Example: User’s library print "User's artists:” user = network.get_authenticated_user() library = user.get_library() for item in library.get_artists(limit=5): print item.item, "(played %d times)" % item.playcount score, shared_artists = user.compare_with_user("averagemike") print 'Similarity to user "averagemike":', score print "Shared artists:", map(str, shared_artists)

Slide 62

Slide 62 text

62

Slide 63

Slide 63 text

July 4, 2013 Check out spotify.com/jobs or @Spotifyjobs for more information. Want to join the band?

Slide 64

Slide 64 text

64 realmike.org @mfoetsch