コード例 #1
0
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)
コード例 #2
0
ファイル: plugin.py プロジェクト: 10alc/Spotify2.bundle
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)