예제 #1
0
    def __init__(self):
        # self.is_debug = os.getenv('CLAY_DEBUG')
        self.mobile_client = Mobileclient()
        self.mobile_client._make_call = self._make_call_proxy(
            self.mobile_client._make_call)
        # if self.is_debug:
        #     self.debug_file = open('/tmp/clay-api-log.json', 'w')
        #     self._last_call_index = 0
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artists = {}
        self.cached_albums = {}
        self.liked_songs = LikedSongs()

        self.invalidate_caches()

        self.auth_state_changed = EventHook()
예제 #2
0
파일: abstract.py 프로젝트: thor/clay
class AbstractPlayer:
    """
    Defines the basic functions used by every player.
    """
    media_position_changed = EventHook()
    media_state_changed = EventHook()
    media_state_stopped = EventHook()
    track_changed = EventHook()
    playback_flags_changed = EventHook()
    queue_changed = EventHook()
    track_appended = EventHook()
    track_removed = EventHook()

    def __init__(self):
        self._create_station_notification = None
        self.queue = _Queue()
        self._loading = False

        # Add notification actions that we are going to use.
        osd_manager.add_to_action("media-skip-backward", "Previous",
                                  lambda: self.prev(force=True))
        osd_manager.add_to_action("media-playback-pause", "Pause",
                                  self.play_pause)
        osd_manager.add_to_action("media-playback-start", "Play",
                                  self.play_pause)
        osd_manager.add_to_action("media-skip-forward", "next", self.next)

    def broadcast_state(self):
        """
        Write current playback state into a ``/tmp/clay.json`` file.
        """
        track = self.queue.get_current_track()
        if track is None:
            data = dict(playing=False,
                        artist=None,
                        title=None,
                        progress=None,
                        length=None)
        else:
            data = dict(loading=self.loading,
                        playing=self.playing,
                        artist=track.artist,
                        title=track.title,
                        progress=self.play_progress_seconds,
                        length=self.length_seconds,
                        album_name=track.album_name,
                        album_url=track.album_url)
        with open('/tmp/clay.json', 'w') as statefile:
            statefile.write(json.dumps(data, indent=4))

    def load_queue(self, data, current_index=0):
        """
        Load queue & start playback

        See :meth:`._Queue.load`
        """
        self.queue.load(data, current_index)
        self.queue_changed.fire()
        self.play()

    def clear_queue(self):
        """
        Clear the queue and stop playback
        """
        self.queue.clear()
        self.queue_changed.fire()
        self.stop()

    def goto_track(self, track):
        """
        Go to a specific track in the queue
        """
        self.queue.goto_track(track)
        self.queue_changed.fire()
        self.play()

    def append_to_queue(self, track):
        """
        Append track to queue.
        Fires :attr:`.track_appended` event

        See :meth:`._Queue.append`
        """
        self.queue.append(track)
        self.track_appended.fire(track)

    def remove_from_queue(self, track):
        """
        Remove track from queue
        Fires :attr:`.trac_removed` event.

        See :meth:`._Queue.remove`
        """
        self.queue.remove(track)
        self.track_removed.fire(track)

    def create_station_from_track(self, track):
        """
        Request creation of new station from some track.

        Runs in background.
        """
        track.create_station_async(callback=self._create_station_ready)

    @property
    def random(self):
        """
        Returns:
           Whether the track selection is random
        """
        return self.queue.random

    @random.setter
    def random(self, value):
        """
        Enable or disable random track selection

        Args:
           value (`bool`):  Whether random track selection should be enabled or disabled.
        """
        self.queue.random = value
        random.shuffle(self.queue.tracks)
        self.queue.current_track_index = 0
        self.play()
        self.playback_flags_changed.fire()
        self.queue_changed.fire()

    @property
    def repeat_one(self):
        """
        Returns:
           Whether single track repition is enabled.
        """
        return self.queue.repeat_one

    @repeat_one.setter
    def repeat_one(self, value):
        """
        Enables or disabled single track repition
        """
        self.queue.repeat_one = value
        self.playback_flags_changed.fire()

    @property
    def repeat_queue(self):
        """
        Returns:
           Whether single track repition is enabled.
        """
        return self.queue.repeat_queue

    @repeat_queue.setter
    def repeat_queue(self, value):
        """
        Enables or disabled single track repition
        """
        self.queue.repeat_queue = value
        self.playback_flags_changed.fire()

    def get_queue_tracks(self):
        """
        Return :attr:`.Queue.get_tracks`
        """
        return self.queue.get_tracks()

    def play(self):
        """
        Pick the current track from the queue and requests the media stream url.
        Should complete in the background.
        """
        raise NotImplementedError

    def _download_track(self, url, error, track):
        if error:
            logger.error("failed to request media URL for track %s: %s",
                         track.original_data, str(error))
            return

        response = urlopen(url)
        path = settings_manager.save_file_to_cache(track.filename,
                                                   response.read())
        self._play_ready(path, None, track)

    @property
    def loading(self):
        return self._loading

    @loading.setter
    def loading(self, value):
        self._loading = value

    @property
    def playing(self):
        raise NotImplementedError

    # Implement as a setter instead?
    def play_pause(self):
        """
        Toggle playback, i.e. play if pause or pause if playing.
        """
        raise NotImplementedError

    @property
    def play_progress(self):
        """
        Return current playback position in range ``[0;1]`` (``float``)
        """
        raise NotImplementedError

    @property
    def play_progress_seconds(self):
        """
        Return the current playback position in seconds (``int``)
        """
        raise NotImplementedError

    @property
    def time(self):
        """
        Returns:
           Get their current movie length in microseconds
e        """
        raise NotImplementedError

    def _seeked(self):
        mpris2.mpris2_manager.Seeked.emit(self.time)

    @time.setter
    def time(self, time):
        """
        Sets the current time in microseconds.
        This is a pythonic alternative to seeking using absolute times instead of percentiles.

        Args:
           time: Time in microseconds.
        """
        raise NotImplementedError

    @property
    def volume(self):
        """
        Returns:
           The current volume of in percentiles (0 = mute, 100 = 0dB)
        """
        raise NotImplementedError

    @volume.setter
    def volume(self, volume):
        """
        Args:
           volume: the volume in percentiles (0 = mute, 1000 = 0dB)

        Returns:
           The current volume of in percentiles (0 = mute, 100 = 0dB)
        """
        raise NotImplementedError

    def mute(self):
        """
        Mutes or unmutes the volume
        """
        raise NotImplementedError

    @property
    def length(self):
        """
        Returns:
          The current playback position in microseconds
        """
        raise NotImplementedError

    @property
    def length_seconds(self):
        """
        Return currently played track's length in seconds (``int``).
        """
        raise NotImplementedError

    def next(self, force=False):
        """
        Advance to next track in queue.
        See :meth:`._Queue.next`
        """
        if self.queue.next(force):
            self.play()
        else:
            self.stop()

    def prev(self):
        """
        Advance to their previous track in their queue
        seek :meth:`._Queue.prev`
        """
        self.queue.prev()
        self.play()

    def get_current_track(self):
        """
        Return currently played track.
        See :meth:`._Queue.get_current_track`.
        """
        return self.queue.get_current_track()

    def seek(self, delta):
        """
        Seek to relative position.
        *delta* must be a ``float`` in range ``[-1;1]``.
        """
        raise NotImplementedError

    def seek_absolute(self, position):
        """
        Seek to absolute position.
        *position* must be a ``float`` in range ``[0;1]``.
        """
        raise NotImplementedError

    @staticmethod
    def get_equalizer_freqs():
        """
        Return a list of equalizer frequencies for each band.
        """
        raise NotImplementedError

    def get_equalizer_amps(self):
        """
        Return a list of equalizer amplifications for each band.
        """
        raise NotImplementedError

    def set_equalizer_value(self, index, amp):
        """
        Set equalizer amplification for specific band.
        """
        raise NotImplementedError

    def set_equalizer_values(self, amps):
        """
        Set a list of equalizer amplifications for each band.
        """
        raise NotImplementedError
예제 #3
0
class _GP(object):
    """
    Interface to :class:`gmusicapi.Mobileclient`. Implements
    asynchronous API calls, caching and some other perks.

    Singleton.
    """
    # TODO: Switch to urwid signals for more explicitness?
    caches_invalidated = EventHook()
    parsed_songs = EventHook()

    def __init__(self):
        # self.is_debug = os.getenv('CLAY_DEBUG')
        self.mobile_client = Mobileclient()
        self.mobile_client._make_call = self._make_call_proxy(
            self.mobile_client._make_call)
        # if self.is_debug:
        #     self.debug_file = open('/tmp/clay-api-log.json', 'w')
        #     self._last_call_index = 0
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artists = {}
        self.cached_albums = {}
        self.liked_songs = LikedSongs()

        self.invalidate_caches()

        self.auth_state_changed = EventHook()

    def _make_call_proxy(self, func):
        """
        Return a function that wraps *fn* and logs args & return values.
        """
        def _make_call(protocol, *args, **kwargs):
            """
            Wrapper function.
            """
            logger.debug('GP::{}(*{}, **{})'.format(protocol.__name__, args,
                                                    kwargs))
            result = func(protocol, *args, **kwargs)
            # self._last_call_index += 1
            # call_index = self._last_call_index
            # self.debug_file.write(json.dumps([
            #     call_index,
            #     protocol.__name__, args, kwargs,
            #     result
            # ]) + '\n')
            # self.debug_file.flush()
            return result

        return _make_call

    def invalidate_caches(self):
        """
        Clear cached tracks & playlists & stations.
        """
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artist = None
        self.caches_invalidated.fire()

    @synchronized
    def login(self, email, password, device_id, **_):
        """
        Log in into Google Play Music.
        """
        self.mobile_client.logout()
        self.invalidate_caches()
        from os.path import exists
        CRED_FILE = "/home/thor/.config/clay/google_auth.cred"
        if not exists(CRED_FILE):
            from oauth2client.client import FlowExchangeError
            try:
                self.mobile_client.perform_oauth(CRED_FILE, open_browser=True)
            except FlowExchangeError:
                raise RuntimeError("OAuth authentication failed, try again")
        result = self.mobile_client.oauth_login(
            self.mobile_client.FROM_MAC_ADDRESS, CRED_FILE)
        self.auth_state_changed.fire(self.is_authenticated)
        return result

    login_async = asynchronous(login)

    @synchronized
    def get_artist_info(self, artist_id):
        """
        Get the artist info
        """
        return self.mobile_client.get_artist_info(artist_id,
                                                  max_rel_artist=0,
                                                  max_top_tracks=15)

    @synchronized
    def get_album_tracks(self, album_id):
        """
        Get album tracks
        """
        return self.mobile_client.get_album_info(album_id,
                                                 include_tracks=True)['tracks']

    @synchronized
    def add_album_song(self, id_, album_name, track):
        """
        Adds an album to an artist and adds the specified track to it

        Args:
            id_ (`str`): the album ID (currently the same as the album title)
            album_name (`str`): the name of the album
            track (`clay.gp.Track`): the track in the album
        """
        if album_name == '':
            id_ = track.artist
            album_name = "Unknown Album"

        if id_ not in self.cached_albums:
            self.cached_albums[id_] = Album(track.album_artist, {
                'albumId': id_,
                'name': album_name
            })

        self.cached_albums[id_].add_track(track)

        return self.cached_albums[id_]

    @synchronized
    def add_artist(self, artist_id, name):
        """
        Creates or lookup an artist object and return it.

        Args:
           artist_id (`str`): The Artist id given by Google Play Music

        Returns:
           The artist class
        """
        name = ("Unknown Artist" if name == '' else name)
        lname = name.lower()
        if lname not in self.cached_artists:
            self.cached_artists[lname] = Artist(artist_id, name)

        return self.cached_artists[lname]

    @synchronized
    def use_authtoken(self, authtoken, device_id):
        """
        Try to use cached token to log into Google Play Music.
        """
        self.mobile_client.session._authtoken = authtoken
        self.mobile_client.session.is_authenticated = True
        self.mobile_client.android_id = device_id
        del self.mobile_client.is_subscribed
        if self.mobile_client.is_subscribed:
            self.auth_state_changed.fire(True)
            return True
        del self.mobile_client.is_subscribed
        self.mobile_client.android_id = None
        self.mobile_client.session.is_authenticated = False
        self.auth_state_changed.fire(False)
        return False

    use_authtoken_async = asynchronous(use_authtoken)

    def get_authtoken(self):
        """
        Return currently active auth token.
        """
        return self.mobile_client.session._authtoken

    @synchronized
    def get_all_tracks(self):
        """
        Cache and return all tracks from "My library".

        Each track will have "id" and "storeId" keys.
        """
        if self.cached_tracks:
            return self.cached_tracks
        data = self.mobile_client.get_all_songs()
        self.cached_tracks = Track.from_data(data, Source.library, True)
        self.parsed_songs.fire()

        return self.cached_tracks

    get_all_tracks_async = asynchronous(get_all_tracks)

    def get_stream_url(self, stream_id):
        """
        Returns playable stream URL of track by id.
        """
        return self.mobile_client.get_stream_url(stream_id)

    get_stream_url_async = asynchronous(get_stream_url)

    def increment_song_playcount(self, track_id):
        """
        increments the playcount of a song with a given `track_id` by one

        Args:
           track_id (`int`): The track id of the song to increment the playcount

        Returns:
           Nothing
        """
        gp.mobile_client.increment_song_playcount(track_id)

    increment_song_playcount_async = asynchronous(increment_song_playcount)

    @synchronized
    def get_all_user_station_contents(self, **_):
        """
              Return list of :class:`.Station` instances.
              """
        if self.cached_stations:
            return self.cached_stations
        self.get_all_tracks()

        self.cached_stations = Station.from_data(
            self.mobile_client.get_all_stations(), True)
        self.cached_stations.insert(0, IFLStation())
        return self.cached_stations

    get_all_user_station_contents_async = (
        asynchronous(get_all_user_station_contents))

    @synchronized
    def get_all_user_playlist_contents(self, **_):
        """
        Return list of :class:`.Playlist` instances.
        """
        if self.cached_playlists:
            return self.cached_playlists

        self.get_all_tracks()

        self.cached_playlists = Playlist.from_data(
            self.mobile_client.get_all_user_playlist_contents(), True)
        self.refresh_liked_songs()
        self.cached_playlists.insert(0, self.liked_songs)
        return self.cached_playlists

    get_all_user_playlist_contents_async = (
        asynchronous(get_all_user_playlist_contents))

    def refresh_liked_songs(self, **_):
        """
        Refresh the liked songs playlist
        """
        self.liked_songs.refresh_tracks(self.mobile_client.get_top_songs())

    refresh_liked_songs_async = asynchronous(refresh_liked_songs)

    def get_cached_tracks_map(self):
        """
        Return a dictionary of tracks where keys are strings with track IDs
        and values are :class:`.Track` instances.
        """
        return {track.id: track for track in self.cached_tracks}

    def get_track_by_id(self, any_id):
        """
        Return track by id or store_id.
        """
        for track in self.cached_tracks:
            if any_id in (track.id_, track.nid, track.store_id):
                return track
        return None

    def search(self, query):
        """
        Find tracks and return an instance of :class:`.SearchResults`.
        """
        results = self.mobile_client.search(query)
        return SearchResults.from_data(results)

    search_async = asynchronous(search)

    def add_to_my_library(self, track):
        """
        Add a track to my library.
        """
        result = self.mobile_client.add_store_tracks(track.id)
        if result:
            self.invalidate_caches()
        return result

    def remove_from_my_library(self, track):
        """
        Remove a track from my library.
        """
        result = self.mobile_client.delete_songs(track.id)
        if result:
            self.invalidate_caches()
        return result

    @property
    def is_authenticated(self):
        """
        Return True if user is authenticated on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_authenticated()

    @property
    def is_subscribed(self):
        """
        Return True if user is subscribed on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_subscribed
예제 #4
0
class _GP(object):
    """
    Interface to :class:`gmusicapi.Mobileclient`. Implements
    asynchronous API calls, caching and some other perks.

    Singleton.
    """
    # TODO: Switch to urwid signals for more explicitness?
    caches_invalidated = EventHook()

    def __init__(self):
        # self.is_debug = os.getenv('CLAY_DEBUG')
        self.mobile_client = Mobileclient()
        self.mobile_client._make_call = self._make_call_proxy(
            self.mobile_client._make_call)
        # if self.is_debug:
        #     self.debug_file = open('/tmp/clay-api-log.json', 'w')
        #     self._last_call_index = 0
        self.cached_tracks = None
        self.cached_liked_songs = LikedSongs()
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artists = {}
        self.cached_albums = {}

        self.invalidate_caches()

        self.auth_state_changed = EventHook()

    def _make_call_proxy(self, func):
        """
        Return a function that wraps *fn* and logs args & return values.
        """
        def _make_call(protocol, *args, **kwargs):
            """
            Wrapper function.
            """
            logger.debug('GP::{}(*{}, **{})'.format(protocol.__name__, args,
                                                    kwargs))
            result = func(protocol, *args, **kwargs)
            # self._last_call_index += 1
            # call_index = self._last_call_index
            # self.debug_file.write(json.dumps([
            #     call_index,
            #     protocol.__name__, args, kwargs,
            #     result
            # ]) + '\n')
            # self.debug_file.flush()
            return result

        return _make_call

    def invalidate_caches(self):
        """
        Clear cached tracks & playlists & stations.
        """
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artist = None
        self.caches_invalidated.fire()

    @synchronized
    def login(self, email, password, device_id, **_):
        """
        Log in into Google Play Music.
        """
        self.mobile_client.logout()
        self.invalidate_caches()
        # prev_auth_state = self.is_authenticated
        result = self.mobile_client.login(email, password, device_id)
        # if prev_auth_state != self.is_authenticated:
        self.auth_state_changed.fire(self.is_authenticated)
        return result

    login_async = asynchronous(login)

    @synchronized
    def get_artist_info(self, artist_id):
        """
        Get the artist info
        """
        return self.mobile_client.get_artist_info(artist_id,
                                                  max_rel_artist=0,
                                                  max_top_tracks=15)

    @synchronized
    def get_album_tracks(self, album_id):
        """
        Get album tracks
        """
        return self.mobile_client.get_album_info(album_id,
                                                 include_tracks=True)['tracks']

    @synchronized
    def add_artist(self, artist_id, name):
        """
        Creates or lookup an artist object and return it.

        Args:
           artist_id (`str`): The Artist id given by Google Play Music

        Returns:
           The artist class
        """
        lname = name.lower()
        if lname not in self.cached_artists:
            self.cached_artists[lname] = Artist(artist_id, name)

        return self.cached_artists[lname]

    @synchronized
    def use_authtoken(self, authtoken, device_id):
        """
        Try to use cached token to log into Google Play Music.
        """
        # pylint: disable=protected-access
        self.mobile_client.session._authtoken = authtoken
        self.mobile_client.session.is_authenticated = True
        self.mobile_client.android_id = device_id
        del self.mobile_client.is_subscribed
        if self.mobile_client.is_subscribed:
            self.auth_state_changed.fire(True)
            return True
        del self.mobile_client.is_subscribed
        self.mobile_client.android_id = None
        self.mobile_client.session.is_authenticated = False
        self.auth_state_changed.fire(False)
        return False

    use_authtoken_async = asynchronous(use_authtoken)

    def get_authtoken(self):
        """
        Return currently active auth token.
        """
        # pylint: disable=protected-access
        return self.mobile_client.session._authtoken

    @synchronized
    def get_all_tracks(self):
        """
        Cache and return all tracks from "My library".

        Each track will have "id" and "storeId" keys.
        """
        if self.cached_tracks:
            return self.cached_tracks
        data = self.mobile_client.get_all_songs()
        self.cached_tracks = Track.from_data(data, Source.library, True)

        return self.cached_tracks

    get_all_tracks_async = asynchronous(get_all_tracks)

    def get_stream_url(self, stream_id):
        """
        Returns playable stream URL of track by id.
        """
        return self.mobile_client.get_stream_url(stream_id)

    get_stream_url_async = asynchronous(get_stream_url)

    @synchronized
    def get_all_user_station_contents(self, **_):
        """
              Return list of :class:`.Station` instances.
              """
        if self.cached_stations:
            return self.cached_stations
        self.get_all_tracks()

        self.cached_stations = Station.from_data(
            self.mobile_client.get_all_stations(), True)
        self.cached_stations.insert(0, IFLStation())
        return self.cached_stations

    get_all_user_station_contents_async = (  # pylint: disable=invalid-name
        asynchronous(get_all_user_station_contents))

    @synchronized
    def get_all_user_playlist_contents(self, **_):
        """
        Return list of :class:`.Playlist` instances.
        """
        if self.cached_playlists:
            return [self.cached_liked_songs] + self.cached_playlists

        self.get_all_tracks()

        self.cached_playlists = Playlist.from_data(
            self.mobile_client.get_all_user_playlist_contents(), True)
        return [self.cached_liked_songs] + self.cached_playlists

    get_all_user_playlist_contents_async = (  # pylint: disable=invalid-name
        asynchronous(get_all_user_playlist_contents))

    def get_cached_tracks_map(self):
        """
        Return a dictionary of tracks where keys are strings with track IDs
        and values are :class:`.Track` instances.
        """
        return {track.id: track for track in self.cached_tracks}

    def get_track_by_id(self, any_id):
        """
        Return track by id or store_id.
        """
        for track in self.cached_tracks:
            if any_id in (track.library_id, track.store_id,
                          track.playlist_item_id):
                return track
        return None

    def search(self, query):
        """
        Find tracks and return an instance of :class:`.SearchResults`.
        """
        results = self.mobile_client.search(query)
        return SearchResults.from_data(results)

    search_async = asynchronous(search)

    def add_to_my_library(self, track):
        """
        Add a track to my library.
        """
        result = self.mobile_client.add_store_tracks(track.id)
        if result:
            self.invalidate_caches()
        return result

    def remove_from_my_library(self, track):
        """
        Remove a track from my library.
        """
        result = self.mobile_client.delete_songs(track.id)
        if result:
            self.invalidate_caches()
        return result

    @property
    def is_authenticated(self):
        """
        Return True if user is authenticated on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_authenticated()

    @property
    def is_subscribed(self):
        """
        Return True if user is subscribed on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_subscribed