class SpotifyPlugin(object): def __init__(self): self.client = None self.server = None self.play_lock = Semaphore(1) self.start_lock = Semaphore(1) self.start_marker = Event() self.last_track_uri = None self.last_track_object = None Dict.Reset() Dict['play_count'] = 0 Dict['last_restart'] = 0 Dict['schedule_restart_each'] = 5 * 60 # restart each X minutes Dict['play_restart_each'] = 2 # restart each X plays Dict[ 'check_restart_each'] = 5 # check if I should restart each X seconds Dict[ 'radio_salt'] = False # Saves last radio salt so multiple queries return the same radio track list self.start() self.session = requests.session() self.session_cached = CacheControl(self.session) Thread.CreateTimer(Dict['check_restart_each'], self.check_automatic_restart, globalize=True) @property def username(self): return Prefs["username"] @property def password(self): return Prefs["password"] def check_automatic_restart(self): can_restart = False try: diff = time.time() - Dict['last_restart'] scheduled_restart = diff >= Dict['schedule_restart_each'] play_count_restart = Dict['play_count'] >= Dict['play_restart_each'] must_restart = play_count_restart or scheduled_restart if must_restart: can_restart = self.play_lock.acquire(blocking=False) if can_restart: Log.Debug('Automatic restart started') self.start() Log.Debug('Automatic restart finished') finally: if can_restart: self.play_lock.release() Thread.CreateTimer(Dict['check_restart_each'], self.check_automatic_restart, globalize=True) @check_restart def preferences_updated(self): """ Called when the user updates the plugin preferences""" self.start() # Trigger a client restart def start(self): """ Start the Spotify client and HTTP server """ if not self.username or not self.password: Log("Username or password not set: not logging in") return False can_start = self.start_lock.acquire(blocking=False) try: # If there is a start in process, just wait until it finishes, but don't raise another one if not can_start: Log.Debug( "Start already in progress, waiting it finishes to return") self.start_lock.acquire() else: Log.Debug("Start triggered, entering private section") self.start_marker.clear() if self.client: self.client.restart(self.username, self.password) else: self.client = SpotifyClient(self.username, self.password) self.last_track_uri = None self.last_track_object = None Dict['play_count'] = 0 Dict['last_restart'] = time.time() self.start_marker.set() Log.Debug("Start finished, leaving private section") finally: self.start_lock.release() return self.client and self.client.is_logged_in() @check_restart def play(self, uri): Log('play(%s)' % repr(uri)) uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") track_url = None if not self.client.is_track_uri_valid(uri): Log("Play track callback invoked with invalid URI (%s). This is very bad :-(" % uri) track_url = "http://www.xamuel.com/blank-mp3-files/2sec.mp3" else: self.play_lock.acquire(blocking=True) try: track_url = self.get_track_url(uri) # If first request failed, trigger re-connection to spotify retry_num = 0 while not track_url and retry_num < 2: Log.Info( 'get_track_url (%s) failed, re-connecting to spotify...' % uri) time.sleep( retry_num * 0.5) # Wait some time based on number of failures if self.start(): track_url = self.get_track_url(uri) retry_num = retry_num + 1 if track_url == False or track_url is None: # Send an empty and short mp3 so player do not fail and we can go on listening next song Log.Error( "Play track (%s) couldn't be obtained. This is very bad :-(" % uri) track_url = 'http://www.xamuel.com/blank-mp3-files/2sec.mp3' elif retry_num == 0: # If I didn't restart, add 1 to playcount Dict['play_count'] = Dict['play_count'] + 1 finally: self.play_lock.release() return Redirect(track_url) def get_track_url(self, track_uri): if not self.client.is_track_uri_valid(track_uri): return None track_url = None track = self.client.get(track_uri) if track: track_url = track.getFileURL(urlOnly=True, retries=1) return track_url # # TRACK DETAIL # @check_restart def metadata(self, uri): Log('metadata(%s)' % repr(uri)) uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") oc = ObjectContainer() track_object = None if not self.client.is_track_uri_valid(uri): Log("Metadata callback invoked with invalid URI (%s)" % uri) track_object = self.create_track_object_empty(uri) else: if self.last_track_uri == uri: track_object = self.last_track_object else: track_metadata = self.get_track_metadata(uri) if track_metadata: track_object = self.create_track_object_from_metatada( track_metadata) self.last_track_uri = uri self.last_track_object = track_object else: track_object = self.create_track_object_empty(uri) oc.add(track_object) return oc def get_track_metadata(self, track_uri): if not self.client.is_track_uri_valid(track_uri): return None track = self.client.get(track_uri) if not track: return None #track_uri = track.getURI().decode("utf-8") title = track.getName().decode("utf-8") image_url = self.select_image(track.getAlbumCovers()) track_duration = int(track.getDuration()) track_number = int(track.getNumber()) track_album = track.getAlbum(nameOnly=True).decode("utf-8") track_artists = track.getArtists(nameOnly=True).decode("utf-8") metadata = TrackMetadata(title, image_url, track_uri, track_duration, track_number, track_album, track_artists) return metadata @staticmethod def select_image(images): if images == None: return None if images.get(640): return images[640] elif images.get(320): return images[320] elif images.get(300): return images[300] elif images.get(160): return images[160] elif images.get(60): return images[60] Log.Info('Unable to select image, available sizes: %s' % images.keys()) return None def get_uri_image(self, uri): images = None obj = self.client.get(uri) if isinstance(obj, SpotifyArtist): images = obj.getPortraits() elif isinstance(obj, SpotifyAlbum): images = obj.getCovers() elif isinstance(obj, SpotifyTrack): images = obj.getAlbum().getCovers() elif isinstance(obj, SpotifyPlaylist): images = obj.getImages() return self.select_image(images) @authenticated @check_restart def image(self, uri): if not uri: # TODO media specific placeholders return Redirect(R('placeholder-artist.png')) Log.Debug('Getting image for: %s' % uri) uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") if uri.startswith('spotify:'): # Fetch object for spotify URI and select image image_url = self.get_uri_image(uri) if not image_url: # TODO media specific placeholders return Redirect(R('placeholder-artist.png')) else: # pre-selected image provided Log.Debug('Using pre-selected image URL: "%s"' % uri) image_url = uri return self.session_cached.get(image_url).content # # SECOND_LEVEL_MENU # @authenticated @check_restart def explore(self): Log("explore") """ Explore shared music """ return ObjectContainer(objects=[ DirectoryObject(key=route_path('explore/featured_playlists'), title=L("MENU_FEATURED_PLAYLISTS"), thumb=R("icon-explore-featuredplaylists.png")), DirectoryObject(key=route_path('explore/top_playlists'), title=L("MENU_TOP_PLAYLISTS"), thumb=R("icon-explore-topplaylists.png")), DirectoryObject(key=route_path('explore/new_releases'), title=L("MENU_NEW_RELEASES"), thumb=R("icon-explore-newreleases.png")), DirectoryObject(key=route_path('explore/genres'), title=L("MENU_GENRES"), thumb=R("icon-explore-genres.png")) ], ) @authenticated @check_restart def discover(self): Log("discover") oc = ObjectContainer(title2=L("MENU_DISCOVER"), view_group=ViewMode.Stories) stories = self.client.discover() for story in stories: self.add_story_to_directory(story, oc) return oc @authenticated @check_restart def radio(self): Log("radio") """ Show radio options """ return ObjectContainer(objects=[ DirectoryObject(key=route_path('radio/stations'), title=L("MENU_RADIO_STATIONS"), thumb=R("icon-radio-stations.png")), DirectoryObject(key=route_path('radio/genres'), title=L("MENU_RADIO_GENRES"), thumb=R("icon-radio-genres.png")) ], ) @authenticated @check_restart def your_music(self): Log("your_music") """ Explore your music """ return ObjectContainer(objects=[ DirectoryObject(key=route_path('your_music/playlists'), title=L("MENU_PLAYLISTS"), thumb=R("icon-playlists.png")), DirectoryObject(key=route_path('your_music/starred'), title=L("MENU_STARRED"), thumb=R("icon-starred.png")), DirectoryObject(key=route_path('your_music/albums'), title=L("MENU_ALBUMS"), thumb=R("icon-albums.png")), DirectoryObject(key=route_path('your_music/artists'), title=L("MENU_ARTISTS"), thumb=R("icon-artists.png")), ], ) # # EXPLORE # @authenticated @check_restart def featured_playlists(self): Log("featured playlists") oc = ObjectContainer(title2=L("MENU_FEATURED_PLAYLISTS"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists) playlists = self.client.get_featured_playlists() for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc @authenticated @check_restart def top_playlists(self): Log("top playlists") oc = ObjectContainer(title2=L("MENU_TOP_PLAYLISTS"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists) playlists = self.client.get_top_playlists() for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc @authenticated @check_restart def new_releases(self): Log("new releases") oc = ObjectContainer(title2=L("MENU_NEW_RELEASES"), content=ContainerContent.Albums, view_group=ViewMode.Albums) albums = self.client.get_new_releases() for album in albums: self.add_album_to_directory(album, oc) return oc @authenticated @check_restart def genres(self): Log("genres") oc = ObjectContainer(title2=L("MENU_GENRES"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists) genres = self.client.get_genres() for genre in genres: self.add_genre_to_directory(genre, oc) return oc @authenticated @check_restart def genre_playlists(self, genre_name): Log("genre playlists") oc = ObjectContainer(title2=genre_name, content=ContainerContent.Playlists, view_group=ViewMode.Playlists) playlists = self.client.get_playlists_by_genre(genre_name) for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc # # RADIO # @authenticated @check_restart def radio_stations(self): Log('radio stations') Dict['radio_salt'] = False oc = ObjectContainer(title2=L("MENU_RADIO_STATIONS")) stations = self.client.get_radio_stations() for station in stations: oc.add( PopupDirectoryObject( key=route_path('radio/stations/' + station.getURI()), title=station.getTitle(), thumb=function_path('image.png', uri=self.select_image( station.getImages())))) return oc @authenticated @check_restart def radio_genres(self): Log('radio genres') Dict['radio_salt'] = False oc = ObjectContainer(title2=L("MENU_RADIO_GENRES")) genres = self.client.get_radio_genres() for genre in genres: oc.add( PopupDirectoryObject( key=route_path('radio/genres/' + genre.getURI()), title=genre.getTitle(), thumb=function_path('image.png', uri=self.select_image( genre.getImages())))) return oc @authenticated @check_restart def radio_track_num(self, uri): Log('radio track num') uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") return ObjectContainer( title2=L("MENU_RADIO_TRACK_NUM"), objects=[ DirectoryObject(key=route_path('radio/play/' + uri + '/10'), title=localized_format("MENU_TRACK_NUM", "10"), thumb=R("icon-radio-item.png")), DirectoryObject(key=route_path('radio/play/' + uri + '/20'), title=localized_format("MENU_TRACK_NUM", "20"), thumb=R("icon-radio-item.png")), DirectoryObject(key=route_path('radio/play/' + uri + '/50'), title=localized_format("MENU_TRACK_NUM", "50"), thumb=R("icon-radio-item.png")), DirectoryObject(key=route_path('radio/play/' + uri + '/80'), title=localized_format("MENU_TRACK_NUM", "80"), thumb=R("icon-radio-item.png")), DirectoryObject(key=route_path('radio/play/' + uri + '/100'), title=localized_format("MENU_TRACK_NUM", "100"), thumb=R("icon-radio-item.png")) ], ) @authenticated @check_restart def radio_tracks(self, uri, num_tracks): Log('radio tracks') uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") oc = None radio = self.client.get_radio(uri) if not Dict['radio_salt']: Dict['radio_salt'] = radio.generateSalt() salt = Dict['radio_salt'] tracks = radio.getTracks(salt=salt, num_tracks=int(num_tracks)) oc = ObjectContainer(title2=radio.getTitle().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks) for track in tracks: self.add_track_to_directory(track, oc) return oc # # YOUR_MUSIC # @authenticated @check_restart def playlists(self): Log("playlists") oc = ObjectContainer(title2=L("MENU_PLAYLISTS"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists) playlists = self.client.get_playlists() for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc @authenticated @check_restart def starred(self): Log("starred") oc = ObjectContainer(title2=L("MENU_STARRED"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks) starred = self.client.get_starred() for x, track in enumerate(starred.getTracks()): self.add_track_to_directory(track, oc, index=x) return oc @authenticated @check_restart def albums(self): Log("albums") oc = ObjectContainer(title2=L("MENU_ALBUMS"), content=ContainerContent.Albums, view_group=ViewMode.Albums) albums = self.client.get_my_albums() for album in albums: self.add_album_to_directory(album, oc) return oc @authenticated @check_restart def artists(self): Log("artists") oc = ObjectContainer(title2=L("MENU_ARTISTS"), content=ContainerContent.Artists, view_group=ViewMode.Artists) artists = self.client.get_my_artists() for artist in artists: self.add_artist_to_directory(artist, oc) return oc # # ARTIST DETAIL # @authenticated @check_restart def artist(self, uri): Log("artist") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") artist = self.client.get(uri) return ObjectContainer( title2=artist.getName().decode("utf-8"), objects=[ DirectoryObject(key=route_path('artist/%s/top_tracks' % uri), title=L("MENU_TOP_TRACKS"), thumb=R("icon-artist-toptracks.png")), DirectoryObject(key=route_path('artist/%s/albums' % uri), title=L("MENU_ALBUMS"), thumb=R("icon-albums.png")), DirectoryObject(key=route_path('artist/%s/related' % uri), title=L("MENU_RELATED"), thumb=R("icon-artist-related.png")), DirectoryObject(key=route_path('radio/stations/' + uri), title=L("MENU_RADIO"), thumb=R("icon-radio-custom.png")) ], ) @authenticated @check_restart def artist_albums(self, uri): Log("artist_albums") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") artist = self.client.get(uri) oc = ObjectContainer(title2=artist.getName().decode("utf-8"), content=ContainerContent.Albums) for album in artist.getAlbums(): self.add_album_to_directory(album, oc) return oc @authenticated @check_restart def artist_top_tracks(self, uri): Log("artist_top_tracks") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") oc = None artist = self.client.get(uri) top_tracks = artist.getTracks() if top_tracks: oc = ObjectContainer(title2=artist.getName().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks) for track in artist.getTracks(): self.add_track_to_directory(track, oc) else: oc = MessageContainer(header=L("MSG_TITLE_NO_RESULTS"), message=localized_format( "MSG_FMT_NO_RESULTS", artist.getName().decode("utf-8"))) return oc @authenticated @check_restart def artist_related(self, uri): Log("artist_related") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") artist = self.client.get(uri) oc = ObjectContainer(title2=localized_format( "MSG_RELATED_TO", artist.getName().decode("utf-8")), content=ContainerContent.Artists) for artist in artist.getRelatedArtists(): self.add_artist_to_directory(artist, oc) return oc # # ALBUM DETAIL # @authenticated @check_restart def album(self, uri): Log("album") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") album = self.client.get(uri) oc = ObjectContainer(title2=album.getName().decode("utf-8"), content=ContainerContent.Artists) oc.add( DirectoryObject(key=route_path('album/%s/tracks' % uri), title=L("MENU_ALBUM_TRACKS"), thumb=R("icon-album-tracks.png"))) artists = album.getArtists() for artist in artists: self.add_artist_to_directory(artist, oc) oc.add( DirectoryObject(key=route_path('radio/stations/' + uri), title=L("MENU_RADIO"), thumb=R("icon-radio-custom.png"))) return oc @authenticated @check_restart def album_tracks(self, uri): Log("album_tracks") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") album = self.client.get(uri) oc = ObjectContainer(title2=album.getName().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks) for track in album.getTracks(): self.add_track_to_directory(track, oc) return oc # # PLAYLIST DETAIL # @authenticated @check_restart def playlist(self, uri): Log("playlist") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") pl = self.client.get(uri) if pl is None: # Unable to find playlist return MessageContainer(header=L("MSG_TITLE_UNKNOWN_PLAYLIST"), message='URI: %s' % uri) Log("Get playlist: %s", pl.getName().decode("utf-8")) Log.Debug('playlist truncated: %s', pl.obj.contents.truncated) oc = ObjectContainer(title2=pl.getName().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks, mixed_parents=True) for x, track in enumerate(pl.getTracks()): self.add_track_to_directory(track, oc, index=x) return oc # # MAIN MENU # def main_menu(self): Log("main_menu") return ObjectContainer(objects=[ InputDirectoryObject(key=route_path('search'), prompt=L("PROMPT_SEARCH"), title=L("MENU_SEARCH"), thumb=R("icon-search.png")), DirectoryObject(key=route_path('explore'), title=L("MENU_EXPLORE"), thumb=R("icon-explore.png")), DirectoryObject(key=route_path('discover'), title=L("MENU_DISCOVER"), thumb=R("icon-discover.png")), DirectoryObject(key=route_path('radio'), title=L("MENU_RADIO"), thumb=R("icon-radio.png")), DirectoryObject(key=route_path('your_music'), title=L("MENU_YOUR_MUSIC"), thumb=R("icon-yourmusic.png")), PrefsObject(title=L("MENU_PREFS"), thumb=R("icon-preferences.png")) ], ) # # Create objects # def create_track_object_from_track(self, track, index=None): if not track: return None # Get metadata info track_uri = track.getURI() title = track.getName().decode("utf-8") image_url = self.select_image(track.getAlbumCovers()) track_duration = int(track.getDuration()) - 500 track_number = int(track.getNumber()) track_album = track.getAlbum(nameOnly=True).decode("utf-8") track_artists = track.getArtists(nameOnly=True).decode("utf-8") metadata = TrackMetadata(title, image_url, track_uri, track_duration, track_number, track_album, track_artists) return self.create_track_object_from_metatada(metadata, index=index) def create_track_object_from_metatada(self, metadata, index=None): if not metadata: return None return self.create_track_object(metadata.uri, metadata.duration, metadata.title, metadata.album, metadata.artists, metadata.number, metadata.image_url, index) def create_track_object_empty(self, uri): if not uri: return None return self.create_track_object(uri, -1, "", "", "", 0, None) def create_track_object(self, uri, duration, title, album, artists, track_number, image_url, index=None): rating_key = uri if index is not None: rating_key = '%s::%s' % (uri, index) art_num = str(randint(1, 40)).rjust(2, "0") track_obj = TrackObject(items=[ MediaObject(parts=[PartObject(key=route_path('play/%s' % uri))], duration=duration, container=Container.MP3, audio_codec=AudioCodec.MP3, audio_channels=2) ], key=route_path('metadata', uri), rating_key=rating_key, title=title, album=album, artist=artists, index=index if index != None else track_number, duration=duration, source_title='Spotify', art=R('art-' + art_num + '.png'), thumb=function_path('image.png', uri=image_url)) Log.Debug( 'New track object for metadata: --|%s|%s|%s|%s|%s|%s|--' % (image_url, uri, str(duration), str(track_number), album, artists)) return track_obj def create_album_object(self, album, custom_summary=None, custom_image_url=None): """ Factory method for album objects """ title = album.getName().decode("utf-8") if Prefs["displayAlbumYear"] and album.getYear() != 0: title = "%s (%s)" % (title, album.getYear()) artist_name = album.getArtists(nameOnly=True).decode("utf-8") summary = '' if custom_summary == None else custom_summary.decode( 'utf-8') image_url = self.select_image(album.getCovers( )) if custom_image_url == None else custom_image_url return DirectoryObject( key=route_path('album', album.getURI()), title=title + " - " + artist_name, tagline=artist_name, summary=summary, art=function_path('image.png', uri=image_url), thumb=function_path('image.png', uri=image_url), ) #return AlbumObject( # key=route_path('album', album.getURI().decode("utf-8")), # rating_key=album.getURI().decode("utf-8"), # # title=title, # artist=artist_name, # summary=summary, # # track_count=album.getNumTracks(), # source_title='Spotify', # # art=function_path('image.png', uri=image_url), # thumb=function_path('image.png', uri=image_url), #) def create_playlist_object(self, playlist): uri = playlist.getURI() image_url = self.select_image(playlist.getImages()) artist = playlist.getUsername().decode('utf8') title = playlist.getName().decode("utf-8") summary = '' if playlist.getDescription() != None and len( playlist.getDescription()) > 0: summary = playlist.getDescription().decode("utf-8") return DirectoryObject( key=route_path('playlist', uri), title=title + " - " + artist, tagline=artist, summary=summary, art=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png"), thumb=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png")) #return AlbumObject( # key=route_path('playlist', uri), # rating_key=uri, # # title=title, # artist=artist, # summary=summary, # # source_title='Spotify', # # art=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png"), # thumb=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png") #) def create_genre_object(self, genre): uri = genre.getTemplateName() title = genre.getName().decode("utf-8") image_url = genre.getIconUrl() return DirectoryObject( key=route_path('genre', uri), title=title, art=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png"), thumb=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png")) def create_artist_object(self, artist, custom_summary=None, custom_image_url=None): image_url = self.select_image(artist.getPortraits( )) if custom_image_url == None else custom_image_url artist_name = artist.getName().decode("utf-8") summary = '' if custom_summary == None else custom_summary.decode( 'utf-8') return DirectoryObject(key=route_path('artist', artist.getURI()), title=artist_name, summary=summary, art=function_path('image.png', uri=image_url), thumb=function_path('image.png', uri=image_url)) #return ArtistObject( # key=route_path('artist', artist.getURI().decode("utf-8")), # rating_key=artist.getURI().decode("utf-8"), # # title=artist_name, # summary=summary, # source_title='Spotify', # # art=function_path('image.png', uri=image_url), # thumb=function_path('image.png', uri=image_url) # ) # # Insert objects into container # def add_section_header(self, title, oc): oc.add(DirectoryObject(key='', title=title)) def add_track_to_directory(self, track, oc, index=None): if not self.client.is_track_playable(track): Log("Ignoring unplayable track: %s" % track.getName()) return track_uri = track.getURI().decode("utf-8") if not self.client.is_track_uri_valid(track_uri): Log("Ignoring unplayable track: %s, invalid uri: %s" % (track.getName(), track_uri)) return oc.add(self.create_track_object_from_track(track, index=index)) def add_album_to_directory(self, album, oc, custom_summary=None, custom_image_url=None): if not self.client.is_album_playable(album): Log("Ignoring unplayable album: %s" % album.getName()) return oc.add( self.create_album_object(album, custom_summary=custom_summary, custom_image_url=custom_image_url)) def add_artist_to_directory(self, artist, oc, custom_summary=None, custom_image_url=None): oc.add( self.create_artist_object(artist, custom_summary=custom_summary, custom_image_url=custom_image_url)) def add_playlist_to_directory(self, playlist, oc): oc.add(self.create_playlist_object(playlist)) def add_genre_to_directory(self, genre, oc): oc.add(self.create_genre_object(genre)) def add_story_to_directory(self, story, oc): content_type = story.getContentType() image_url = self.select_image(story.getImages()) item = story.getObject() if content_type == 'artist': self.add_artist_to_directory(item, oc, custom_summary=story.getDescription(), custom_image_url=image_url) elif content_type == 'album': self.add_album_to_directory(item, oc, custom_summary=story.getDescription(), custom_image_url=image_url) elif content_type == 'track': self.add_album_to_directory(item.getAlbum(), oc, custom_summary=story.getDescription() + " - " + item.getName(), custom_image_url=image_url)
class SpotifyPlugin(RunLoopMixin): ''' The main spotify plugin class ''' def __init__(self, ioloop): self.ioloop = ioloop self.client = None self.server = None self.browsers = {} self.start() @property def username(self): return Prefs["username"] @property def password(self): return Prefs["password"] def preferences_updated(self): ''' Called when the user updates the plugin preferences Note: if a user changes the username and password and we have an existing client we need to restart the plugin to use the new details. libspotify doesn't play nice with username and password changes. ''' if not self.client: self.start() elif self.client.needs_restart(self.username, self.password): self.restart() else: Log("User details unchanged") def restart(self): ''' Restart the plugin to pick up new authentication details Note: don't restart inline since it will make the framework barf. Instead schedule a callback on the ioloop's next tick ''' Log("Restarting plugin") if self.client: self.client.disconnect() self.schedule_timer(0.2, lambda: urlopen(RESTART_URL)) def start(self): ''' Start the Spotify client and HTTP server ''' if not self.username or not self.password: Log("Username or password not set: not logging in") return self.client = SpotifyClient(self.username, self.password, self.ioloop) self.client.connect() self.server = SpotifyServer(self.client) self.server.start() def play_track(self, uri): ''' Play a spotify track: redirect the user to the actual stream ''' if not uri: Log("Play track callback invoked with NULL URI") return track_url = self.server.get_track_url(uri) Log("Redirecting client to stream proxied at: %s" % track_url) return Redirect(track_url) def create_track_object(self, track): ''' Factory for track directory objects ''' album_uri = str(Link.from_album(track.album())) track_uri = str(Link.from_track(track, 0)) thumbnail_url = self.server.get_art_url(album_uri) callback = Callback(self.play_track, uri=track_uri, ext="aiff") artists = (a.name().decode("utf-8") for a in track.artists()) return TrackObject( items=[MediaObject(parts=[PartObject(key=callback)], )], key=track.name().decode("utf-8"), rating_key=track.name().decode("utf-8"), title=track.name().decode("utf-8"), album=track.album().name().decode("utf-8"), artist=", ".join(artists), index=track.index(), duration=int(track.duration()), thumb=thumbnail_url) def create_album_object(self, album): ''' Factory method for album objects ''' album_uri = str(Link.from_album(album)) title = album.name().decode("utf-8") if Prefs["displayAlbumYear"] and album.year() != 0: title = "%s (%s)" % (title, album.year()) return DirectoryObject(key=Callback(self.get_album_tracks, uri=album_uri), title=title, thumb=self.server.get_art_url(album_uri)) def add_track_to_directory(self, track, directory): if not self.client.is_track_playable(track): Log("Ignoring unplayable track: %s" % track.name()) return directory.add(self.create_track_object(track)) def add_album_to_directory(self, album, directory): if not self.client.is_album_playable(album): Log("Ignoring unplayable album: %s" % album.name()) return directory.add(self.create_album_object(album)) def add_artist_to_directory(self, artist, directory): artist_uri = str(Link.from_artist(artist)) directory.add( DirectoryObject(key=Callback(self.get_artist_albums, uri=artist_uri), title=artist.name().decode("utf-8"), thumb=R("placeholder-artist.png"))) @authenticated def get_playlist(self, folder_id, index): playlists = self.client.get_playlists(folder_id) if len(playlists) < index + 1: return MessageContainer(header=L("MSG_TITLE_PLAYLIST_ERROR"), message=L("MSG_BODY_PLAYIST_ERROR")) playlist = playlists[index] tracks = list(playlist) Log("Get playlist: %s", playlist.name().decode("utf-8")) directory = ObjectContainer(title2=playlist.name().decode("utf-8"), view_group=ViewMode.Tracks) for track in assert_loaded(tracks): self.add_track_to_directory(track, directory) return directory @authenticated def get_artist_albums(self, uri, completion): ''' Browse an artist invoking the completion callback when done. :param uri: The Spotify URI of the artist to browse. :param completion: A callback to invoke with results when done. ''' artist = Link.from_string(uri).as_artist() def browse_finished(browser): del self.browsers[uri] albums = browser.albums() directory = ObjectContainer(title2=artist.name().decode("utf-8"), view_group=ViewMode.Tracks) for album in albums: self.add_album_to_directory(album, directory) completion(directory) self.browsers[uri] = self.client.browse_artist(artist, browse_finished) @authenticated def get_album_tracks(self, uri, completion): ''' Browse an album invoking the completion callback when done. :param uri: The Spotify URI of the album to browse. :param completion: A callback to invoke with results when done. ''' album = Link.from_string(uri).as_album() def browse_finished(browser): del self.browsers[uri] tracks = list(browser) directory = ObjectContainer(title2=album.name().decode("utf-8"), view_group=ViewMode.Tracks) for track in tracks: self.add_track_to_directory(track, directory) completion(directory) self.browsers[uri] = self.client.browse_album(album, browse_finished) @authenticated def get_playlists(self, folder_id=0): Log("Get playlists") directory = ObjectContainer(title2=L("MENU_PREFS"), view_group=ViewMode.Playlists) playlists = self.client.get_playlists(folder_id) for playlist in playlists: index = playlists.index(playlist) if playlist.type() in [ 'folder_start', 'folder_end', 'placeholder' ]: callback = Callback(self.get_playlists, folder_id=playlist.id()) else: callback = Callback(self.get_playlist, folder_id=folder_id, index=index) directory.add( DirectoryObject(key=callback, title=playlist.name().decode("utf-8"), thumb=R("placeholder-playlist.png"))) return directory @authenticated def get_starred_tracks(self): ''' Return a directory containing the user's starred tracks''' Log("Get starred tracks") directory = ObjectContainer(title2=L("MENU_STARRED"), view_group=ViewMode.Tracks) starred = list(self.client.get_starred_tracks()) for track in starred: self.add_track_to_directory(track, directory) return directory @authenticated def search(self, query, completion, artists=False, albums=False): ''' Search asynchronously invoking the completion callback when done. :param query: The query string to use. :param completion: A callback to invoke with results when done. :param artists: Determines whether artist matches are returned. :param albums: Determines whether album matches are returned. ''' params = "%s: %s" % ("artists" if artists else "albums", query) Log("Search for %s" % params) def search_finished(results, userdata): Log("Search completed: %s" % params) result = ObjectContainer(title2="Results") for artist in results.artists() if artists else (): self.add_artist_to_directory(artist, result) for album in results.albums() if albums else (): self.add_album_to_directory(album, result) if not len(result): if len(results.did_you_mean()): message = localized_format("MSG_FMT_DID_YOU_MEAN", results.did_you_mean()) else: message = localized_format("MSG_FMT_NO_RESULTS", query) result = MessageContainer(header=L("MSG_TITLE_NO_RESULTS"), message=message) completion(result) self.client.search(query, search_finished) @authenticated def search_menu(self): Log("Search menu") return ObjectContainer( title2=L("MENU_SEARCH"), objects=[ InputDirectoryObject(key=Callback(self.search, albums=True), prompt=L("PROMPT_ALBUM_SEARCH"), title=L("MENU_ALBUM_SEARCH"), thumb=R("icon-default.png")), InputDirectoryObject(key=Callback(self.search, artists=True), prompt=L("PROMPT_ARTIST_SEARCH"), title=L("MENU_ARTIST_SEARCH"), thumb=R("icon-default.png")) ], ) def main_menu(self): Log("Spotify main menu") return ObjectContainer(objects=[ DirectoryObject(key=Callback(self.get_playlists), title=L("MENU_PLAYLISTS"), thumb=R("icon-default.png")), DirectoryObject(key=Callback(self.search_menu), title=L("MENU_SEARCH"), thumb=R("icon-default.png")), DirectoryObject(key=Callback(self.get_starred_tracks), title=L("MENU_STARRED"), thumb=R("icon-default.png")), PrefsObject(title=L("MENU_PREFS"), thumb=R("icon-default.png")) ], )
class SpotifyPlugin(object): def __init__(self): self.client = None self.server = None self.play_lock = Semaphore(1) self.start_lock = Semaphore(1) self.start_marker = Event() self.last_track_uri = None self.last_track_object = None Dict.Reset() Dict['play_count'] = 0 Dict['last_restart'] = 0 Dict['schedule_restart_each'] = 5*60 # restart each X minutes Dict['play_restart_each'] = 2 # restart each X plays Dict['check_restart_each'] = 5 # check if I should restart each X seconds Dict['radio_salt'] = False # Saves last radio salt so multiple queries return the same radio track list self.start() self.session = requests.session() self.session_cached = CacheControl(self.session) Thread.CreateTimer(Dict['check_restart_each'], self.check_automatic_restart, globalize=True) @property def username(self): return Prefs["username"] @property def password(self): return Prefs["password"] def check_automatic_restart(self): can_restart = False try: diff = time.time() - Dict['last_restart'] scheduled_restart = diff >= Dict['schedule_restart_each'] play_count_restart = Dict['play_count'] >= Dict['play_restart_each'] must_restart = play_count_restart or scheduled_restart if must_restart: can_restart = self.play_lock.acquire(blocking=False) if can_restart: Log.Debug('Automatic restart started') self.start() Log.Debug('Automatic restart finished') finally: if can_restart: self.play_lock.release() Thread.CreateTimer(Dict['check_restart_each'], self.check_automatic_restart, globalize=True) @check_restart def preferences_updated(self): """ Called when the user updates the plugin preferences""" self.start() # Trigger a client restart def start(self): """ Start the Spotify client and HTTP server """ if not self.username or not self.password: Log("Username or password not set: not logging in") return False can_start = self.start_lock.acquire(blocking=False) try: # If there is a start in process, just wait until it finishes, but don't raise another one if not can_start: Log.Debug("Start already in progress, waiting it finishes to return") self.start_lock.acquire() else: Log.Debug("Start triggered, entering private section") self.start_marker.clear() if self.client: self.client.restart(self.username, self.password) else: self.client = SpotifyClient(self.username, self.password) self.last_track_uri = None self.last_track_object = None Dict['play_count'] = 0 Dict['last_restart'] = time.time() self.start_marker.set() Log.Debug("Start finished, leaving private section") finally: self.start_lock.release() return self.client and self.client.is_logged_in() @check_restart def play(self, uri): Log('play(%s)' % repr(uri)) uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") track_url = None if not self.client.is_track_uri_valid(uri): Log("Play track callback invoked with invalid URI (%s). This is very bad :-(" % uri) track_url = "http://www.xamuel.com/blank-mp3-files/2sec.mp3" else: self.play_lock.acquire(blocking=True) try: track_url = self.get_track_url(uri) # If first request failed, trigger re-connection to spotify retry_num = 0 while not track_url and retry_num < 2: Log.Info('get_track_url (%s) failed, re-connecting to spotify...' % uri) time.sleep(retry_num*0.5) # Wait some time based on number of failures if self.start(): track_url = self.get_track_url(uri) retry_num = retry_num + 1 if track_url == False or track_url is None: # Send an empty and short mp3 so player do not fail and we can go on listening next song Log.Error("Play track (%s) couldn't be obtained. This is very bad :-(" % uri) track_url = 'http://www.xamuel.com/blank-mp3-files/2sec.mp3' elif retry_num == 0: # If I didn't restart, add 1 to playcount Dict['play_count'] = Dict['play_count'] + 1 finally: self.play_lock.release() return Redirect(track_url) def get_track_url(self, track_uri): if not self.client.is_track_uri_valid(track_uri): return None track_url = None track = self.client.get(track_uri) if track: track_url = track.getFileURL(urlOnly=True, retries=1) return track_url # # TRACK DETAIL # @check_restart def metadata(self, uri): Log('metadata(%s)' % repr(uri)) uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") oc = ObjectContainer() track_object = None if not self.client.is_track_uri_valid(uri): Log("Metadata callback invoked with invalid URI (%s)" % uri) track_object = self.create_track_object_empty(uri) else: if self.last_track_uri == uri: track_object = self.last_track_object else: track_metadata = self.get_track_metadata(uri) if track_metadata: track_object = self.create_track_object_from_metatada(track_metadata) self.last_track_uri = uri self.last_track_object = track_object else: track_object = self.create_track_object_empty(uri) oc.add(track_object) return oc def get_track_metadata(self, track_uri): if not self.client.is_track_uri_valid(track_uri): return None track = self.client.get(track_uri) if not track: return None #track_uri = track.getURI().decode("utf-8") title = track.getName().decode("utf-8") image_url = self.select_image(track.getAlbumCovers()) track_duration = int(track.getDuration()) track_number = int(track.getNumber()) track_album = track.getAlbum(nameOnly=True).decode("utf-8") track_artists = track.getArtists(nameOnly=True).decode("utf-8") metadata = TrackMetadata(title, image_url, track_uri, track_duration, track_number, track_album, track_artists) return metadata @staticmethod def select_image(images): if images == None: return None if images.get(640): return images[640] elif images.get(320): return images[320] elif images.get(300): return images[300] elif images.get(160): return images[160] elif images.get(60): return images[60] Log.Info('Unable to select image, available sizes: %s' % images.keys()) return None def get_uri_image(self, uri): images = None obj = self.client.get(uri) if isinstance(obj, SpotifyArtist): images = obj.getPortraits() elif isinstance(obj, SpotifyAlbum): images = obj.getCovers() elif isinstance(obj, SpotifyTrack): images = obj.getAlbum().getCovers() elif isinstance(obj, SpotifyPlaylist): images = obj.getImages() return self.select_image(images) @authenticated @check_restart def image(self, uri): if not uri: # TODO media specific placeholders return Redirect(R('placeholder-artist.png')) Log.Debug('Getting image for: %s' % uri) uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") if uri.startswith('spotify:'): # Fetch object for spotify URI and select image image_url = self.get_uri_image(uri) if not image_url: # TODO media specific placeholders return Redirect(R('placeholder-artist.png')) else: # pre-selected image provided Log.Debug('Using pre-selected image URL: "%s"' % uri) image_url = uri return self.session_cached.get(image_url).content # # SECOND_LEVEL_MENU # @authenticated @check_restart def explore(self): Log("explore") """ Explore shared music """ return ObjectContainer( objects=[ DirectoryObject( key=route_path('explore/featured_playlists'), title=L("MENU_FEATURED_PLAYLISTS"), thumb=R("icon-explore-featuredplaylists.png") ), DirectoryObject( key=route_path('explore/top_playlists'), title=L("MENU_TOP_PLAYLISTS"), thumb=R("icon-explore-topplaylists.png") ), DirectoryObject( key=route_path('explore/new_releases'), title=L("MENU_NEW_RELEASES"), thumb=R("icon-explore-newreleases.png") ), DirectoryObject( key=route_path('explore/genres'), title=L("MENU_GENRES"), thumb=R("icon-explore-genres.png") ) ], ) @authenticated @check_restart def discover(self): Log("discover") oc = ObjectContainer( title2=L("MENU_DISCOVER"), view_group=ViewMode.Stories ) stories = self.client.discover() for story in stories: self.add_story_to_directory(story, oc) return oc @authenticated @check_restart def radio(self): Log("radio") """ Show radio options """ return ObjectContainer( objects=[ DirectoryObject( key=route_path('radio/stations'), title=L("MENU_RADIO_STATIONS"), thumb=R("icon-radio-stations.png") ), DirectoryObject( key=route_path('radio/genres'), title=L("MENU_RADIO_GENRES"), thumb=R("icon-radio-genres.png") ) ], ) @authenticated @check_restart def your_music(self): Log("your_music") """ Explore your music """ return ObjectContainer( objects=[ DirectoryObject( key=route_path('your_music/playlists'), title=L("MENU_PLAYLISTS"), thumb=R("icon-playlists.png") ), DirectoryObject( key=route_path('your_music/starred'), title=L("MENU_STARRED"), thumb=R("icon-starred.png") ), DirectoryObject( key=route_path('your_music/albums'), title=L("MENU_ALBUMS"), thumb=R("icon-albums.png") ), DirectoryObject( key=route_path('your_music/artists'), title=L("MENU_ARTISTS"), thumb=R("icon-artists.png") ), ], ) # # EXPLORE # @authenticated @check_restart def featured_playlists(self): Log("featured playlists") oc = ObjectContainer( title2=L("MENU_FEATURED_PLAYLISTS"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists ) playlists = self.client.get_featured_playlists() for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc @authenticated @check_restart def top_playlists(self): Log("top playlists") oc = ObjectContainer( title2=L("MENU_TOP_PLAYLISTS"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists ) playlists = self.client.get_top_playlists() for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc @authenticated @check_restart def new_releases(self): Log("new releases") oc = ObjectContainer( title2=L("MENU_NEW_RELEASES"), content=ContainerContent.Albums, view_group=ViewMode.Albums ) albums = self.client.get_new_releases() for album in albums: self.add_album_to_directory(album, oc) return oc @authenticated @check_restart def genres(self): Log("genres") oc = ObjectContainer( title2=L("MENU_GENRES"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists ) genres = self.client.get_genres() for genre in genres: self.add_genre_to_directory(genre, oc) return oc @authenticated @check_restart def genre_playlists(self, genre_name): Log("genre playlists") oc = ObjectContainer( title2=genre_name, content=ContainerContent.Playlists, view_group=ViewMode.Playlists ) playlists = self.client.get_playlists_by_genre(genre_name) for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc # # RADIO # @authenticated @check_restart def radio_stations(self): Log('radio stations') Dict['radio_salt'] = False oc = ObjectContainer(title2=L("MENU_RADIO_STATIONS")) stations = self.client.get_radio_stations() for station in stations: oc.add(PopupDirectoryObject( key=route_path('radio/stations/' + station.getURI()), title=station.getTitle(), thumb=function_path('image.png', uri=self.select_image(station.getImages())) )) return oc @authenticated @check_restart def radio_genres(self): Log('radio genres') Dict['radio_salt'] = False oc = ObjectContainer(title2=L("MENU_RADIO_GENRES")) genres = self.client.get_radio_genres() for genre in genres: oc.add(PopupDirectoryObject( key=route_path('radio/genres/' + genre.getURI()), title=genre.getTitle(), thumb=function_path('image.png', uri=self.select_image(genre.getImages())) )) return oc @authenticated @check_restart def radio_track_num(self, uri): Log('radio track num') uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") return ObjectContainer( title2=L("MENU_RADIO_TRACK_NUM"), objects=[ DirectoryObject( key=route_path('radio/play/' + uri + '/10'), title=localized_format("MENU_TRACK_NUM", "10"), thumb=R("icon-radio-item.png") ), DirectoryObject( key=route_path('radio/play/' + uri + '/20'), title=localized_format("MENU_TRACK_NUM", "20"), thumb=R("icon-radio-item.png") ), DirectoryObject( key=route_path('radio/play/' + uri + '/50'), title=localized_format("MENU_TRACK_NUM", "50"), thumb=R("icon-radio-item.png") ), DirectoryObject( key=route_path('radio/play/' + uri + '/80'), title=localized_format("MENU_TRACK_NUM", "80"), thumb=R("icon-radio-item.png") ), DirectoryObject( key=route_path('radio/play/' + uri + '/100'), title=localized_format("MENU_TRACK_NUM", "100"), thumb=R("icon-radio-item.png") ) ], ) @authenticated @check_restart def radio_tracks(self, uri, num_tracks): Log('radio tracks') uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") oc = None radio = self.client.get_radio(uri) if not Dict['radio_salt']: Dict['radio_salt'] = radio.generateSalt() salt = Dict['radio_salt'] tracks = radio.getTracks(salt=salt, num_tracks=int(num_tracks)) oc = ObjectContainer( title2 = radio.getTitle().decode("utf-8"), content = ContainerContent.Tracks, view_group = ViewMode.Tracks ) for track in tracks: self.add_track_to_directory(track, oc) return oc # # YOUR_MUSIC # @authenticated @check_restart def playlists(self): Log("playlists") oc = ObjectContainer( title2=L("MENU_PLAYLISTS"), content=ContainerContent.Playlists, view_group=ViewMode.Playlists ) playlists = self.client.get_playlists() for playlist in playlists: self.add_playlist_to_directory(playlist, oc) return oc @authenticated @check_restart def starred(self): Log("starred") oc = ObjectContainer( title2=L("MENU_STARRED"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks ) starred = self.client.get_starred() for x, track in enumerate(starred.getTracks()): self.add_track_to_directory(track, oc, index=x) return oc @authenticated @check_restart def albums(self): Log("albums") oc = ObjectContainer( title2=L("MENU_ALBUMS"), content=ContainerContent.Albums, view_group=ViewMode.Albums ) albums = self.client.get_my_albums() for album in albums: self.add_album_to_directory(album, oc) return oc @authenticated @check_restart def artists(self): Log("artists") oc = ObjectContainer( title2=L("MENU_ARTISTS"), content=ContainerContent.Artists, view_group=ViewMode.Artists ) artists = self.client.get_my_artists() for artist in artists: self.add_artist_to_directory(artist, oc) return oc # # ARTIST DETAIL # @authenticated @check_restart def artist(self, uri): Log("artist") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") artist = self.client.get(uri) return ObjectContainer( title2=artist.getName().decode("utf-8"), objects=[ DirectoryObject( key = route_path('artist/%s/top_tracks' % uri), title=L("MENU_TOP_TRACKS"), thumb=R("icon-artist-toptracks.png") ), DirectoryObject( key = route_path('artist/%s/albums' % uri), title =L("MENU_ALBUMS"), thumb =R("icon-albums.png") ), DirectoryObject( key = route_path('artist/%s/related' % uri), title =L("MENU_RELATED"), thumb =R("icon-artist-related.png") ), DirectoryObject( key=route_path('radio/stations/' + uri), title =L("MENU_RADIO"), thumb =R("icon-radio-custom.png") ) ], ) @authenticated @check_restart def artist_albums(self, uri): Log("artist_albums") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") artist = self.client.get(uri) oc = ObjectContainer( title2=artist.getName().decode("utf-8"), content=ContainerContent.Albums ) for album in artist.getAlbums(): self.add_album_to_directory(album, oc) return oc @authenticated @check_restart def artist_top_tracks(self, uri): Log("artist_top_tracks") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") oc = None artist = self.client.get(uri) top_tracks = artist.getTracks() if top_tracks: oc = ObjectContainer( title2=artist.getName().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks ) for track in artist.getTracks(): self.add_track_to_directory(track, oc) else: oc = MessageContainer( header=L("MSG_TITLE_NO_RESULTS"), message=localized_format("MSG_FMT_NO_RESULTS", artist.getName().decode("utf-8")) ) return oc @authenticated @check_restart def artist_related(self, uri): Log("artist_related") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") artist = self.client.get(uri) oc = ObjectContainer( title2=localized_format("MSG_RELATED_TO", artist.getName().decode("utf-8")), content=ContainerContent.Artists ) for artist in artist.getRelatedArtists(): self.add_artist_to_directory(artist, oc) return oc # # ALBUM DETAIL # @authenticated @check_restart def album(self, uri): Log("album") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") album = self.client.get(uri) oc = ObjectContainer( title2=album.getName().decode("utf-8"), content=ContainerContent.Artists ) oc.add(DirectoryObject( key = route_path('album/%s/tracks' % uri), title=L("MENU_ALBUM_TRACKS"), thumb=R("icon-album-tracks.png"))) artists = album.getArtists() for artist in artists: self.add_artist_to_directory(artist, oc) oc.add(DirectoryObject( key=route_path('radio/stations/' + uri), title =L("MENU_RADIO"), thumb =R("icon-radio-custom.png"))) return oc @authenticated @check_restart def album_tracks(self, uri): Log("album_tracks") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") album = self.client.get(uri) oc = ObjectContainer( title2=album.getName().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks ) for track in album.getTracks(): self.add_track_to_directory(track, oc) return oc # # PLAYLIST DETAIL # @authenticated @check_restart def playlist(self, uri): Log("playlist") uri = urllib.quote(uri.encode("utf8")).replace("%3A", ":").decode("utf8") pl = self.client.get(uri) if pl is None: # Unable to find playlist return MessageContainer( header=L("MSG_TITLE_UNKNOWN_PLAYLIST"), message='URI: %s' % uri ) Log("Get playlist: %s", pl.getName().decode("utf-8")) Log.Debug('playlist truncated: %s', pl.obj.contents.truncated) oc = ObjectContainer( title2=pl.getName().decode("utf-8"), content=ContainerContent.Tracks, view_group=ViewMode.Tracks, mixed_parents=True ) for x, track in enumerate(pl.getTracks()): self.add_track_to_directory(track, oc, index=x) return oc # # MAIN MENU # def main_menu(self): Log("main_menu") return ObjectContainer( objects=[ InputDirectoryObject( key=route_path('search'), prompt=L("PROMPT_SEARCH"), title=L("MENU_SEARCH"), thumb=R("icon-search.png") ), DirectoryObject( key=route_path('explore'), title=L("MENU_EXPLORE"), thumb=R("icon-explore.png") ), DirectoryObject( key=route_path('discover'), title=L("MENU_DISCOVER"), thumb=R("icon-discover.png") ), DirectoryObject( key=route_path('radio'), title=L("MENU_RADIO"), thumb=R("icon-radio.png") ), DirectoryObject( key=route_path('your_music'), title=L("MENU_YOUR_MUSIC"), thumb=R("icon-yourmusic.png") ), PrefsObject( title=L("MENU_PREFS"), thumb=R("icon-preferences.png") ) ], ) # # Create objects # def create_track_object_from_track(self, track, index=None): if not track: return None # Get metadata info track_uri = track.getURI() title = track.getName().decode("utf-8") image_url = self.select_image(track.getAlbumCovers()) track_duration = int(track.getDuration()) - 500 track_number = int(track.getNumber()) track_album = track.getAlbum(nameOnly=True).decode("utf-8") track_artists = track.getArtists(nameOnly=True).decode("utf-8") metadata = TrackMetadata(title, image_url, track_uri, track_duration, track_number, track_album, track_artists) return self.create_track_object_from_metatada(metadata, index=index) def create_track_object_from_metatada(self, metadata, index=None): if not metadata: return None return self.create_track_object(metadata.uri, metadata.duration, metadata.title, metadata.album, metadata.artists, metadata.number, metadata.image_url, index) def create_track_object_empty(self, uri): if not uri: return None return self.create_track_object(uri, -1, "", "", "", 0, None) def create_track_object(self, uri, duration, title, album, artists, track_number, image_url, index=None): rating_key = uri if index is not None: rating_key = '%s::%s' % (uri, index) art_num = str(randint(1,40)).rjust(2, "0") track_obj = TrackObject( items=[ MediaObject( parts=[PartObject(key=route_path('play/%s' % uri))], duration=duration, container=Container.MP3, audio_codec=AudioCodec.MP3, audio_channels = 2 ) ], key = route_path('metadata', uri), rating_key = rating_key, title = title, album = album, artist = artists, index = index if index != None else track_number, duration = duration, source_title='Spotify', art = R('art-' + art_num + '.png'), thumb = function_path('image.png', uri=image_url) ) Log.Debug('New track object for metadata: --|%s|%s|%s|%s|%s|%s|--' % (image_url, uri, str(duration), str(track_number), album, artists)) return track_obj def create_album_object(self, album, custom_summary=None, custom_image_url=None): """ Factory method for album objects """ title = album.getName().decode("utf-8") if Prefs["displayAlbumYear"] and album.getYear() != 0: title = "%s (%s)" % (title, album.getYear()) artist_name = album.getArtists(nameOnly=True).decode("utf-8") summary = '' if custom_summary == None else custom_summary.decode('utf-8') image_url = self.select_image(album.getCovers()) if custom_image_url == None else custom_image_url return DirectoryObject( key=route_path('album', album.getURI()), title=title + " - " + artist_name, tagline=artist_name, summary=summary, art=function_path('image.png', uri=image_url), thumb=function_path('image.png', uri=image_url), ) #return AlbumObject( # key=route_path('album', album.getURI().decode("utf-8")), # rating_key=album.getURI().decode("utf-8"), # # title=title, # artist=artist_name, # summary=summary, # # track_count=album.getNumTracks(), # source_title='Spotify', # # art=function_path('image.png', uri=image_url), # thumb=function_path('image.png', uri=image_url), #) def create_playlist_object(self, playlist): uri = playlist.getURI() image_url = self.select_image(playlist.getImages()) artist = playlist.getUsername().decode('utf8') title = playlist.getName().decode("utf-8") summary = '' if playlist.getDescription() != None and len(playlist.getDescription()) > 0: summary = playlist.getDescription().decode("utf-8") return DirectoryObject( key=route_path('playlist', uri), title=title + " - " + artist, tagline=artist, summary=summary, art=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png"), thumb=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png") ) #return AlbumObject( # key=route_path('playlist', uri), # rating_key=uri, # # title=title, # artist=artist, # summary=summary, # # source_title='Spotify', # # art=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png"), # thumb=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png") #) def create_genre_object(self, genre): uri = genre.getTemplateName() title = genre.getName().decode("utf-8") image_url = genre.getIconUrl() return DirectoryObject( key=route_path('genre', uri), title=title, art=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png"), thumb=function_path('image.png', uri=image_url) if image_url != None else R("placeholder-playlist.png") ) def create_artist_object(self, artist, custom_summary=None, custom_image_url=None): image_url = self.select_image(artist.getPortraits()) if custom_image_url == None else custom_image_url artist_name = artist.getName().decode("utf-8") summary = '' if custom_summary == None else custom_summary.decode('utf-8') return DirectoryObject( key=route_path('artist', artist.getURI()), title=artist_name, summary=summary, art=function_path('image.png', uri=image_url), thumb=function_path('image.png', uri=image_url) ) #return ArtistObject( # key=route_path('artist', artist.getURI().decode("utf-8")), # rating_key=artist.getURI().decode("utf-8"), # # title=artist_name, # summary=summary, # source_title='Spotify', # # art=function_path('image.png', uri=image_url), # thumb=function_path('image.png', uri=image_url) # ) # # Insert objects into container # def add_section_header(self, title, oc): oc.add( DirectoryObject( key='', title=title ) ) def add_track_to_directory(self, track, oc, index = None): if not self.client.is_track_playable(track): Log("Ignoring unplayable track: %s" % track.getName()) return track_uri = track.getURI().decode("utf-8") if not self.client.is_track_uri_valid(track_uri): Log("Ignoring unplayable track: %s, invalid uri: %s" % (track.getName(), track_uri)) return oc.add(self.create_track_object_from_track(track, index=index)) def add_album_to_directory(self, album, oc, custom_summary=None, custom_image_url=None): if not self.client.is_album_playable(album): Log("Ignoring unplayable album: %s" % album.getName()) return oc.add(self.create_album_object(album, custom_summary=custom_summary, custom_image_url=custom_image_url)) def add_artist_to_directory(self, artist, oc, custom_summary=None, custom_image_url=None): oc.add(self.create_artist_object(artist, custom_summary=custom_summary, custom_image_url=custom_image_url)) def add_playlist_to_directory(self, playlist, oc): oc.add(self.create_playlist_object(playlist)) def add_genre_to_directory(self, genre, oc): oc.add(self.create_genre_object(genre)) def add_story_to_directory(self, story, oc): content_type = story.getContentType() image_url = self.select_image(story.getImages()) item = story.getObject() if content_type == 'artist': self.add_artist_to_directory(item, oc, custom_summary=story.getDescription(), custom_image_url=image_url) elif content_type == 'album': self.add_album_to_directory(item, oc, custom_summary=story.getDescription(), custom_image_url=image_url) elif content_type == 'track': self.add_album_to_directory(item.getAlbum(), oc, custom_summary=story.getDescription() + " - " + item.getName(), custom_image_url=image_url)
class SpotifyPlugin(RunLoopMixin): ''' The main spotify plugin class ''' def __init__(self, ioloop): self.ioloop = ioloop self.client = None self.server = None self.browsers = {} self.start() @property def username(self): return Prefs["username"] @property def password(self): return Prefs["password"] def preferences_updated(self): ''' Called when the user updates the plugin preferences Note: if a user changes the username and password and we have an existing client we need to restart the plugin to use the new details. libspotify doesn't play nice with username and password changes. ''' if not self.client: self.start() elif self.client.needs_restart(self.username, self.password): self.restart() else: Log("User details unchanged") def restart(self): ''' Restart the plugin to pick up new authentication details Note: don't restart inline since it will make the framework barf. Instead schedule a callback on the ioloop's next tick ''' Log("Restarting plugin") if self.client: self.client.disconnect() self.schedule_timer(0.2, lambda: urlopen(RESTART_URL)) def start(self): ''' Start the Spotify client and HTTP server ''' if not self.username or not self.password: Log("Username or password not set: not logging in") return self.client = SpotifyClient(self.username, self.password, self.ioloop) self.client.connect() self.server = SpotifyServer(self.client) self.server.start() def play_track(self, uri): ''' Play a spotify track: redirect the user to the actual stream ''' if not uri: Log("Play track callback invoked with NULL URI") return track_url = self.server.get_track_url(uri) Log("Redirecting client to stream proxied at: %s" % track_url) return Redirect(track_url) def create_track_object(self, track): ''' Factory for track directory objects ''' album_uri = str(Link.from_album(track.album())) track_uri = str(Link.from_track(track, 0)) thumbnail_url = self.server.get_art_url(album_uri) callback = Callback(self.play_track, uri = track_uri, ext = "aiff") artists = (a.name().decode("utf-8") for a in track.artists()) return TrackObject( items = [ MediaObject( parts = [PartObject(key = callback)], ) ], key = track.name().decode("utf-8"), rating_key = track.name().decode("utf-8"), title = track.name().decode("utf-8"), album = track.album().name().decode("utf-8"), artist = ", ".join(artists), index = track.index(), duration = int(track.duration()), thumb = thumbnail_url ) def create_album_object(self, album): ''' Factory method for album objects ''' album_uri = str(Link.from_album(album)) title = album.name().decode("utf-8") if Prefs["displayAlbumYear"] and album.year() != 0: title = "%s (%s)" % (title, album.year()) return DirectoryObject( key = Callback(self.get_album_tracks, uri = album_uri), title = title, thumb = self.server.get_art_url(album_uri) ) def add_track_to_directory(self, track, directory): if not self.client.is_track_playable(track): Log("Ignoring unplayable track: %s" % track.name()) return directory.add(self.create_track_object(track)) def add_album_to_directory(self, album, directory): if not self.client.is_album_playable(album): Log("Ignoring unplayable album: %s" % album.name()) return directory.add(self.create_album_object(album)) def add_artist_to_directory(self, artist, directory): artist_uri = str(Link.from_artist(artist)) directory.add( DirectoryObject( key = Callback(self.get_artist_albums, uri = artist_uri), title = artist.name().decode("utf-8"), thumb = R("placeholder-artist.png") ) ) @authenticated def get_playlist(self, folder_id, index): playlists = self.client.get_playlists(folder_id) if len(playlists) < index + 1: return MessageContainer( header = L("MSG_TITLE_PLAYLIST_ERROR"), message = L("MSG_BODY_PLAYIST_ERROR") ) playlist = playlists[index] tracks = list(playlist) Log("Get playlist: %s", playlist.name().decode("utf-8")) directory = ObjectContainer( title2 = playlist.name().decode("utf-8"), view_group = ViewMode.Tracks) for track in assert_loaded(tracks): self.add_track_to_directory(track, directory) return directory @authenticated def get_artist_albums(self, uri, completion): ''' Browse an artist invoking the completion callback when done. :param uri: The Spotify URI of the artist to browse. :param completion: A callback to invoke with results when done. ''' artist = Link.from_string(uri).as_artist() def browse_finished(browser): del self.browsers[uri] albums = list(browser) directory = ObjectContainer( title2 = artist.name().decode("utf-8"), view_group = ViewMode.Tracks) for album in albums: self.add_album_to_directory(album, directory) completion(directory) self.browsers[uri] = self.client.browse_artist(artist, browse_finished) @authenticated def get_album_tracks(self, uri, completion): ''' Browse an album invoking the completion callback when done. :param uri: The Spotify URI of the album to browse. :param completion: A callback to invoke with results when done. ''' album = Link.from_string(uri).as_album() def browse_finished(browser): del self.browsers[uri] tracks = list(browser) directory = ObjectContainer( title2 = album.name().decode("utf-8"), view_group = ViewMode.Tracks) for track in tracks: self.add_track_to_directory(track, directory) completion(directory) self.browsers[uri] = self.client.browse_album(album, browse_finished) @authenticated def get_playlists(self, folder_id = 0): Log("Get playlists") directory = ObjectContainer( title2 = L("MENU_PREFS"), view_group = ViewMode.Playlists) playlists = self.client.get_playlists(folder_id) for playlist in playlists: index = playlists.index(playlist) if isinstance(playlist, PlaylistFolder): callback = Callback( self.get_playlists, folder_id = playlist.id()) else: callback = Callback( self.get_playlist, folder_id = folder_id, index = index) directory.add( DirectoryObject( key = callback, title = playlist.name().decode("utf-8"), thumb = R("placeholder-playlist.png") ) ) return directory @authenticated def get_starred_tracks(self): ''' Return a directory containing the user's starred tracks''' Log("Get starred tracks") directory = ObjectContainer( title2 = L("MENU_STARRED"), view_group = ViewMode.Tracks) starred = list(self.client.get_starred_tracks()) for track in starred: self.add_track_to_directory(track, directory) return directory @authenticated def search(self, query, completion, artists = False, albums = False): ''' Search asynchronously invoking the completion callback when done. :param query: The query string to use. :param completion: A callback to invoke with results when done. :param artists: Determines whether artist matches are returned. :param albums: Determines whether album matches are returned. ''' params = "%s: %s" % ("artists" if artists else "albums", query) Log("Search for %s" % params) def search_finished(results, userdata): Log("Search completed: %s" % params) result = ObjectContainer(title2 = "Results") for artist in results.artists() if artists else (): self.add_artist_to_directory(artist, result) for album in results.albums() if albums else (): self.add_album_to_directory(album, result) if not len(result): if len(results.did_you_mean()): message = localized_format( "MSG_FMT_DID_YOU_MEAN", results.did_you_mean()) else: message = localized_format("MSG_FMT_NO_RESULTS", query) result = MessageContainer( header = L("MSG_TITLE_NO_RESULTS"), message = message) completion(result) self.client.search(query, search_finished) @authenticated def search_menu(self): Log("Search menu") return ObjectContainer( title2 = L("MENU_SEARCH"), objects = [ InputDirectoryObject( key = Callback(self.search, albums = True), prompt = L("PROMPT_ALBUM_SEARCH"), title = L("MENU_ALBUM_SEARCH"), thumb = R("icon-default.png") ), InputDirectoryObject( key = Callback(self.search, artists = True), prompt = L("PROMPT_ARTIST_SEARCH"), title = L("MENU_ARTIST_SEARCH"), thumb = R("icon-default.png") ) ], ) def main_menu(self): Log("Spotify main menu") return ObjectContainer( objects = [ DirectoryObject( key = Callback(self.get_playlists), title = L("MENU_PLAYLISTS"), thumb = R("icon-default.png") ), DirectoryObject( key = Callback(self.search_menu), title = L("MENU_SEARCH"), thumb = R("icon-default.png") ), DirectoryObject( key = Callback(self.get_starred_tracks), title = L("MENU_STARRED"), thumb = R("icon-default.png") ), PrefsObject( title = L("MENU_PREFS"), thumb = R("icon-default.png") ) ], )