class MobileClientWrapper(_Base): def __init__(self, log=False, quiet=False): self.api = Mobileclient(debug_logging=log) self.api.logger.addHandler(logging.NullHandler()) self.print_ = safe_print if not quiet else lambda *args, **kwargs: None def login(self, *args): """Authenticate the gmusicapi Mobileclient instance.""" if len(args) == 2: username, password = args[0], args[1] elif len(args) == 1: username, password = args[0], None else: username, password = None, None if not self.api.login(username, password): if not username: username = raw_input("Enter your Google username or email address: ") if not password: password = getpass.getpass(b"Enter your Google Music password: "******"Sorry, login failed.") return False self.print_("Successfully logged in.\n") return True def logout(self): """Log out the gmusicapi Mobileclient instance.""" self.api.logout() def get_google_songs(self, filters=None, filter_all=False): """Load song list from Google Music library.""" self.print_("Loading Google Music songs...") google_songs = [] filter_songs = [] songs = self.api.get_all_songs() google_songs, filter_songs = match_filters_google(songs, filters, filter_all) self.print_("Filtered {0} Google Music songs".format(len(filter_songs))) self.print_("Loaded {0} Google Music songs\n".format(len(google_songs))) return google_songs
def exportlib(user, password): """Logs into Google Music and exports the user's library to a file called 'export.json'. """ keys = ['comment', 'rating', 'composer', 'year', 'album', 'albumArtist', 'title', 'totalDiscCount', 'trackNumber', 'discNumber', 'totalTrackCount', 'estimatedSize', 'beatsPerMinute', 'genre', 'playCount', 'artist', 'durationMillis'] client = Mobileclient() client.login(user, password) with open('export.json', 'w+') as out: for songs in client.get_all_songs(incremental=True): for song in songs: pruned = select_keys(song, keys) print(json.dumps(pruned), file=out)
class GMusic(object): def __init__(self, user, password): self.client = Mobileclient() self.client.login(user, password, Mobileclient.FROM_MAC_ADDRESS) def genTracks(self): for chunk in self.client.get_all_songs(incremental=True): for track in chunk: yield track def findTrack(self, rdio_track, keys=('artist', 'album', 'name',)): if not keys: return results = self.client.search_all_access(' '.join(rdio_track[k] for k in keys))['song_hits'] if not results: return self.findTrack(rdio_track, keys[1:]) # FIXME: is the best match always first? best_match = results[0] return best_match['track'] def addTrack(self, google_track): self.client.add_aa_track(google_track['nid'])
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 _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 PlaylistSync: def __init__(self, root, playlist_name): self.root = root self.playlist_name = playlist_name def _login_mc(self): APP_NAME = 'gmusic-sync-playlist' CONFIG_FILE = 'auth.cfg' config = SafeConfigParser({ 'username': '', 'device_id': '' }) config.read(CONFIG_FILE) if not config.has_section('auth'): config.add_section('auth') username = config.get('auth','username') password = None if username != '': password = keyring.get_password(APP_NAME, username) if password == None or not self.mc.login(username, password): while 1: username = raw_input("Username: "******"Password: "******"Sign-on failed." config.set('auth', 'username', username) with open(CONFIG_FILE, 'wb') as f: config.write(f) keyring.set_password(APP_NAME, username, password) device_id = config.get('auth', 'device_id') if device_id == '': wc = Webclient() if not wc.login(username, password): raise Exception('could not log in via Webclient') devices = wc.get_registered_devices() mobile_devices = [d for d in devices if d[u'type'] in (u'PHONE', u'IOS')] if len(mobile_devices) < 1: raise Exception('could not find any registered mobile devices') device_id = mobile_devices[0][u'id'] if device_id.startswith(u'0x'): device_id = device_id[2:] config.set('auth', 'device_id', device_id) with open(CONFIG_FILE, 'wb') as f: config.write(f) print('Device ID: {}'.format(device_id)) self.mc.device_id = device_id def login(self): self.mc = Mobileclient() self._login_mc() self.mm = Musicmanager() #self.mm.perform_oauth() self.mm.login() def track_file_name(self, track): if 'albumArtist' in track: albumartist = track['albumArtist'] else: albumartist = 'Various' if not albumartist: albumartist = 'Various' file_name = escape_path(u'{trackNumber:02d} {title}.mp3'.format(**track)) if track.get('totalDiscCount', 1) > 1: file_name = u'{discNumber}-'.format(**track) + file_name return unicodedata.normalize('NFD', os.path.join(self.root, escape_path(albumartist), escape_path(track['album']), file_name)) def get_local_tracks(self): # return (metadata, file_name) of all files in root tracks = [] for root, dirs, files in os.walk(self.root): for f in files: if os.path.splitext(f)[1].lower() == '.mp3': file_name = os.path.join(root, f) #id3 = EasyID3(file_name) track = {} #track = { # 'name': id3['title'], # 'album': id3['album'], # 'track': id3['tracknumber'], # 'disc': id3['discnumber'] #} yield unicodedata.normalize('NFD', file_name.decode('utf-8')), track def get_playlist_tracks(self): # return (metadata, local_file_name) for each track in playlist all_playlists = self.mc.get_all_playlists() try: playlist = next(p for p in all_playlists if p['name'] == self.playlist_name) except StopIteration: raise Exception('playlist "{0}" not found'.format(self.playlist_name)) contents = self.mc.get_shared_playlist_contents(playlist['shareToken']) for t in contents: track = t[u'track'] #pprint(track) #raw_input() yield (self.track_file_name(track), track) #for p in all_playlists: # shared = self.mc.get_shared_playlist_contents(p['shareToken']) # pprint(shared) #for p in self.mc.get_all_user_playlist_contents(): # del p['tracks'] # pprint(p) # raw_input() return all_songs = self.mc.get_all_songs() pprint(all_songs[0]) for p in self.mc.get_all_user_playlist_contents(): if p['name'] == self.playlist_name: for track in p['tracks']: song = next(s for s in all_songs if s['id'] == track['trackId']) print(u'{album} - {title}'.format(**song)) #pprint(song) yield self.track_file_name(song), song def add_track(self, track, file_name): # download track from gmusic, write to file_name if not os.path.exists(os.path.dirname(file_name)): os.makedirs(os.path.dirname(file_name)) if track[u'kind'] != u'sj#track' or u'id' not in track: if u'id' not in track: track[u'id'] = track[u'storeId'] url = self.mc.get_stream_url(track[u'id'], self.mc.device_id) r = requests.get(url) data = r.content #data = self.wc.get_stream_audio(track['id']) with open(file_name, 'wb') as f: f.write(data) _copy_track_metadata(file_name, track) else: fn, audio = self.mm.download_song(track['id']) with open(file_name, 'wb') as f: f.write(audio) def remove_track(self, file_name): """Removes the track and walks up the tree deleting empty folders """ os.remove(file_name) rel = os.path.relpath(file_name, self.root) dirs = os.path.split(rel)[0:-1] for i in xrange(1, len(dirs) + 1): dir_path = os.path.join(self.root, *dirs[0:i]) if not os.listdir(dir_path): os.unlink(dir_path) def sync(self, confirm=True, remove=False): print 'Searching for local tracks ...' local = dict(self.get_local_tracks()) print 'Getting playlist ...' playlist = OrderedDict(self.get_playlist_tracks()) to_add = [] to_remove = [] to_rename = [] for file_name, track in playlist.iteritems(): if file_name not in local and file_name.encode('ascii', 'replace').replace('?','_') not in local: to_add.append((track, file_name)) elif file_name not in local and file_name.encode('ascii', 'replace').replace('?','_') in local: to_rename.append((file_name.encode('ascii', 'replace').replace('?','_'), file_name)) if remove: for file_name, track in sorted(local.iteritems()): if file_name not in playlist: to_remove.append((track, file_name)) if to_remove: print 'Deleting tracks:' for track, file_name in to_remove: print ' ' + file_name print '' if to_add: to_add = list(reversed(to_add)) print 'Adding tracks:' for track, file_name in to_add: print ' ' + file_name print '' if to_rename: print 'Renaming tracks:' for src, dst in to_rename: print ' {0} to {1}'.format(src, dst) print '' if not (to_add or to_remove): print 'Nothing to do.' print '' if confirm: raw_input('Press enter to proceed') for src, dst in to_rename: if not os.path.exists(os.path.dirname(dst)): os.makedirs(os.path.dirname(dst)) shutil.move(src, dst) for track, file_name in to_remove: print 'removing track ' + file_name self.remove_track(file_name) for track, file_name in to_add: print u'adding track: {album} / \n {title}'.format(**track).encode('utf-8', 'replace') self.add_track(track, file_name)
class MobileClientWrapper(_Base): """Wraps gmusicapi's Mobileclient client interface to provide extra functionality and conveniences.""" def __init__(self, log=False): """ :param log: Enable gmusicapi's debug_logging option. """ self.api = Mobileclient(debug_logging=log) self.api.logger.addHandler(logging.NullHandler()) def login(self, username=None, password=None, android_id=None): """Authenticate the gmusicapi Mobileclient instance. Returns ``True`` on successful login or ``False`` on unsuccessful login. :param username: (Optional) Your Google Music username. Will be prompted if not given. :param password: (Optional) Your Google Music password. Will be prompted if not given. :param android_id: (Optional) The 16 hex digits from an Android device ID. Default: Use gmusicapi.Mobileclient.FROM_MAC_ADDRESS to create ID from computer's MAC address. """ if not username: username = raw_input("Enter your Google username or email address: ") if not password: password = getpass.getpass(b"Enter your Google Music password: "******"Sorry, login failed.") return False logger.info("Successfully logged in.\n") return True def logout(self): """Log out the gmusicapi Mobileclient instance. Returns ``True`` on success. """ return self.api.logout() def get_google_songs(self, include_filters=None, exclude_filters=None, all_include_filters=False, all_exclude_filters=False): """Create song list from user's Google Music library using gmusicapi's Mobileclient.get_all_songs(). Returns a list of Google Music song dicts matching criteria and a list of Google Music song dicts filtered out using filter criteria. :param include_filters: A list of ``(field, pattern)`` tuples. Fields are any valid Google Music metadata field available to the Musicmanager client. Patterns are Python regex patterns. Google Music songs are filtered out if the given metadata field values don't match any of the given patterns. :param exclude_filters: A list of ``(field, pattern)`` tuples. Fields are any valid Google Music metadata field available to the Musicmanager client. Patterns are Python regex patterns. Google Music songs are filtered out if the given metadata field values match any of the given patterns. :param all_include_filters: If ``True``, all include_filters criteria must match to include a song. :param all_exclude_filters: If ``True``, all exclude_filters criteria must match to exclude a song. """ logger.info("Loading Google Music songs...") google_songs = self.api.get_all_songs() if include_filters or exclude_filters: matched_songs, filtered_songs = filter_google_songs( google_songs, include_filters, exclude_filters, all_include_filters, all_exclude_filters ) else: matched_songs = google_songs filtered_songs = [] logger.info("Filtered {0} Google Music songs".format(len(filtered_songs))) logger.info("Loaded {0} Google Music songs".format(len(matched_songs))) return matched_songs, filtered_songs
class MobileClientWrapper(_Base): def __init__(self, log=False, quiet=False): self.api = Mobileclient(debug_logging=log) self.api.logger.addHandler(logging.NullHandler()) self.print_ = safe_print if not quiet else lambda *args, **kwargs: None def login(self, *args): """Authenticate the gmusicapi Mobileclient instance.""" if len(args) == 2: username, password = args[0], args[1] elif len(args) == 1: username, password = args[0], None else: username, password = None, None if not self.api.login(username, password): if not username: username = raw_input( "Enter your Google username or email address: ") if not password: password = getpass.getpass( b"Enter your Google Music password: "******"Sorry, login failed.") return False self.print_("Successfully logged in.\n") return True def logout(self): """Log out the gmusicapi Mobileclient instance.""" self.api.logout() def get_google_songs(self, filters=None, filter_all=False): """Load song list from Google Music library.""" self.print_("Loading Google Music songs...") google_songs = [] filter_songs = [] songs = self.api.get_all_songs() google_songs, filter_songs = match_filters_google( songs, filters, filter_all) self.print_("Filtered {0} Google Music songs".format( len(filter_songs))) self.print_("Loaded {0} Google Music songs\n".format( len(google_songs))) return google_songs