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()
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
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
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