def __init__(self): self.logs = [] self.logfile = open('/tmp/clay.log', 'w') self._lock = Lock() self.on_log_event = EventHook()
def __init__(self): assert self.__class__.instance is None, 'Can be created only once!' self.is_debug = os.getenv('CLAY_DEBUG') self.mobile_client = Mobileclient() if self.is_debug: self.mobile_client._make_call = self._make_call_proxy( self.mobile_client._make_call) self.debug_file = open('/tmp/clay-api-log.json', 'w') self._last_call_index = 0 self.cached_tracks = None self.cached_playlists = None self.invalidate_caches() self.auth_state_changed = 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.invalidate_caches() self.auth_state_changed = EventHook()
def __init__(self): assert self.__class__.instance is None, 'Can be created only once!' self.hotkeys = {} self.config = None self.play_pause = EventHook() self.next = EventHook() self.prev = EventHook() if IS_INIT: Keybinder.init() self.initialize() threading.Thread(target=Gtk.main).start() else: NotificationArea.notify( 'Could not import Keybinder and Gtk. Error was: "{}"\n' 'Global shortcuts will not work.'.format(ERROR))
def __init__(self): self._x_hotkeys = {} self._hotkeys = self._parse_hotkeys() self.config = None self.play_pause = EventHook() self.next = EventHook() self.prev = EventHook() if IS_INIT: Keybinder.init() self.initialize() threading.Thread(target=Gtk.main).start() else: logger.debug("Not loading the global shortcuts.") notification_area.notify( ERROR_MESSAGE + ", this means the global shortcuts will not work.\n" + "You can check the log for more details.")
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.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.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 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, Track.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) def increment_song_playcount(self, track_id): """ Increments the playcount of the song given by track_id by one. """ return 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) 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
class _Player(object): """ Interface to libVLC. Uses Queue as a playback plan. Emits various events if playback state, tracks or play flags change. Singleton. """ media_position_changed = EventHook() media_state_changed = EventHook() track_changed = EventHook() playback_flags_changed = EventHook() queue_changed = EventHook() track_appended = EventHook() track_removed = EventHook() def __init__(self): self.instance = vlc.Instance() print_func = CFUNCTYPE( c_void_p, c_void_p, # data c_int, # level c_void_p, # context c_char_p, # fmt c_void_p) # args self.instance.log_set(print_func(_dummy_log), None) self.instance.set_user_agent(meta.APP_NAME, meta.USER_AGENT) self.media_player = self.instance.media_player_new() self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerPlaying, self._media_state_changed) self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerPaused, self._media_state_changed) self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerEndReached, self._media_end_reached) self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerPositionChanged, self._media_position_changed) self.equalizer = vlc.libvlc_audio_equalizer_new() self.media_player.set_equalizer(self.equalizer) self._create_station_notification = None self._is_loading = False self.queue = _Queue() def enable_xorg_bindings(self): """Enable the global X bindings using keybinder""" if os.environ.get("DISPLAY") is None: logger.debug( "X11 isn't running so we can't load the global keybinds") return from clay.hotkeys import hotkey_manager hotkey_manager.play_pause += self.play_pause hotkey_manager.next += self.next hotkey_manager.prev += lambda: self.seek_absolute(0) 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.is_loading, playing=self.is_playing, artist=track.artist, title=track.title, progress=self.get_play_progress_seconds(), length=self.get_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 _media_state_changed(self, event): """ Called when a libVLC playback state changes. Broadcasts playback state & fires :attr:`media_state_changed` event. """ assert event self.broadcast_state() self.media_state_changed.fire(self.is_loading, self.is_playing) def _media_end_reached(self, event): """ Called when end of currently played track is reached. Increments the play count and advances to the next track. """ assert event self.queue.get_current_track().increment_playcount() self.next() def _media_position_changed(self, event): """ Called when playback position changes (this happens few times each second.) Fires :attr:`.media_position_changed` event. """ assert event self.broadcast_state() self.media_position_changed.fire(self.get_play_progress()) def load_queue(self, data, current_index=None): """ Load queue & start playback. Fires :attr:`.queue_changed` event. See :meth:`._Queue.load`. """ self.queue.load(data, current_index) 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) # self.queue_changed.fire() def remove_from_queue(self, track): """ Remove track from queue. Fires :attr:`.track_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. """ self._create_station_notification = notification_area.notify( 'Creating station...') track.create_station_async( callback=self._create_station_from_track_ready) def _create_station_from_track_ready(self, station, error): """ Called when a station is created. If *error* is ``None``, load new station's tracks into queue. """ if error: self._create_station_notification.update( 'Failed to create station: {}'.format(str(error))) return if not station.get_tracks(): self._create_station_notification.update( 'Newly created station is empty :(') return self.load_queue(station.get_tracks()) self._create_station_notification.update('Station ready!') def get_is_random(self): """ Return ``True`` if track selection from queue is randomed, ``False`` otherwise. """ return self.queue.random def get_is_repeat_one(self): """ Return ``True`` if track repetition in queue is enabled, ``False`` otherwise. """ return self.queue.repeat_one def set_random(self, value): """ Enable/disable random track selection. """ self.queue.random = value self.playback_flags_changed.fire() def set_repeat_one(self, value): """ Enable/disable track repetition. """ self.queue.repeat_one = 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 current track from a queue and requests media stream URL. Completes in background. """ track = self.queue.get_current_track() if track is None: return self._is_loading = True self.broadcast_state() self.track_changed.fire(track) if settings.get('download_tracks', 'play_settings') or \ settings.get_is_file_cached(track.filename): path = settings.get_cached_file_path(track.filename) if path is None: logger.debug('Track %s not in cache, downloading...', track.store_id) track.get_url(callback=self._download_track) else: logger.debug('Track %s in cache, playing', track.store_id) self._play_ready(path, None, track) else: logger.debug('Starting to stream %s', track.store_id) track.get_url(callback=self._play_ready) def _download_track(self, url, error, track): if error: notification_area.notify('Failed to request media URL: {}'.format( str(error))) logger.error('Failed to request media URL for track %s: %s', track.original_data, str(error)) return response = urlopen(url) path = settings.save_file_to_cache(track.filename, response.read()) self._play_ready(path, None, track) def _play_ready(self, url, error, track): """ Called once track's media stream URL request completes. If *error* is ``None``, tell libVLC to play media by *url*. """ self._is_loading = False if error: notification_area.notify('Failed to request media URL: {}'.format( str(error))) logger.error('Failed to request media URL for track %s: %s', track.original_data, str(error)) return assert track media = vlc.Media(url) self.media_player.set_media(media) self.media_player.play() osd_manager.notify(track) @property def is_loading(self): """ True if current libVLC state is :attr:`vlc.State.Playing` """ return self._is_loading @property def is_playing(self): """ True if current libVLC state is :attr:`vlc.State.Playing` """ return self.media_player.get_state() == vlc.State.Playing def play_pause(self): """ Toggle playback, i.e. play if paused or pause if playing. """ if self.is_playing: self.media_player.pause() else: self.media_player.play() def get_play_progress(self): """ Return current playback position in range ``[0;1]`` (``float``). """ return self.media_player.get_position() def get_play_progress_seconds(self): """ Return current playback position in seconds (``int``). """ return int(self.media_player.get_position() * self.media_player.get_length() / 1000) def get_length_seconds(self): """ Return currently played track's length in seconds (``int``). """ return int(self.media_player.get_length() // 1000) def next(self, force=False): """ Advance to next track in queue. See :meth:`._Queue.next`. """ self.queue.next(force) self._play() def prev(self, force=False): """ Advance to their previous track in their queue seek :meth:`._Queue.prev` """ self.queue.prev(force) 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]``. """ self.media_player.set_position(self.get_play_progress() + delta) def seek_absolute(self, position): """ Seek to absolute position. *position* must be a ``float`` in range ``[0;1]``. """ self.media_player.set_position(position) @staticmethod def get_equalizer_freqs(): """ Return a list of equalizer frequencies for each band. """ return [ vlc.libvlc_audio_equalizer_get_band_frequency(index) for index in range(vlc.libvlc_audio_equalizer_get_band_count()) ] def get_equalizer_amps(self): """ Return a list of equalizer amplifications for each band. """ return [ vlc.libvlc_audio_equalizer_get_amp_at_index(self.equalizer, index) for index in range(vlc.libvlc_audio_equalizer_get_band_count()) ] def set_equalizer_value(self, index, amp): """ Set equalizer amplification for specific band. """ assert vlc.libvlc_audio_equalizer_set_amp_at_index( self.equalizer, amp, index) == 0 self.media_player.set_equalizer(self.equalizer) def set_equalizer_values(self, amps): """ Set a list of equalizer amplifications for each band. """ assert len(amps) == vlc.libvlc_audio_equalizer_get_band_count() for index, amp in enumerate(amps): assert vlc.libvlc_audio_equalizer_set_amp_at_index( self.equalizer, amp, index) == 0 self.media_player.set_equalizer(self.equalizer)
class _Logger(object): """ Global logger. Allows subscribing to log events. """ def __init__(self): self.logs = [] self.logfile = open('/tmp/clay.log', 'w') self._lock = Lock() self.on_log_event = EventHook() def log(self, level, message, *args): """ Add log item. """ self._lock.acquire() try: logger_record = _LoggerRecord(level, message, args) self.logs.append(logger_record) self.logfile.write('{} {:8} {}\n'.format( logger_record.formatted_timestamp, logger_record.verbosity, logger_record.formatted_message)) self.logfile.flush() self.on_log_event.fire(logger_record) finally: self._lock.release() def debug(self, message, *args): """ Add debug log item. """ self.log('DEBUG', message, *args) def info(self, message, *args): """ Add info log item. """ self.log('INFO', message, *args) def warn(self, message, *args): """ Add warning log item. """ self.log('WARNING', message, *args) warning = warn def error(self, message, *args): """ Add error log item. """ self.log('ERROR', message, *args) def get_logs(self): """ Return all logs. """ return self.logs
class Player(object): """ Interface to libVLC. Uses Queue as a playback plan. Emits various events if playback state, tracks or play flags change. Singleton. """ instance = None media_position_changed = EventHook() media_state_changed = EventHook() track_changed = EventHook() playback_flags_changed = EventHook() queue_changed = EventHook() track_appended = EventHook() track_removed = EventHook() def __init__(self): assert self.__class__.instance is None, 'Can be created only once!' self.instance = vlc.Instance() self.instance.set_user_agent( meta.APP_NAME, meta.USER_AGENT ) self.media_player = self.instance.media_player_new() self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerPlaying, self._media_state_changed ) self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerPaused, self._media_state_changed ) self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerEndReached, self._media_end_reached ) self.media_player.event_manager().event_attach( vlc.EventType.MediaPlayerPositionChanged, self._media_position_changed ) self.equalizer = vlc.libvlc_audio_equalizer_new() self.media_player.set_equalizer(self.equalizer) hotkey_manager = HotkeyManager.get() hotkey_manager.play_pause += self.play_pause hotkey_manager.next += self.next hotkey_manager.prev += lambda: self.seek_absolute(0) self._create_station_notification = None self._is_loading = False self.queue = Queue() @classmethod def get(cls): """ Create new :class:`.Player` instance or return existing one. """ if cls.instance is None: cls.instance = Player() return cls.instance 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.is_loading, playing=self.is_playing, artist=track.artist, title=track.title, progress=self.get_play_progress_seconds(), length=self.get_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 _media_state_changed(self, event): """ Called when a libVLC playback state changes. Broadcasts playback state & fires :attr:`media_state_changed` event. """ assert event self.broadcast_state() self.media_state_changed.fire(self.is_loading, self.is_playing) def _media_end_reached(self, event): """ Called when end of currently played track is reached. Advances to the next track. """ assert event self.next() def _media_position_changed(self, event): """ Called when playback position changes (this happens few times each second.) Fires :attr:`.media_position_changed` event. """ assert event self.broadcast_state() self.media_position_changed.fire( self.get_play_progress() ) def load_queue(self, data, current_index=None): """ Load queue & start playback. Fires :attr:`.queue_changed` event. See :meth:`.Queue.load`. """ self.queue.load(data, current_index) 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) # self.queue_changed.fire() def remove_from_queue(self, track): """ Remove track from queue. Fires :attr:`.track_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. """ self._create_station_notification = NotificationArea.notify('Creating station...') track.create_station_async(callback=self._create_station_from_track_ready) def _create_station_from_track_ready(self, station, error): """ Called when a station is created. If *error* is ``None``, load new station's tracks into queue. """ if error: self._create_station_notification.update( 'Failed to create station: {}'.format(str(error)) ) return if not station.get_tracks(): self._create_station_notification.update( 'Newly created station is empty :(' ) return self.load_queue(station.get_tracks()) self._create_station_notification.update('Station ready!') def get_is_random(self): """ Return ``True`` if track selection from queue is randomed, ``False`` otherwise. """ return self.queue.random def get_is_repeat_one(self): """ Return ``True`` if track repetition in queue is enabled, ``False`` otherwise. """ return self.queue.repeat_one def set_random(self, value): """ Enable/disable random track selection. """ self.queue.random = value self.playback_flags_changed.fire() def set_repeat_one(self, value): """ Enable/disable track repetition. """ self.queue.repeat_one = 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 current track from a queue and requests media stream URL. Completes in background. """ track = self.queue.get_current_track() if track is None: return self._is_loading = True track.get_url(callback=self._play_ready) self.broadcast_state() self.track_changed.fire(track) def _play_ready(self, url, error, track): """ Called once track's media stream URL request completes. If *error* is ``None``, tell libVLC to play media by *url*. """ self._is_loading = False if error: NotificationArea.notify('Failed to request media URL: {}'.format(str(error))) return assert track media = vlc.Media(url) self.media_player.set_media(media) self.media_player.play() @property def is_loading(self): """ True if current libVLC state is :attr:`vlc.State.Playing` """ return self._is_loading @property def is_playing(self): """ True if current libVLC state is :attr:`vlc.State.Playing` """ return self.media_player.get_state() == vlc.State.Playing def play_pause(self): """ Toggle playback, i.e. play if paused or pause if playing. """ if self.is_playing: self.media_player.pause() else: self.media_player.play() def get_play_progress(self): """ Return current playback position in range ``[0;1]`` (``float``). """ return self.media_player.get_position() def get_play_progress_seconds(self): """ Return current playback position in seconds (``int``). """ return int(self.media_player.get_position() * self.media_player.get_length() / 1000) def get_length_seconds(self): """ Return currently played track's length in seconds (``int``). """ return int(self.media_player.get_length() // 1000) def next(self, force=False): """ Advance to next track in queue. See :meth:`.Queue.next`. """ self.queue.next(force) 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]``. """ self.media_player.set_position(self.get_play_progress() + delta) def seek_absolute(self, position): """ Seek to absolute position. *position* must be a ``float`` in range ``[0;1]``. """ self.media_player.set_position(position) @staticmethod def get_equalizer_freqs(): """ Return a list of equalizer frequencies for each band. """ return [ vlc.libvlc_audio_equalizer_get_band_frequency(index) for index in range(vlc.libvlc_audio_equalizer_get_band_count()) ] def get_equalizer_amps(self): """ Return a list of equalizer amplifications for each band. """ return [ vlc.libvlc_audio_equalizer_get_amp_at_index( self.equalizer, index ) for index in range(vlc.libvlc_audio_equalizer_get_band_count()) ] def set_equalizer_value(self, index, amp): """ Set equalizer amplification for specific band. """ assert vlc.libvlc_audio_equalizer_set_amp_at_index( self.equalizer, amp, index ) == 0 self.media_player.set_equalizer(self.equalizer) def set_equalizer_values(self, amps): """ Set a list of equalizer amplifications for each band. """ assert len(amps) == vlc.libvlc_audio_equalizer_get_band_count() for index, amp in enumerate(amps): assert vlc.libvlc_audio_equalizer_set_amp_at_index( self.equalizer, amp, index ) == 0 self.media_player.set_equalizer(self.equalizer)