Esempio n. 1
0
    def play_track(self, uri, audio_callback, stop_callback):
        ''' Start playing a spotify track

        :param uri:              The spotify URI of the track to play.
        :param audio_callback:   A callback to invoke when audio arrives.
                                 Return a boolean to indicate if more audio can
                                 be processed.
        :param stop_callback:    A callback to invoke when playback is stopped.
        '''
        self.log("Play track: %s" % uri)
        track = self.load_track(uri)
        self.stop_playback()
        self.session.load(track)
        self.session.play(True)
        self.audio_converter = PCMToAIFFConverter(track)
        self.audio_callback = audio_callback
        self.stop_callback = stop_callback
Esempio n. 2
0
    def play_track(self, uri, audio_callback, stop_callback):
        ''' Start playing a spotify track

        :param uri:              The spotify URI of the track to play.
        :param audio_callback:   A callback to invoke when audio arrives.
                                 Return a boolean to indicate if more audio can
                                 be processed.
        :param stop_callback:    A callback to invoke when playback is stopped.
        '''
        self.log("Play track: %s" % uri)
        track = self.load_track(uri)
        self.stop_playback()
        self.session.load(track)
        self.session.play(True)
        self.audio_converter = PCMToAIFFConverter(track)
        self.audio_callback = audio_callback
        self.stop_callback = stop_callback
Esempio n. 3
0
class SpotifyClient(SpotifySessionManager, RunLoopMixin):
    ''' Spotify client that runs all code on a tornado ioloop

    This subclass is intended to be used in the context of an application
    that uses a tornado ioloop running on a single thread to do its work.
    All Spotify callbacks are bounced to the ioloop passed to the constructor
    so that it is not necessary to lock non thread-safe code.
    '''

    user_agent = PLUGIN_ID
    application_key = Resource.Load('spotify_appkey.key')

    def __init__(self, username, password, ioloop):
        ''' Initializer

        :param username:       The username to connect to spotify with.
        :param password:       The password to authenticate with.
        :param ioloop:         The tornado IOLoop instance to run on.
        '''
        super(SpotifyClient, self).__init__(username, password)
        self.ioloop = ioloop
        self.timer = None
        self.session = None
        self.login_error = None
        self.logging_in = False
        self.stop_callback = None
        self.audio_callback = None
        self.audio_converter = None
        self.playlist_folders = {}
        self.images = {}

    ''' Public methods (names with unscores are disallowed by Plex) '''

    def is_logging_in(self):
        return self.logging_in

    def is_logged_in(self):
        return self.session is not None

    def needs_restart(self, username, password):
        ''' Determines if the library should be restarted '''
        return self.username != username \
            or self.password != password

    def connect(self):
        ''' Connect to Spotify '''
        self.log("Connecting as %s" % self.username)
        self.logging_in = True
        self.schedule_periodic_check(connect(self))

    def disconnect(self):
        ''' Disconnect from Spotify '''
        if not self.session:
            return
        self.log("Logging out")
        self.session.logout()

    def is_album_playable(self, album):
        ''' Check if an album can be played by a client or not '''
        assert_loaded(album)
        return album.is_available()

    def is_track_playable(self, track):
        ''' Check if a track can be played by a client or not '''
        playable = True
        assert_loaded(track)
        if track.is_local():
            playable = False
        elif not track.availability():
            playable = False
        return playable

    def get_art(self, uri, callback):
        ''' Fetch and return album artwork.

        note:: Currently only album artowk can be retrieved.

        :param uri:            The spotify URI of the album to load art for.
        :param callback:       The callback to invoke when artwork is loaded.
                               Should take image data as a single parameter.
        '''
        self.log("Get artwork: %s" % uri)
        link = Link.from_string(uri)
        if link.type() != Link.LINK_ALBUM:
            raise RuntimeError("Non album artwork not supported")
        album = link.as_album()
        def browse_finished(browser):
            self.load_image(uri, album.cover(), callback)
        return self.browse_album(album, browse_finished)

    def notify_main_thread(self, session=None):
        self.log("Notify main thread", debug=True)
        self.schedule_periodic_check(session or self.session)

    def get_playlists(self, folder_id = 0):
        ''' Return the user's playlists

        :param folder_id       The id of the playlist folder to return.
        '''
        self.log("Get playlists (folder id: %s)" % folder_id)
        result = []
        if folder_id in self.playlist_folders:
            result = self.playlist_folders[folder_id]
        return result

    def get_starred_tracks(self):
        ''' Return the user's starred tracks

        TODO this should be made async with a callback rather than assuming
        the starred playlist is loaded (will fail if it isn't right now).
        '''
        self.log("Get starred")
        return assert_loaded(self.session.starred()) if self.session else None

    def search(self, query, callback):
        ''' Execute a search

        :param query:          A query string.
        :param callback:       A callback to invoke when the search is finished.
                               Should take the results list as a parameter.
        '''
        self.log("Search (query = %s)" % query)
        search = self.session.search(query = query, callback = callback)

    def browse_album(self, album, callback):
        ''' Browse an album, invoking the callback when done

        :param album:          An album instance to browse.
        :param callback:       A callback to invoke when the album is loaded.
                               Should take the browser as a single parameter.
        '''
        link = Link.from_album(album)
        def callback_wrapper(browser, userdata):
            self.log("Album browse complete: %s" % link)
            callback(browser)
        self.log("Browse album: %s" % link)
        browser = AlbumBrowser(album, callback_wrapper)
        return browser

    def browse_artist(self, artist, callback):
        ''' Browse an artist, invoking the callback when done

        :param artist:         An artist instance to browse.
        :param callback:       A callback to invoke when the album is loaded.
                               Should take the browser as a single parameter.
        '''
        link = Link.from_artist(artist)
        def callback_wrapper(browser, userdata):
            self.log("Artist browse complete: %s" % Link.from_artist(artist))
            callback(browser)
        self.log("Browse artist: %s" % link)
        browser = ArtistBrowser(artist, "no_tracks", callback_wrapper)
        return browser

    def stop_playback(self):
        ''' Stop playing the current stream '''
        if self.audio_converter is None:
            return
        self.log("Stop playback")
        self.session.play(0)
        self.cleanup()
        self.log("Playback stopped")

    def load_image(self, uri, image_id, callback):
        ''' Load an image from an image id

        Note: this currently polls as I had trouble waiting for callbacks
        when loading images

        :param image_id:         The spotify id of the image to load.
        '''
        def callback_wrapper(image):
            self.log("Image loaded: %s" % uri)
            callback(str(image.data()))
            if uri in self.images:
                del self.images[uri]
        self.log("Loading image: %s" % uri)
        if image_id is not None:
            image = self.images.get(uri, self.session.image_create(image_id))
            image.add_load_callback(callback_wrapper)
            self.images[uri] = image
            return image
        else:
            callback(None)

    def load_track(self, uri):
        ''' Load a track from a spotify URI

        Note: this currently polls as there is no API for browsing
        individual tracks

        :param uri:              The spotify URI of the track to load.
        '''
        track = Link.from_string(uri).as_track()
        return self.wait_until_loaded(track, POLL_TIMEOUT)

    def play_track(self, uri, audio_callback, stop_callback):
        ''' Start playing a spotify track

        :param uri:              The spotify URI of the track to play.
        :param audio_callback:   A callback to invoke when audio arrives.
                                 Return a boolean to indicate if more audio can
                                 be processed.
        :param stop_callback:    A callback to invoke when playback is stopped.
        '''
        self.log("Play track: %s" % uri)
        track = self.load_track(uri)
        self.stop_playback()
        self.session.load(track)
        self.session.play(True)
        self.audio_converter = PCMToAIFFConverter(track)
        self.audio_callback = audio_callback
        self.stop_callback = stop_callback

    ''' Utility methods '''

    def wait_until_loaded(self, spotify_object, timeout):
        ''' Poll a spotify object until it is loaded

        :param spotify_object:   The spotify object to poll.
        :param timeout:          A timeout in seconds.
        '''
        start = time()
        while not spotify_object.is_loaded() and start > time() - timeout:
            message = "Waiting for spotify object: %s" % spotify_object
            self.log(message, debug = True)
            self.session.process_events()
            sleep(POLL_INTERVAL)
        assert_loaded(spotify_object)
        return spotify_object

    def log(self, message, debug = False):
        ''' Logging helper function

        :param message:    The message to output to the log.
        :param debug:      Only output the message in debug mode?
        '''
        message = "SPOTIFY: %s" % message
        Log.Debug(message) if debug else Log(message)

    def schedule_periodic_check(self, session, timeout = 0):
        ''' Schedules the next periodic Spotify event processing call '''
        callback = lambda: self.periodic_check(session)
        self.timer = self.schedule_timer(timeout, callback)

    def periodic_check(self, session):
        ''' Process pending Spotify events and schedule the next check '''
        self.log("Processing events", debug=True)
        timeout = session.process_events() / 1000.0
        self.log('Will wait %.3fs for next message' % timeout, debug=True)
        self.schedule_periodic_check(session, timeout)

    def cleanup(self):
        ''' Cleanup after a track ends explicitly or implicitly '''
        if self.stop_callback is not None:
            self.stop_callback()
            self.stop_callback = None
        self.audio_converter = None

    ''' Spotify callbacks '''

    def logged_in(self, session, error):
        ''' libspotify callback for login attempts '''
        self.logging_in = False
        if error:
            self.log("Error logging in: %s" % error)
            self.login_error = error
        else:
            self.log("Logged in")
            self.session = session
            self.session.playlist_container().add_loaded_callback(
                self.playlists_loaded_callback)

    def logged_out(self, session):
        ''' libspotiy callback for logout requests '''
        if not self.seesion:
            return
        self.log("Logged out")
        self.session = None
        self.cancel_timer(self.timer)

    def playlists_loaded_callback(self, container, userinfo):
        ''' Callback invoked when playlists are loaded '''
        current_folder = []
        folder_stack = []
        folder_map = {
            0 : current_folder
        }
        for playlist in list(self.session.playlist_container()):
            if playlist.type() == "folder_start":
                folder_stack.append(current_folder)
                current_folder.append(playlist)
                current_folder = []
                folder_map[playlist.id()] = current_folder
            elif playlist.type() == "folder_end":
                current_folder = folder_stack.pop()
            elif playlist.type() == "placeholder":
                pass
            else:
                current_folder.append(playlist)
        self.playlist_folders = folder_map

    def end_of_track(self, session):
        ''' libspotify callback for when the current track ends '''
        self.log("Track ended")
        self.cleanup()

    def metadata_updated(self, sess):
        ''' libspotify callback when new metadata arrives '''
        self.log("Metadata update", debug = True)

    def log_message(self, sess, message):
        ''' libspotify callback for system messages '''
        self.log("Message (%s)" % message.strip())

    def connection_error(self, sess, error):
        ''' libspotify callback for connection errors '''
        if error is not None:
            self.log("Connection error (%s)" % error.strip())

    def message_to_user(self, sess, message):
        ''' libspotify callback for user messages '''
        self.log("User message (%s)" % message)

    def music_delivery(self, session, frames, frame_size, num_frames,
                       sample_type, sample_rate, channels):
        ''' Called when libspotify has audio data ready for consumption '''
        if num_frames == 0:
            return 0
        try:
            frames_converted = self.audio_converter.convert(frames, num_frames)
            if not self.audio_callback(self.audio_converter.get_pending_data()):
                self.stop_playback()
                return 0
            return frames_converted
        except Exception, e:
            if self.audio_converter:
                self.log("Playback error: %s" % Plugin.Traceback())
                self.stop_playback()
            return 0
Esempio n. 4
0
class SpotifyClient(SpotifySessionManager, RunLoopMixin):
    ''' Spotify client that runs all code on a tornado ioloop

    This subclass is intended to be used in the context of an application
    that uses a tornado ioloop running on a single thread to do its work.
    All Spotify callbacks are bounced to the ioloop passed to the constructor
    so that it is not necessary to lock non thread-safe code.
    '''

    user_agent = PLUGIN_ID
    application_key = Resource.Load('spotify_appkey.key')

    def __init__(self, username, password, ioloop):
        ''' Initializer

        :param username:       The username to connect to spotify with.
        :param password:       The password to authenticate with.
        :param ioloop:         The tornado IOLoop instance to run on.
        '''
        super(SpotifyClient, self).__init__(username, password)
        self.ioloop = ioloop
        self.timer = None
        self.session = None
        self.login_error = None
        self.logging_in = False
        self.stop_callback = None
        self.audio_callback = None
        self.audio_converter = None
        self.playlist_folders = {}

    ''' Public methods (names with unscores are disallowed by Plex) '''

    def is_logging_in(self):
        return self.logging_in

    def is_logged_in(self):
        return self.session is not None

    def needs_restart(self, username, password):
        ''' Determines if the library should be restarted '''
        return self.username != username \
            or self.password != password

    def connect(self):
        ''' Connect to Spotify '''
        self.log("Connecting as %s" % self.username)
        self.logging_in = True
        self.schedule_periodic_check(connect(self))

    def disconnect(self):
        ''' Disconnect from Spotify '''
        if not self.session:
            return
        self.log("Logging out")
        self.session.logout()

    def is_album_playable(self, album):
        ''' Check if an album can be played by a client or not '''
        assert_loaded(album)
        return album.is_available()

    def is_track_playable(self, track):
        ''' Check if a track can be played by a client or not '''
        playable = True
        assert_loaded(track)
        if track.is_local():
            playable = False
        elif not track.availability():
            playable = False
        return playable

    def get_art(self, uri, callback):
        ''' Fetch and return album artwork.

        note:: Currently only album artowk can be retrieved.

        :param uri:            The spotify URI of the album to load art for.
        :param callback:       The callback to invoke when artwork is loaded.
                               Should take image data as a single parameter.
        '''
        self.log("Get artwork: %s" % uri)
        link = Link.from_string(uri)
        if link.type() != Link.LINK_ALBUM:
            raise RuntimeError("Non album artwork not supported")
        album = link.as_album()

        def browse_finished(browser):
            art = self.load_image(album.cover())
            self.log("Artwork loaded: %s" % album)
            callback(str(art.data()))

        return self.browse_album(album, browse_finished)

    def get_playlists(self, folder_id=0):
        ''' Return the user's playlists

        :param folder_id       The id of the playlist folder to return.
        '''
        self.log("Get playlists (folder id: %s)" % folder_id)
        result = []
        if folder_id in self.playlist_folders:
            result = self.playlist_folders[folder_id]
        return result

    def get_starred_tracks(self):
        ''' Return the user's starred tracks

        TODO this should be made async with a callback rather than assuming
        the starred playlist is loaded (will fail if it isn't right now).
        '''
        self.log("Get starred")
        return assert_loaded(self.session.starred()) if self.session else None

    def search(self, query, callback):
        ''' Execute a search

        :param query:          A query string.
        :param callback:       A callback to invoke when the search is finished.
                               Should take the results list as a parameter.
        '''
        self.log("Search (query = %s)" % query)
        search = self.session.search(query=query, callback=callback)

    def browse_album(self, album, callback):
        ''' Browse an album, invoking the callback when done

        :param album:          An album instance to browse.
        :param callback:       A callback to invoke when the album is loaded.
                               Should take the browser as a single parameter.
        '''
        link = Link.from_album(album)

        def callback_wrapper(browser):
            self.log("Album browse complete: %s" % link)
            callback(browser)

        self.log("Browse album: %s" % link)
        browser = self.session.browse_album(album, callback_wrapper)
        return browser

    def browse_artist(self, artist, callback):
        ''' Browse an artist, invoking the callback when done

        :param artist:         An artist instance to browse.
        :param callback:       A callback to invoke when the album is loaded.
                               Should take the browser as a single parameter.
        '''
        link = Link.from_artist(artist)

        def callback_wrapper(browser):
            self.log("Artist browse complete: %s" % Link.from_artist(artist))
            callback(browser)

        self.log("Browse artist: %s" % link)
        browser = self.session.browse_artist(artist, callback_wrapper)
        return browser

    def stop_playback(self):
        ''' Stop playing the current stream '''
        if self.audio_converter is None:
            return
        self.log("Stop playback")
        self.session.play(0)
        self.cleanup()
        self.log("Playback stopped")

    def load_image(self, image_id):
        ''' Load an image from an image id

        Note: this currently polls as I had trouble waiting for callbacks
        when loading images

        :param image_id:         The spotify id of the image to load.
        '''
        image = self.session.image_create(image_id)
        return self.wait_until_loaded(image, POLL_TIMEOUT)

    def load_track(self, uri):
        ''' Load a track from a spotify URI

        Note: this currently polls as there is no API for browsing
        individual tracks

        :param uri:              The spotify URI of the track to load.
        '''
        track = Link.from_string(uri).as_track()
        return self.wait_until_loaded(track, POLL_TIMEOUT)

    def play_track(self, uri, audio_callback, stop_callback):
        ''' Start playing a spotify track

        :param uri:              The spotify URI of the track to play.
        :param audio_callback:   A callback to invoke when audio arrives.
                                 Return a boolean to indicate if more audio can
                                 be processed.
        :param stop_callback:    A callback to invoke when playback is stopped.
        '''
        self.log("Play track: %s" % uri)
        track = self.load_track(uri)
        self.stop_playback()
        self.session.load(track)
        self.session.play(True)
        self.audio_converter = PCMToAIFFConverter(track)
        self.audio_callback = audio_callback
        self.stop_callback = stop_callback

    ''' Utility methods '''

    def wait_until_loaded(self, spotify_object, timeout):
        ''' Poll a spotify object until it is loaded

        :param spotify_object:   The spotify object to poll.
        :param timeout:          A timeout in seconds.
        '''
        start = time()
        while not spotify_object.is_loaded() and start > time() - timeout:
            message = "Waiting for spotify object: %s" % spotify_object
            self.log(message, debug=True)
            self.session.process_events()
            sleep(POLL_INTERVAL)
        assert_loaded(spotify_object)
        return spotify_object

    def log(self, message, debug=False):
        ''' Logging helper function

        :param message:    The message to output to the log.
        :param debug:      Only output the message in debug mode?
        '''
        message = "SPOTIFY: %s" % message
        Log.Debug(message) if debug else Log(message)

    def schedule_periodic_check(self, session, timeout=0):
        ''' Schedules the next periodic Spotify event processing call '''
        callback = lambda: self.periodic_check(session)
        self.timer = self.schedule_timer(timeout, callback)

    def periodic_check(self, session):
        ''' Process pending Spotify events and schedule the next check '''
        self.log("Processing events", debug=True)
        timeout = session.process_events()
        self.schedule_periodic_check(session, timeout / 1000.0)

    def cleanup(self):
        ''' Cleanup after a track ends explicitly or implicitly '''
        if self.stop_callback is not None:
            self.stop_callback()
            self.stop_callback = None
        self.audio_converter = None

    ''' Spotify callbacks '''

    def logged_in(self, session, error):
        ''' libspotify callback for login attempts '''
        self.logging_in = False
        if error:
            self.log("Error logging in: %s" % error)
            self.login_error = error
        else:
            self.log("Logged in")
            self.session = session
            self.session.playlist_container().add_loaded_callback(
                self.playlists_loaded_callback)

    def logged_out(self, session):
        ''' libspotiy callback for logout requests '''
        if not self.seesion:
            return
        self.log("Logged out")
        self.session = None
        self.cancel_timer(self.timer)

    def playlists_loaded_callback(self, container, userinfo):
        ''' Callback invoked when playlists are loaded '''
        current_folder = []
        folder_stack = []
        folder_map = {0: current_folder}
        for playlist in list(self.session.playlist_container()):
            if isinstance(playlist, PlaylistFolder):
                if playlist.type() == "folder_start":
                    folder_stack.append(current_folder)
                    current_folder.append(playlist)
                    current_folder = []
                    folder_map[playlist.id()] = current_folder
                elif playlist.type() == "folder_end":
                    current_folder = folder_stack.pop()
            else:
                current_folder.append(playlist)
        self.playlist_folders = folder_map

    def end_of_track(self, session):
        ''' libspotify callback for when the current track ends '''
        self.log("Track ended")
        self.cleanup()

    def wake(self, session):
        ''' libspotify callback to wake the main thread '''
        self.log("Waking main thread", debug=True)
        self.schedule_periodic_check(session)

    def metadata_updated(self, sess):
        ''' libspotify callback when new metadata arrives '''
        self.log("Metadata update", debug=True)

    def log_message(self, sess, message):
        ''' libspotify callback for system messages '''
        self.log("Message (%s)" % message.strip())

    def connection_error(self, sess, error):
        ''' libspotify callback for connection errors '''
        if error is not None:
            self.log("Connection error (%s)" % error.strip())

    def message_to_user(self, sess, message):
        ''' libspotify callback for user messages '''
        self.log("User message (%s)" % message)

    def music_delivery(self, session, frames, frame_size, num_frames,
                       sample_type, sample_rate, channels):
        ''' Called when libspotify has audio data ready for consumption '''
        try:
            frames_converted = self.audio_converter.convert(frames, num_frames)
            if not self.audio_callback(
                    self.audio_converter.get_pending_data()):
                self.stop_playback()
                return 0
            return frames_converted
        except Exception, e:
            if self.audio_converter:
                self.log("Playback error: %s" % Plugin.Traceback())
                self.stop_playback()
            return 0