def download_album(username, password, artist_name, album_name): api = Mobileclient() api.login(username, password, Mobileclient.FROM_MAC_ADDRESS) library = api.get_all_songs() songs = [s for s in library if s['albumArtist'] == artist_name and s['album'] == album_name] if len(songs) == 0: print('Error: Album not found', file=sys.stderr) return device_id = api.get_registered_devices()[0]['id'].replace('0x', '') dname = slugify(unicode(album_name)) os.mkdir(dname) # download songs for song in tqdm(songs, desc='Downloading'): fname = slugify(song['title']) mpg_name = os.path.join(dname, fname + '.mpg') mp3_name = os.path.join(dname, fname + '.mp3') url = api.get_stream_url(song['id'], device_id=device_id) response = requests.get(url) # first save as MPEG video with open(mpg_name, 'wb') as fout: for chunk in response.iter_content(chunk_size=128): fout.write(chunk) # call FFMPEG to convert to MP3 os.system(' '.join([FFMPEG_CMD] + FFMPEG_ARGS).format( input=mpg_name, output=mp3_name, title=song['title'], artist=song['albumArtist'], album=song['album'], track=song['trackNumber'])) os.remove(mpg_name) # download album art art_name = os.path.join(dname, dname + '.png') album_info = api.get_album_info(songs[0]['albumId'], include_tracks=False) response = requests.get(album_info['albumArtRef']) t = magic.from_buffer(response.content, mime=True) if t == 'image/jpeg': ext = '.jpg' elif t == 'image/png': ext = '.png' else: print('Unknown MIME type: {}'.format(t), file=sys.stderr) ext = '.wat' with open(os.path.join(dname, dname + ext), 'wb') as fout: fout.write(response.content)
def _get_global_tracks(api: gmusicapi.Mobileclient, artist_ids, album_ids): artist_ids = list(artist_ids) album_ids = list(album_ids) for artist_id in artist_ids: results = api.get_artist_info(artist_id) for album_stub in results['albums']: album_id = album_stub['albumId'] album_ids.append(album_id) for album_id in album_ids: album = api.get_album_info(album_id) yield from album['tracks']
class GMusicClient(ContentConsumer): """Element in charge of interfacing with GMusicApi Client""" def __init__(self, data_cache): self.client = Mobileclient() self.data_cache = data_cache def login(self): """Use data/unlocked/credentials.json to log in""" mac = Mobileclient.FROM_MAC_ADDRESS try: credentials = json.load(open("data/unlocked/credentials.json", "r")) result = self.client.login(credentials["username"], credentials["password"], mac) if result == False: print("\n\033[93mLogin failed.") print("Please double check that data/unlocked/credentials.json has correct information.\033[m\n") os._exit(1) except: print("\n\033[93mdata/unlocked/credentials.json is not valid.") print("You may need to delete and regnerate the credentials file.\033[m\n") exit(1) def load_my_library(self): """Load user's songs, playlists, and stations""" track_load = threading.Thread(target=self.load_tracks) radio_load = threading.Thread(target=self.load_radios) playlist_load = threading.Thread(target=self.load_playlists) track_load.start() radio_load.start() playlist_load.start() track_load.join() radio_load.join() playlist_load.join() def load_playlists(self): playlists = self.client.get_all_user_playlist_contents() playlists.reverse() self.data_cache.playlists = playlists def load_tracks(self): self.data_cache.tracks = [t for t in self.client.get_all_songs() if "nid" in t] def scrape_song(self, track): return track def load_radios(self): radios = self.client.get_all_stations() radios.reverse() self.data_cache.radios = radios def get_radio_contents(self, radio_id): tracks = self.client.get_station_tracks(radio_id) return tracks def get_radio_list(self, name): return [r for r in self.data_cache.radios if name in r["name"]] def filter(self, element, field, filter_by): return [e for e in element if filter_by in e[field]] def get_playlist_list(self, name): return self.filter(self.data_cache.playlists, "name", name) def search_all_access(self, query): return self.client.search_all_access(query) def create_radio(self, seed_type, id, name): """Create a radio""" # This is a weird way to do this, but there's no other choice ids = {"track": None, "album": None, "artist": None} seed_id_name = self.get_type_name(seed_type) ids[seed_id_name] = id return self.client.create_station( name=name, track_id=ids["track"], album_id=ids["album"], artist_id=ids["artist"] ) def search_items_all_access(self, type, query): """Searches Albums, Artists, and Songs; uses metaprogramming""" index_arguments = self.get_index_arguments(type) items = self.search_all_access(query)["{0}_hits".format(type[:-1])] return [self.format_item(item, type, index_arguments) for item in items] def get_sub_items(self, type_from, search_type, from_id): """Here type_from refers to artist or album we're indexing against""" args = self.get_index_arguments(search_type) if type_from == "playlist": return self.get_playlist_contents(from_id) else: # Get the appropriate search method and execute it search_method_name = "get_{0}_info".format(type_from) search_method = getattr(self.client, search_method_name) try: items = search_method(from_id, True)[args["type"] + "s"] # True here includes subelements except: # User passed in a bad id or something return # Now return appropriately return [self.format_subitems(t, args) for t in items if args["id"] in t] def get_playlist_contents(self, from_id): """Playlist exclusive stuff""" shareToken = [t for t in self.data_cache.playlists if t["id"] == from_id][0]["shareToken"] contents = self.client.get_shared_playlist_contents(shareToken) return [self.format_playlist_contents(t) for t in contents if "track" in t] def get_suggested(self): """Returns a list of tracks that the user might be interested in""" items = sorted(self.client.get_promoted_songs(), key=lambda y: y["title"]) return [self.format_suggested(t) for t in items if "storeId" in t] def get_information_about(self, from_type, from_id): """Gets specific information about an id (depending on the type)""" if "artist" in from_type.lower(): return self.client.get_artist_info(from_id, include_albums=False) if "album" in from_type.lower(): return self.client.get_album_info(from_id, include_tracks=False) return self.client.get_track_info(from_id) def get_stream_url(self, nid): return self.client.get_stream_url(nid) def lookup(self, nid): return self.client.get_track_info(nid) def add_track_to_library(self, nid): self.client.add_aa_track(nid) def add_to_playlist(self, playlist_id, nid): self.client.add_songs_to_playlist(playlist_id, nid) playlist_load = threading.Thread(target=self.load_playlists) playlist_load.daemon = True playlist_load.start() def format_suggested(self, t): return (t["title"], t["storeId"], "Play", t["album"]) def format_playlist_contents(self, t): return (t["track"]["title"], t["trackId"], "Play", t["track"]["album"]) def format_subitems(self, t, args): return (t[args["name"]], t[args["id"]], args["command"], t[args["alt"]])
class MusicLibrary(object): 'Read information about your Google Music library' def __init__(self, username=None, password=None, true_file_size=False, scan=True, verbose=0): self.verbose = False if verbose > 1: self.verbose = True self.__login_and_setup(username, password) self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__galbums = {} self.__gartists = {} self.__albums = [] # [Album(), ...] if scan: self.rescan() self.true_file_size = true_file_size def rescan(self): self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__albums = [] # [Album(), ...] self.__galbums = {} self.__gartists = {} self.__aggregate_albums() def __login_and_setup(self, username=None, password=None): # If credentials are not specified, get them from $HOME/.gmusicfs if not username or not password: cred_path = os.path.join(os.path.expanduser('~'), '.gmusicfs') if not os.path.isfile(cred_path): raise NoCredentialException( 'No username/password was specified. No config file could ' 'be found either. Try creating %s and specifying your ' 'username/password there. Make sure to chmod 600.' % cred_path) if not oct(os.stat(cred_path)[os.path.stat.ST_MODE]).endswith('00'): raise NoCredentialException( 'Config file is not protected. Please run: ' 'chmod 600 %s' % cred_path) self.config = ConfigParser.ConfigParser() self.config.read(cred_path) username = self.config.get('credentials','username') password = self.config.get('credentials','password') global deviceId deviceId = self.config.get('credentials','deviceId') if not username or not password: raise NoCredentialException( 'No username/password could be read from config file' ': %s' % cred_path) if not deviceId: raise NoCredentialException( 'No deviceId could be read from config file' ': %s' % cred_path) self.api = GoogleMusicAPI(debug_logging=self.verbose) log.info('Logging in...') self.api.login(username, password) log.info('Login successful.') def __set_key_from_ginfo(self, track, ginfo, key, to_key=None): 'Set track key from either album_info or artist_info' if to_key is None: to_key = key try: int_key = int(key) except ValueError: int_key = None if (not track.has_key(key) or track[key] == "" or int_key == 0) and ginfo.has_key(to_key): track[key] = ginfo[to_key] return track def __cleanup_artist(self, artist): if artist.startswith("featuring"): artist = artist[len("featuring"):].strip() if artist.startswith("feat"): artist = artist[len("feat"):].strip() return artist def __cleanup_name(self, name, track): for bracket in (('\[', '\]'), ('\{', '\}'), ('\(', '\)')): # Remove (xxx Album Version) from track names match = re.compile('^(?P<name>(.*))([ ]+[%s-]([^%s]*)[Vv]ersion[%s]?[ ]*)$' % (bracket[0], bracket[1], bracket[1])).match(name) if match is not None: name = match.groupdict()['name'] name, track = self.__cleanup_name(name, track) # Pull (feat. <artist>) out of name and add to artist list match = re.compile('^(?P<name>(.*))([ ]+[%s][ ]*[Ff]eat[\.]?[ ]*(?P<artist>(.*))[%s]+)(?P<postfix>(.*))$' % (bracket[0], bracket[1])).match(name) if match is not None: name = match.groupdict()['name'] artist = match.groupdict()['artist'] if match.groupdict().has_key('postfix') and match.groupdict()['postfix'] is not None: name += match.groupdict()['postfix'] artist = artist.strip() if artist[-1] in ")}]": # I hate regex's. The one above doesn't catch the last parenthesis if there's one artist = artist[:-1] if artist.find(" and ") > -1 or artist.find(" & ") > -1: artist = artist.replace(', ', ';') artist = artist.replace(' & ', ';') artist = artist.replace(' and ', ';') alist = artist.split(';') for artist in alist: track['artist'].append(artist.strip()) name, track = self.__cleanup_name(name, track) # Remove () or ( ) from track names match = re.compile('^(?P<name>(.*))([ ]*[%s][ ]?[%s][ ]*)$' % (bracket[0], bracket[1])).match(name) if match is not None: name = match.groupdict()['name'] name, track = self.__cleanup_name(name, track) # Strip any extra whitespace from the name name = name.strip() return name, track def __cleanup_track(self, track): name = track['title'] name, track = self.__cleanup_name(name, track) track['title'] = name for anum in range(0, len(track['artist'])): track['artist'][anum] = self.__cleanup_artist(track['artist'][anum]) return track def __aggregate_albums(self): 'Get all the tracks in the library, parse into artist and album dicts' all_artist_albums = {} log.info('Gathering track information...') tracks = self.api.get_all_songs() for track in tracks: if track.has_key('artist'): if track['artist'].find(" and ") > -1 or track['artist'].find(" & ") > -1: track['artist'] = track['artist'].replace(', ', ';') track['artist'] = track['artist'].replace(' & ', ';') track['artist'] = track['artist'].replace(' and ', ';') track['artist'] = track['artist'].split(';') else: track['artist'] = [] track = self.__cleanup_track(track) if track.has_key('albumArtist') and track['albumArtist'] != "": albumartist = track['albumArtist'] elif len(track['artist']) == 1 and track['artist'][0] != "": albumartist = track['artist'][0] else: albumartist = "Unknown" # Get album and artist information from Google if track.has_key('albumId'): if self.__galbums.has_key(track['albumId']): album_info = self.__galbums[track['albumId']] else: print "Downloading album info for '%s'" % track['album'] album_info = self.__galbums[track['albumId']] = self.api.get_album_info(track['albumId'], include_tracks=False) if album_info.has_key('artistId') and len(album_info['artistId']) > 0 and album_info['artistId'][0] != "": artist_id = album_info['artistId'][0] if self.__gartists.has_key(artist_id): artist_info = self.__gartists[artist_id] else: print "Downloading artist info for '%s'" % album_info['albumArtist'] if album_info['albumArtist'] == "Various": print album_info artist_info = self.__gartists[artist_id] = self.api.get_artist_info(artist_id, include_albums=False, max_top_tracks=0, max_rel_artist=0) else: artist_info = {} else: album_info = {} artist_info = {} track = self.__set_key_from_ginfo(track, album_info, 'album', 'name') track = self.__set_key_from_ginfo(track, album_info, 'year') track = self.__set_key_from_ginfo(track, artist_info, 'albumArtist', 'name') # Fix for odd capitalization issues if artist_info.has_key('name') and track['albumArtist'].lower() == artist_info['name'].lower() and track['albumArtist'] != artist_info['name']: track['albumArtist'] = artist_info['name'] for anum in range(0, len(track['artist'])): if artist_info.has_key('name') and track['artist'][anum].lower() == artist_info['name'].lower() and track['artist'][anum] != artist_info['name']: track['artist'][anum] = artist_info['name'] if not track.has_key('albumId'): track['albumKey'] = "%s|||%s" % (albumartist, track['album']) else: track['albumKey'] = track['albumId'] album = all_artist_albums.get(track['albumKey'], None) if not album: album = all_artist_albums[track['albumKey']] = Album( self, formatNames(track['album']), track['albumArtist'], track['album'], track['year'] ) self.__albums.append(album) artist_albums = self.__artists.get(track['albumArtist'], None) if artist_albums: artist_albums[formatNames(album.normtitle)] = album else: self.__artists[track['albumArtist']] = {album.normtitle: album} artist_albums = self.__artists[track['albumArtist']] album.add_track(track) # Separate multi-disc albums for artist in self.__artists.values(): for key in artist.keys(): album = artist[key] if album.get_disc_count() > 1: for d in album.get_discs(): new_name = "%s - Disc %i" % (album.album, d) new_album = Album(album.library, formatNames(new_name), album.artist, new_name, album.year) album.copy_art_to(new_album) new_album.show_discnum = True new_key = None for t in album.get_tracks(): if int(t['discNumber']) == d: new_album.add_track(t) artist[formatNames(new_name)] = new_album del artist[key] log.debug('%d tracks loaded.' % len(tracks)) log.debug('%d artists loaded.' % len(self.__artists)) log.debug('%d albums loaded.' % len(self.__albums)) def get_artists(self): return self.__artists def get_albums(self): return self.__albums def get_artist_albums(self, artist): log.debug(artist) return self.__artists[artist] def cleanup(self): pass
class MusicManager(object): def __init__(self): self.api = Mobileclient(validate=False, debug_logging=False) if config.GOOGLE_STREAMKEY is not None: self.api.login(config.GOOGLE_USERNAME, config.GOOGLE_PASSWORD, config.GOOGLE_STREAMKEY) else: self.api.login(config.GOOGLE_USERNAME, config.GOOGLE_PASSWORD, Mobileclient.FROM_MAC_ADDRESS) self.queue = [] self.current_index = len(self.queue) - 1 self.vlc = VlcManager() self.state_thread = Thread(target=self.check_state) self.state_thread.daemon = True self.state_thread.start() def play_song(self, id): song = self.queue_song(id) self.current_index = len(self.queue) - 1 self.load_song() return song def queue_song(self, id): self.queue.append(self.getSongInfo(id)) def play_radio_station(self, id): results = self.api.get_station_tracks(id, num_tracks=40) for song in results: song['albumArtRef'] = song['albumArtRef'][0]['url'] if 'artistId' in song: song['artistId'] = song['artistId'][0] self.current_index = len(self.queue) - 1 self.queue.append(results) self.load_song() def play_album(self, args): album = self.get_album_details(args) songs = [] for index in range(len(album['tracks'])): song = album['tracks'][index] if index == 0: songs.append(self.play_song(song['nid'])) else: songs.append(self.queue_song(song['nid'])) return songs def queue_album(self, args): album = self.get_album_details(args) songs = [] for song in album['tracks']: songs.append(self.queue_song(song['nid'])) return songs def next(self): self.current_index += 1 self.load_song() def prev(self): self.current_index -= 1 self.load_song() def pause(self): self.vlc.vlc_pause() def resume(self): self.vlc.vlc_resume() def volume(self, val): self.vlc.vlc_volume(val) def delete(self, id): if id > self.current_index: del self.queue[id] elif id < self.current_index: del self.queue[id] self.current_index -= 1 else: del self.queue[id] self.load_song() def go_to(self, id): self.current_index = id self.load_song() def load_song(self): if self.current_index < len(self.queue): song = self.queue[self.current_index] url = self.api.get_stream_url(song['nid'], config.GOOGLE_STREAMKEY) self.vlc.vlc_play(url) def check_state(self): while True: status = self.vlc.player.get_state() if status == vlc.State.Ended: if self.current_index != len(self.queue) - 1: self.next() time.sleep(1) def get_status(self): status = self.vlc.vlc_status() # status['queue'] = self.queue[:] # for i in range(len(status['queue'])): # status['queue'][i]['vlcid'] = i # if i == self.current_index: # status['queue'][i]['current'] = True # status['current'] = status['queue'][i] if len(self.queue) > 0: status['current'] = self.queue[self.current_index] return status def get_queue(self): queue = self.queue[:] for i in range(len(queue)): queue[i]['vlcid'] = i return queue def search(self, query): results = self.api.search_all_access(query, max_results=50) results['artist_hits'] = [artist['artist'] for artist in results['artist_hits']] results['album_hits'] = [album['album'] for album in results['album_hits']] for album in results['album_hits']: album['artistId'] = album['artistId'][0] results['song_hits'] = [song['track'] for song in results['song_hits']] for song in results['song_hits']: song['albumArtRef'] = song['albumArtRef'][0]['url'] if 'artistId' in song: song['artistId'] = song['artistId'][0] return results def get_album_details(self, id): results = self.api.get_album_info(album_id=id, include_tracks=True) results['artistId'] = results['artistId'][0] for song in results['tracks']: song['albumArtRef'] = song['albumArtRef'][0]['url'] if 'artistId' in song: song['artistId'] = song['artistId'][0] return results def get_artist_details(self, id): results = self.api.get_artist_info(artist_id=id) for album in results['albums']: album['artistId'] = album['artistId'][0] for song in results['topTracks']: song['albumArtRef'] = song['albumArtRef'][0]['url'] if 'artistId' in song: song['artistId'] = song['artistId'][0] return results def create_radio_station(self, name, id): if id[0] == 'A': station_id = self.api.create_station(name, artist_id=id) elif id[0] == 'B': station_id = self.api.create_station(name, album_id=id) else: station_id = self.api.create_station(name, track_id=id) return station_id def get_radio_stations(self): return self.api.get_all_stations() def flush(self): self.vlc.vlc_stop() self.queue = [] def getSongInfo(self, id): song = self.api.get_track_info(id) song['albumArtRef'] = song['albumArtRef'][0]['url'] if 'artistId' in song: song['artistId'] = song['artistId'][0] return song
class Gmusic(object): """Class to handle Google Music-related functionality""" def __init__(self, bot): """ init """ self.bot = bot self.mob = Mobileclient() def login(self, username, password, android_id=Mobileclient.FROM_MAC_ADDRESS): """ login method """ self.mob.login(username, password, android_id) return self.mob.is_authenticated() def search(self, searchterms): """ search for stuff """ hits = self.mob.search("{0}".format(searchterms)) return hits def create_playlist(self, name, song_ids, public=True): """ create new playlist named 'name', containing songs with 'song_id' """ playlist_id = self.mob.create_playlist(name, description="Bot Playlist", public=public) self.mob.add_songs_to_playlist(playlist_id, song_ids) return playlist_id def _make_playlist_share_link(self, share_token): base_share_url = "https://play.google.com/music/playlist" return "{}/{}".format(base_share_url, share_token) def share_playlist(self, playlist_id): try: [share_token] = [ plist['shareToken'] for plist in self.mob.get_all_playlists() if plist['id'] == playlist_id ] return self._make_playlist_share_link(share_token) except ValueError: return "Cannot find playlist" def get_best_song_match(self, artist, title): hits = self.search("{0} {1}".format(artist, title)) tracks = self.filter_to_song_minimum_info(self.get_songs(hits)) similarities = [(similarity(track['artist'], artist, track['title'], title), track) for track in tracks] sorted_tracks = sorted(similarities, key=lambda k: k[0]) best_track = None if len(sorted_tracks) > 0: best_track = sorted_tracks[0][1] return best_track def get_best_album_match(self, artist, album): hits = self.search("{0} {1}".format(artist, album)) albums = self.get_albums(hits) similarities = [(similarity(a['artist'], artist, a['album'], album), a) for a in albums] sorted_albums = sorted(similarities, key=lambda k: k[0]) if len(sorted_albums) == 0: return [] best_album = sorted_albums[0][1] album_info = self.mob.get_album_info(best_album['albumId']) store_ids = [t['storeId'] for t in album_info['tracks']] print("Store ids in best_album_match: {0}".format(store_ids)) return store_ids def format_best_match(self, artist, title): track = self.get_best_song_match(artist, title) share_base_url = 'https://play.google.com/music/m/' return "{0} {1} {2} - {3}{4}".format(track['artist'], track['album'], track['title'], share_base_url, track['storeId']) def get_albums(self, results): albums = [album.get('album', None) for album in results['album_hits']] album_details = [{ 'artist': a['artist'], 'album': a['name'], 'albumId': a['albumId'] } for a in albums] return album_details def get_songs(self, results): return [song.get('track', None) for song in results['song_hits']] def filter_to_song_minimum_info(self, results): return [{ 'artist': song.get('artist', None), 'album': song.get('album', None), 'title': song.get('title', None), 'storeId': song.get('storeId', None) } for song in results] def convert_spotify_embed_to_gmusic(self, url): s_list = SpotifyPlaylist(url) title = s_list.title best_matches = [ self.get_best_song_match(i.artist, i.track) for i in s_list.items ] filtered_matches = [i for i in best_matches if i is not None] store_ids = [i.get('storeId') for i in filtered_matches] new_plist = self.create_playlist(title, store_ids) return self.share_playlist(new_plist) def convert_hbih_to_gmusic(self, url): hbih_list = HBIHPlaylist(url) title = hbih_list.title store_ids = [] for item in hbih_list.items: album_store_ids = self.get_best_album_match(item[0], item[1]) print("Adding store ids: {0}".format(album_store_ids)) store_ids.extend(album_store_ids) store_id_set = IndexedSet(store_ids) no_dupes_store_ids = list(store_id_set) new_plist = self.create_playlist(title, no_dupes_store_ids[0:1000]) return self.share_playlist(new_plist) def create_playlist_from_song_names(self, artist, songs): year = datetime.datetime.now().year title = "{} setlist ({})".format(artist, year) best_matches = [self.get_best_song_match(artist, s) for s in songs] filtered_matches = [i for i in best_matches if i is not None] store_ids = [i.get('storeId') for i in filtered_matches] new_plist = self.create_playlist(title, store_ids) return self.share_playlist(new_plist) def get_newest_playlists(self, count=5): """ return 'count' newest playlists """ all_plists = self.mob.get_all_playlists() sorted_plists = sorted(all_plists, key=lambda k: k['lastModifiedTimestamp'], reverse=True) if count > 0: newest_plists = sorted_plists[:count] else: newest_plists = sorted_plists info = [{ 'name': p['name'], 'share': self._make_playlist_share_link(p['shareToken']) } for p in newest_plists] return info def get_all_playlists(self): """ return all playlists """ return self.get_newest_playlists(0) # 0 = return everything def find_playlists(self, searchterm): """ find all playlists that have a name containing 'searchterm' """ all_plists = self.get_all_playlists() all_matches = all_plists all_terms = searchterm.split(' ') for term in all_terms: all_matches = [ p for p in all_matches if p['name'].lower().find(term.lower()) != -1 ] return all_matches
class GMusicWrapper: def __init__(self, username, password, logger=None): self._api = Mobileclient() self.logger = logger success = self._api.login( username, password, environ.get('ANDROID_ID', Mobileclient.FROM_MAC_ADDRESS)) if not success: raise Exception("Unsuccessful login. Aborting!") # Populate our library self.library = {} self.indexing_thread = threading.Thread(target=self.index_library) self.indexing_thread.start() def _search(self, query_type, query): try: results = self._api.search(query) except CallFailure: return [] hits_key = "%s_hits" % query_type if hits_key not in results: return [] # Ugh, Google had to make this schema nonstandard... if query_type == 'song': query_type = 'track' return map(lambda x: x[query_type], results[hits_key]) def is_indexing(self): return self.indexing_thread.is_alive() def index_library(self): """ Downloads the a list of every track in a user's library and populates self.library with storeIds -> track definitions """ self.logger.debug('Fetching library...') tracks = self.get_all_songs() for track in tracks: song_id = track['id'] self.library[song_id] = track self.logger.debug('Done! Discovered %d tracks.' % len(self.library)) def get_artist(self, name): """ Fetches information about an artist given its name """ search = self._search("artist", name) if len(search) == 0: return False return self._api.get_artist_info(search[0]['artistId'], max_top_tracks=100) def get_album(self, name, artist_name=None): if artist_name: name = "%s %s" % (name, artist_name) search = self._search("album", name) if len(search) == 0: return False return self._api.get_album_info(search[0]['albumId']) def get_song(self, name, artist_name=None): if artist_name: name = "%s %s" % (artist_name, name) search = self._search("song", name) if len(search) == 0: return False return search[0] def get_station(self, title, artist_id=None): if artist_id != None: return self._api.create_station(title, artist_id=artist_id) def get_station_tracks(self, station_id): return self._api.get_station_tracks(station_id) def get_google_stream_url(self, song_id): return self._api.get_stream_url(song_id) def get_stream_url(self, song_id): return "%s/stream/%s" % (environ['APP_URL'], song_id) def get_all_user_playlist_contents(self): return self._api.get_all_user_playlist_contents() def get_all_songs(self): return self._api.get_all_songs() def rate_song(self, song, rating): return self._api.rate_songs(song, rating) def extract_track_info(self, track): # When coming from a playlist, track info is nested under the "track" # key if 'track' in track: track = track['track'] if 'storeId' in track: return (track, track['storeId']) elif 'trackId' in track: return (self.library[track['trackId']], track['trackId']) else: return (None, None) def increment_song_playcount(self, song_id, plays=1, playtime=None): return self._api.increment_song_playcount(song_id, plays, playtime) def get_song_data(self, song_id): return self._api.get_track_info(song_id) @classmethod def generate_api(cls, **kwargs): return cls(environ['GOOGLE_EMAIL'], environ['GOOGLE_PASSWORD'], **kwargs)
class GMusicClient(ContentConsumer): '''Element in charge of interfacing with GMusicApi Client''' def __init__(self, data_cache): self.client = Mobileclient() self.data_cache = data_cache def login(self): '''Use data/unlocked/credentials.json to log in''' mac = Mobileclient.FROM_MAC_ADDRESS credentials = json.load(open('data/unlocked/credentials.json', 'r')) self.client.login(credentials['username'], credentials['password'], mac) def load_my_library(self): '''Load user's songs, playlists, and stations''' track_load = threading.Thread(target=self.load_tracks) radio_load = threading.Thread(target=self.load_radios) playlist_load = threading.Thread(target=self.load_playlists) track_load.start() radio_load.start() playlist_load.start() track_load.join() radio_load.join() playlist_load.join() def load_playlists(self): playlists = self.client.get_all_user_playlist_contents() playlists.reverse() self.data_cache.playlists = playlists def load_tracks(self): self.data_cache.tracks = [t for t in self.client.get_all_songs() if 'nid' in t] def scrape_song(self, track): return track def load_radios(self): radios = self.client.get_all_stations() radios.reverse() self.data_cache.radios = radios def get_radio_contents(self, radio_id): tracks = self.client.get_station_tracks(radio_id) return tracks def get_radio_list(self, name): return [r for r in self.data_cache.radios if name in r['name']] def filter(self, element, field, filter_by): return [e for e in element if filter_by in e[field]] def get_playlist_list(self, name): return self.filter(self.data_cache.playlists, 'name', name) def search_all_access(self, query): return self.client.search_all_access(query) def create_radio(self, seed_type, id, name): '''Create a radio''' # This is a weird way to do this, but there's no other choice ids = {"track": None, "album": None, "artist": None} seed_id_name = self.get_type_name(seed_type) ids[seed_id_name] = id return self.client.create_station(name=name, track_id=ids['track'], album_id=ids['album'], artist_id=ids['artist']) def search_items_all_access(self, type, query): '''Searches Albums, Artists, and Songs; uses metaprogramming''' index_arguments = self.get_index_arguments(type) items = self.search_all_access(query)['{0}_hits'.format(type[:-1])] return [self.format_item(item, type, index_arguments) for item in items] def get_sub_items(self, type_from, search_type, from_id): '''Here type_from refers to artist or album we're indexing against''' args = self.get_index_arguments(search_type) if type_from == 'playlist': return self.get_playlist_contents(from_id) else: # Get the appropriate search method and execute it search_method_name = 'get_{0}_info'.format(type_from) search_method = getattr(self.client, search_method_name) try: items = search_method(from_id, True)[args['type']+'s'] # True here includes subelements except: # User passed in a bad id or something return # Now return appropriately return [self.format_subitems(t, args) for t in items if args['id'] in t] def get_playlist_contents(self, from_id): '''Playlist exclusive stuff''' shareToken = [t for t in self.data_cache.playlists \ if t['id'] == from_id][0]['shareToken'] contents = self.client.get_shared_playlist_contents(shareToken) return [self.format_playlist_contents(t) for t in contents if 'track' in t] def get_suggested(self): '''Returns a list of tracks that the user might be interested in''' items = sorted(self.client.get_promoted_songs(), key=lambda y: y['title']) return [self.format_suggested(t) for t in items if 'storeId' in t] def get_information_about(self, from_type, from_id): '''Gets specific information about an id (depending on the type)''' if 'artist' in from_type.lower(): return self.client.get_artist_info(from_id, include_albums=False) if 'album' in from_type.lower(): return self.client.get_album_info(from_id, include_tracks=False) return self.client.get_track_info(from_id) def get_stream_url(self, nid): return self.client.get_stream_url(nid) def lookup(self, nid): return self.client.get_track_info(nid) def add_track_to_library(self, nid): self.client.add_aa_track(nid) def add_to_playlist(self, playlist_id, nid): self.client.add_songs_to_playlist(playlist_id, nid) playlist_load = threading.Thread(target=self.load_playlists) playlist_load.daemon = True playlist_load.start() def format_suggested(self, t): return (t['title'], t['storeId'], 'Play', t['album']) def format_playlist_contents(self, t): return (t['track']['title'], t['trackId'], 'Play', t['track']['album']) def format_subitems(self, t, args): return (t[args['name']], t[args['id']], args['command'], t[args['alt']])
class IBCMusicClient(): # Please have this be an absolute path SONG_DIR = "/home/pi/Desktop/JukeSite/songs" def __init__(self): self.api = None self.player = None def start(self): """ Starts the MobileClient """ self.api = Mobileclient() def stop(self): """ Deletes MobileClient and sets self.api to default(None) """ del self.api self.api = None def logon(self, email, password): """ Logs onto google music as a mobile client. Returns true is sucessful. :param email: Email of the Google user :param password: Pass of the google user :return: Bool if connection was successful """ if self.api is None: raise errors.MobileClientNotInitError( "The Client has not been init therefor it cannot logon.", 1000) try: res = self.api.login(email, password, self.api.FROM_MAC_ADDRESS) except AlreadyLoggedIn as e: self.api.logout() res = self.api.login(email, password, self.api.FROM_MAC_ADDRESS) del email del password return res def logout(self): """ logs out of google music mobile client. :return: if it was succesful """ return self.api.logout() def is_authenticated(self): if not self.api.is_authenticated(): raise errors.SessionNotActive( "The session is no longer active. Either it timedout or you have not logged in", 1001) def search_song(self, query): """ Searchs for the given query and return the song results Will check for authentication. [{ 'track': { 'album': 'Work Out', 'albumArtRef': [{ 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh5.ggpht.com/DVIg4GiD6msHfgPs_Vu_2eRxCyAoz0fFdxj5w...' }], 'albumArtist': 'J.Cole', 'albumAvailableForPurchase': True, 'albumId': 'Bfp2tuhynyqppnp6zennhmf6w3y', 'artist': 'J Cole', 'artistId': ['Ajgnxme45wcqqv44vykrleifpji', 'Ampniqsqcwxk7btbgh5ycujij5i'], 'composer': '', 'discNumber': 1, 'durationMillis': '234000', 'estimatedSize': '9368582', 'explicitType': '1', 'genre': 'Pop', 'kind': 'sj#track', 'nid': 'Tq3nsmzeumhilpegkimjcnbr6aq', 'primaryVideo': { 'id': '6PN78PS_QsM', 'kind': 'sj#video', 'thumbnails': [{ 'height': 180, 'url': 'https://i.ytimg.com/vi/6PN78PS_QsM/mqdefault.jpg', 'width': 320 }] }, 'storeId': 'Tq3nsmzeumhilpegkimjcnbr6aq', 'title': 'Work Out', 'trackAvailableForPurchase': True, 'trackAvailableForSubscription': True, 'trackNumber': 1, 'trackType': '7', 'year': 2011 }, 'type': '1' }] :param query: The song query :return: [list] all the song hits """ self.is_authenticated() res = self.api.search(query) songs = res['song_hits'] return songs def search_album(self, query): """ Searchs for the given query and returns the album results. Will check for authenitcation. e.g return: [{ 'album': { 'albumArtRef': 'http://lh5.ggpht.com/DVIg4GiD6msHfgPs_Vu_2eRxCyAoz0fF...', 'albumArtist': 'J.Cole', 'albumId': 'Bfp2tuhynyqppnp6zennhmf6w3y', 'artist': 'J.Cole', 'artistId': ['Ajgnxme45wcqqv44vykrleifpji'], 'description_attribution': { 'kind': 'sj#attribution', 'license_title': 'Creative Commons Attribution CC-BY', 'license_url': 'http://creativecommons.org/licenses/by/4.0/legalcode', 'source_title': 'Freebase', 'source_url': '' }, 'explicitType': '1', 'kind': 'sj#album', 'name': 'Work Out', 'year': 2011 }, 'type': '3' }] :param query: [string] The album query :return: [list] A list of all the album hits """ self.is_authenticated() res = self.api.search(query) albums = res['album_hits'] return albums def get_album_info(self, album_id): """ Returns information about an album e.g return: { 'kind': 'sj#album', 'name': 'Circle', 'artist': 'Amorphis', 'albumArtRef': 'http://lh6.ggpht.com/...', 'tracks': [ # if `include_tracks` is True { 'album': 'Circle', 'kind': 'sj#track', 'storeId': 'T5zb7luo2vkroozmj57g2nljdsy', # can be used as a song id 'artist': 'Amorphis', 'albumArtRef': [ { 'url': 'http://lh6.ggpht.com/...' }], 'title': 'Shades of Grey', 'nid': 'T5zb7luo2vkroozmj57g2nljdsy', 'estimatedSize': '13115591', 'albumId': 'Bfr2onjv7g7tm4rzosewnnwxxyy', 'artistId': ['Apoecs6off3y6k4h5nvqqos4b5e'], 'albumArtist': 'Amorphis', 'durationMillis': '327000', 'composer': '', 'genre': 'Metal', 'trackNumber': 1, 'discNumber': 1, 'trackAvailableForPurchase': True, 'trackType': '7', 'albumAvailableForPurchase': True }, # ... ], 'albumId': 'Bfr2onjv7g7tm4rzosewnnwxxyy', 'artistId': ['Apoecs6off3y6k4h5nvqqos4b5e'], 'albumArtist': 'Amorphis', 'year': 2013 } :param album_id: The albumId :return: Dictionary in the format above """ self.is_authenticated() return self.api.get_album_info(album_id) def get_song_info(self, song_id): """ Returns information about a song e.g return { 'album': 'Best Of', 'kind': 'sj#track', 'storeId': 'Te2qokfjmhqxw4bnkswbfphzs4m', 'artist': 'Amorphis', 'albumArtRef': [ { 'url': 'http://lh5.ggpht.com/...' }], 'title': 'Hopeless Days', 'nid': 'Te2qokfjmhqxw4bnkswbfphzs4m', 'estimatedSize': '12325643', 'albumId': 'Bsbjjc24a5xutbutvbvg3h4y2k4', 'artistId': ['Apoecs6off3y6k4h5nvqqos4b5e'], 'albumArtist': 'Amorphis', 'durationMillis': '308000', 'composer': '', 'genre': 'Metal', 'trackNumber': 2, 'discNumber': 1, 'trackAvailableForPurchase': True, 'trackType': '7', 'albumAvailableForPurchase': True } :param song_id: The songds storeId :return: A dict with the above information """ self.is_authenticated() return self.api.get_track_info(song_id) def get_song_url(self, song_id): self.is_authenticated() res = self.api.get_stream_url(song_id) return res def download_song(self, song_id): """ Download the song from the storeId. :param song_id: the 'storeId' of the specific song """ url = self.get_song_url(song_id) song_file_path = "{}/{}.mp3".format(IBCMusicClient.SONG_DIR, song_id) if os.path.isfile(song_file_path): raise errors.SongAlreadyDownloadedException( "The song '{}' has already been downloaded and cached".format( song_file_path), 8002) # This need to not use subprocessing command = ['wget', url, '-O', song_file_path] res = check_output(command) lines = res.decode().split('\n') error_lines = [line for line in lines if 'failed' in line] if len(error_lines) > 0: # We have an error raise errors.CannotDownloadSongError( "Could not download the given song. {}".format( str(error_lines)), 1003)
class MusicLibrary(object): """This class reads information about your Google Play Music library""" def __init__(self, username=None, password=None, true_file_size=False, scan=True, verbose=0): self.verbose = False if verbose > 1: self.verbose = True self.__login_and_setup(username, password) self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__gartists = {} self.__albums = [] # [Album(), ...] self.__galbums = {} self.__tracks = {} self.__playlists = {} if scan: self.rescan() self.true_file_size = true_file_size def rescan(self): """Scan the Google Play Music library""" self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__gartists = {} self.__albums = [] # [Album(), ...] self.__galbums = {} self.__tracks = {} self.__playlists = {} self.__aggregate_albums() def __login_and_setup(self, username=None, password=None): # If credentials are not specified, get them from $HOME/.gmusicfs if not username or not password: cred_path = os.path.join(os.path.expanduser('~'), '.gmusicfs') if not os.path.isfile(cred_path): raise NoCredentialException( 'No username/password was specified. No config file could ' 'be found either. Try creating %s and specifying your ' 'username/password there. Make sure to chmod 600.' % cred_path) if not oct( os.stat(cred_path)[os.path.stat.ST_MODE]).endswith('00'): raise NoCredentialException( 'Config file is not protected. Please run: ' 'chmod 600 %s' % cred_path) self.config = ConfigParser.ConfigParser() self.config.read(cred_path) username = self.config.get('credentials', 'username') password = self.config.get('credentials', 'password') global deviceId deviceId = self.config.get('credentials', 'deviceId') if not username or not password: raise NoCredentialException( 'No username/password could be read from config file' ': %s' % cred_path) if not deviceId: raise NoCredentialException( 'No deviceId could be read from config file' ': %s' % cred_path) if deviceId.startswith("0x"): deviceId = deviceId[2:] self.api = GoogleMusicAPI(debug_logging=self.verbose) log.info('Logging in...') self.api.login(username, password, deviceId) log.info('Login successful.') def __set_key_from_ginfo(self, track, ginfo, key, to_key=None): """Set track key from either album_info or artist_info""" if to_key is None: to_key = key try: int_key = int(key) except ValueError: int_key = None if (not track.has_key(key) or track[key] == "" or int_key == 0) and ginfo.has_key(to_key): track[key] = ginfo[to_key] return track def __aggregate_albums(self): """Get all the tracks and playlists in the library, parse into relevant dicts""" log.info('Gathering track information...') tracks = self.api.get_all_songs() for track in tracks: log.debug('track = %s' % pp.pformat(track)) # Get album and artist information from Google if track.has_key('albumId'): if self.__galbums.has_key(track['albumId']): album_info = self.__galbums[track['albumId']] else: log.info("Downloading album info for %s '%s'", track['albumId'], track['album']) try: album_info = self.__galbums[ track['albumId']] = self.api.get_album_info( track['albumId'], include_tracks=False) except gmusicapi.exceptions.CallFailure: log.exception( "Failed to download album info for %s '%s'", track['albumId'], track['album']) album_info = {} if album_info.has_key('artistId') and len( album_info['artistId'] ) > 0 and album_info['artistId'][0] != "": artist_id = album_info['artistId'][0] if self.__gartists.has_key(artist_id): artist_info = self.__gartists[artist_id] else: log.info("Downloading artist info for %s '%s'", artist_id, album_info['albumArtist']) #if album_info['albumArtist'] == "Various": # print album_info artist_info = self.__gartists[ artist_id] = self.api.get_artist_info( artist_id, include_albums=False, max_top_tracks=0, max_rel_artist=0) else: artist_info = {} else: album_info = {} artist_info = {} track = self.__set_key_from_ginfo(track, album_info, 'album', 'name') track = self.__set_key_from_ginfo(track, album_info, 'year') track = self.__set_key_from_ginfo(track, artist_info, 'albumArtist', 'name') # Prefer the album artist over the track artist if there is one artist_name = formatNames(track['albumArtist']) if artist_name.strip() == '': artist_name = formatNames(track['artist']) if artist_name.strip() == '': artist_name = 'Unknown' # Get the Artist object, or create one if it doesn't exist artist = self.__artists.get(artist_name.lower(), None) if not artist: artist = Artist(self, artist_name) self.__artists[artist_name.lower()] = artist # Get the Album object, or create one if it doesn't exist album = artist.get_album(formatNames(track['album'])) if not album: album = Album(self, track['album']) self.__albums.append( album) # NOTE: Current no purpose other than to count artist.add_album(album) # Add track to album album.add_track(track) # Add track to list of all tracks, indexable by track ID if 'id' in track: self.__tracks[track['id']] = track log.info('%d tracks loaded.' % len(tracks)) log.info('%d artists loaded.' % len(self.__artists)) log.info('%d albums loaded.' % len(self.__albums)) # Add all playlists playlists = self.api.get_all_user_playlist_contents() for pldata in playlists: playlist = Playlist(self, pldata) self.__playlists[playlist.dirname.lower()] = playlist log.debug('%d playlists loaded.' % len(self.__playlists)) def get_artists(self): """Return all artists in the library""" return self.__artists def get_artist(self, name): """Return the artist from the library with the specified name""" return self.__artists.get(name.lower(), None) def get_playlists(self): """Return list of all playlists in the library""" return self.__playlists.values() def get_playlist(self, name): """Return the playlist from the library with the specified name""" return self.__playlists.get(name.lower(), None) def get_track(self, trackid): """Return the track from the library with the specified track ID""" return self.__tracks.get(trackid, None) def cleanup(self): pass
class Session(object): def __init__(self): self.api = None self.user = None self.lib_albums = {} self.lib_artists = {} self.lib_tracks = {} self.lib_playlists = {} self.lib_updatetime = 0 self.sitdata = [] self.sitbyid = {} self.sitdataupdtime = 0 def dmpdata(self, who, data): uplog("%s: %s" % (who, json.dumps(data, indent=4))) # Look for an Android device id in the registered devices. def find_device_id(self, data): for entry in data: if "type" in entry and entry["type"] == u"ANDROID": # Get rid of 0x id = entry["id"][2:] uplog("Using deviceid %s" % id) return id return None def login(self, username, password, deviceid=None): self.api = Mobileclient(debug_logging=False) if deviceid is None: logged_in = self.api.login(username, password, Mobileclient.FROM_MAC_ADDRESS) if logged_in: # Try to re-login with a valid deviceid data = self.api.get_registered_devices() #self.dmpdata("registered devices", data) deviceid = self.find_device_id(data) if deviceid: logged_in = self.login(username, password, deviceid) else: logged_in = self.api.login(username, password, deviceid) isauth = self.api.is_authenticated() #uplog("login: Logged in: %s. Auth ok: %s" % (logged_in, isauth)) return logged_in def _get_user_library(self): now = time.time() if now - self.lib_updatetime < 300: return if self.lib_updatetime == 0: data = self.api.get_all_songs() #self.dmpdata("all_songs", data) else: data = self.api.get_all_songs(updated_after=datetime.datetime.fromtimestamp(self.lib_updatetime)) #self.dmpdata("all_songs_since_update", data) self.lib_updatetime = now tracks = [_parse_track(t) for t in data] self.lib_tracks.update(dict([(t.id, t) for t in tracks])) for track in tracks: # We would like to use the album id here, but gmusic # associates the tracks with any compilations after # uploading (does not use the metadata apparently), so # that we can't (we would end up with multiple # albums). OTOH, the album name is correct (so seems to # come from the metadata). What we should do is test the # album ids for one album with a matching title, but we're # not sure to succeed. So at this point, the album id we # end up storing could be for a different albums, and we # should have a special library-local get_album_tracks self.lib_albums[track.album.name] = track.album self.lib_artists[track.artist.id] = track.artist def get_user_albums(self): self._get_user_library() return self.lib_albums.values() def get_user_artists(self): self._get_user_library() return self.lib_artists.values() def get_user_playlists(self): pldata = self.api.get_all_playlists() #self.dmpdata("playlists", pldata) return [_parse_playlist(pl) for pl in pldata] def get_user_playlist_tracks(self, playlist_id): self._get_user_library() # Unfortunately gmusic does not offer incremental updates for # playlists. This means we must download all playlist data any # time we want an update. Playlists include track information # for gmusic songs, so if a user has a lot of playlists this # can take some time. # For now, we only load the playlists one time for performance purposes if len(self.lib_playlists) == 0: data = self.api.get_all_user_playlist_contents() self.lib_playlists = dict([(pl['id'], pl) for pl in data]) tracks = [] if playlist_id in self.lib_playlists: for entry in self.lib_playlists[playlist_id]['tracks']: if entry['deleted']: continue if entry['source'] == u'1': tracks.append(self.lib_tracks[entry['trackId']]) elif 'track' in entry: tracks.append(_parse_track(entry['track']) ) return tracks def create_station_for_genre(self, genre_id): id = self.api.create_station("station"+genre_id, genre_id=genre_id) return id def get_user_stations(self): data = self.api.get_all_stations() # parse_playlist works fine for stations stations = [_parse_playlist(d) for d in data] return stations def delete_user_station(self, id): self.api.delete_stations(id) # not working right now def listen_now(self): print("api.get_listen_now_items()", file=sys.stderr) ret = {'albums' : [], 'stations' : []} try: data = self.api.get_listen_now_items() except Exception as err: print("api.get_listen_now_items failed: %s" % err, file=sys.stderr) data = None # listen_now entries are not like normal albums or stations, # and need special parsing. I could not make obvious sense of # the station-like listen_now entries, so left them aside for # now. Maybe should use create_station on the artist id? if data: ret['albums'] = [_parse_ln_album(a['album']) \ for a in data if 'album' in a] #ret['stations'] = [_parse_ln_station(d['radio_station']) \ # for d in data if 'radio_station' in d] else: print("listen_now: no items returned !", file=sys.stderr) print("get_listen_now_items: returning %d albums and %d stations" %\ (len(ret['albums']), len(ret['stations'])), file=sys.stderr) return ret def get_situation_content(self, id = None): ret = {'situations' : [], 'stations' : []} now = time.time() if id is None and now - self.sitdataupdtime > 300: self.sitbyid = {} self.sitdata = self.api.get_listen_now_situations() self.sitdataupdtime = now # Root is special, it's a list of situations if id is None: ret['situations'] = [self._parse_situation(s) \ for s in self.sitdata] return ret # not root if id not in self.sitbyid: print("get_situation_content: %s unknown" % id, file=sys.stderr) return ret situation = self.sitbyid[id] #self.dmpdata("situation", situation) if 'situations' in situation: ret['situations'] = [self._parse_situation(s) \ for s in situation['situations']] if 'stations' in situation: ret['stations'] = [_parse_situation_station(s) \ for s in situation['stations']] return ret def _parse_situation(self, data): self.sitbyid[data['id']] = data return Playlist(id=data['id'], name=data['title']) def create_curated_and_get_tracks(self, id): sid = self.api.create_station("station"+id, curated_station_id=id) print("create_curated: sid %s"%sid, file=sys.stderr) tracks = [_parse_track(t) for t in self.api.get_station_tracks(sid)] #print("curated tracks: %s"%tracks, file=sys.stderr) self.api.delete_stations(sid) return tracks def get_station_tracks(self, id): return [_parse_track(t) for t in self.api.get_station_tracks(id)] def get_media_url(self, song_id, quality=u'med'): url = self.api.get_stream_url(song_id, quality=quality) print("get_media_url got: %s" % url, file=sys.stderr) return url def get_album_tracks(self, album_id): data = self.api.get_album_info(album_id, include_tracks=True) album = _parse_album(data) return [_parse_track(t, album) for t in data['tracks']] def get_promoted_tracks(self): data = self.api.get_promoted_songs() #self.dmpdata("promoted_tracks", data) return [_parse_track(t) for t in data] def get_genres(self, parent=None): data = self.api.get_genres(parent_genre_id=parent) return [_parse_genre(g) for g in data] def get_artist_info(self, artist_id, doRelated=False): ret = {"albums" : [], "toptracks" : [], "related" : []} # Happens,some library tracks have no artistId entry if artist_id is None or artist_id == 'None': uplog("get_artist_albums: artist_id is None") return ret else: uplog("get_artist_albums: artist_id %s" % artist_id) maxrel = 20 if doRelated else 0 maxtop = 0 if doRelated else 10 incalbs = False if doRelated else True data = self.api.get_artist_info(artist_id, include_albums=incalbs, max_top_tracks=maxtop, max_rel_artist=maxrel) #self.dmpdata("artist_info", data) if 'albums' in data: ret["albums"] = [_parse_album(alb) for alb in data['albums']] if 'topTracks' in data: ret["toptracks"] = [_parse_track(t) for t in data['topTracks']] if 'related_artists' in data: ret["related"] = [_parse_artist(a) for a in data['related_artists']] return ret def get_artist_related(self, artist_id): data = self.get_artist_info(artist_id, doRelated=True) return data["related"] def search(self, query): data = self.api.search(query, max_results=50) #self.dmpdata("Search", data) tr = [_parse_track(i['track']) for i in data['song_hits']] ar = [_parse_artist(i['artist']) for i in data['artist_hits']] al = [_parse_album(i['album']) for i in data['album_hits']] #self.dmpdata("Search playlists", data['playlist_hits']) try: pl = [_parse_splaylist(i) for i in data['playlist_hits']] except: pl = [] return SearchResult(artists=ar, albums=al, playlists=pl, tracks=tr)
class Session(object): def __init__(self): self.api = None self.user = None self.lib_albums = {} self.lib_artists = {} self.lib_tracks = {} self.lib_updatetime = 0 self.sitdata = [] self.sitbyid = {} self.sitdataupdtime = 0 def dmpdata(self, who, data): print("%s: %s" % (who, json.dumps(data, indent=4)), file=sys.stderr) def login(self, username, password, deviceid=None): self.api = Mobileclient(debug_logging=False) if deviceid is None: logged_in = self.api.login(username, password, Mobileclient.FROM_MAC_ADDRESS) else: logged_in = self.api.login(username, password, deviceid) # print("Logged in: %s" % logged_in) # data = self.api.get_registered_devices() # print("registered: %s" % data) # isauth = self.api.is_authenticated() # print("Auth ok: %s" % isauth) return logged_in def _get_user_library(self): now = time.time() if now - self.lib_updatetime < 300: return data = self.api.get_all_songs() # self.dmpdata("all_songs", data) self.lib_updatetime = now tracks = [_parse_track(t) for t in data] self.lib_tracks = dict([(t.id, t) for t in tracks]) for track in tracks: # We would like to use the album id here, but gmusic # associates the tracks with any compilations after # uploading (does not use the metadata apparently), so # that we can't (we would end up with multiple # albums). OTOH, the album name is correct (so seems to # come from the metadata). What we should do is test the # album ids for one album with a matching title, but we're # not sure to succeed. So at this point, the album id we # end up storing could be for a different albums, and we # should have a special library-local get_album_tracks self.lib_albums[track.album.name] = track.album self.lib_artists[track.artist.id] = track.artist def get_user_albums(self): self._get_user_library() return self.lib_albums.values() def get_user_artists(self): self._get_user_library() return self.lib_artists.values() def get_user_playlists(self): pldata = self.api.get_all_playlists() # self.dmpdata("playlists", pldata) return [_parse_playlist(pl) for pl in pldata] def get_user_playlist_tracks(self, playlist_id): self._get_user_library() data = self.api.get_all_user_playlist_contents() # self.dmpdata("user_playlist_content", data) trkl = [item["tracks"] for item in data if item["id"] == playlist_id] if not trkl: return [] try: return [self.lib_tracks[track["trackId"]] for track in trkl[0]] except: return [] def create_station_for_genre(self, genre_id): id = self.api.create_station("station" + genre_id, genre_id=genre_id) return id def get_user_stations(self): data = self.api.get_all_stations() # parse_playlist works fine for stations stations = [_parse_playlist(d) for d in data] return stations def delete_user_station(self, id): self.api.delete_stations(id) # not working right now def listen_now(self): print("api.get_listen_now_items()", file=sys.stderr) ret = {"albums": [], "stations": []} try: data = self.api.get_listen_now_items() except Exception as err: print("api.get_listen_now_items failed: %s" % err, file=sys.stderr) data = None # listen_now entries are not like normal albums or stations, # and need special parsing. I could not make obvious sense of # the station-like listen_now entries, so left them aside for # now. Maybe should use create_station on the artist id? if data: ret["albums"] = [_parse_ln_album(a["album"]) for a in data if "album" in a] # ret['stations'] = [_parse_ln_station(d['radio_station']) \ # for d in data if 'radio_station' in d] else: print("listen_now: no items returned !", file=sys.stderr) print( "get_listen_now_items: returning %d albums and %d stations" % (len(ret["albums"]), len(ret["stations"])), file=sys.stderr, ) return ret def get_situation_content(self, id=None): ret = {"situations": [], "stations": []} now = time.time() if id is None and now - self.sitdataupdtime > 300: self.sitbyid = {} self.sitdata = self.api.get_listen_now_situations() self.sitdataupdtime = now # Root is special, it's a list of situations if id is None: ret["situations"] = [self._parse_situation(s) for s in self.sitdata] return ret # not root if id not in self.sitbyid: print("get_situation_content: %s unknown" % id, file=sys.stderr) return ret situation = self.sitbyid[id] # self.dmpdata("situation", situation) if "situations" in situation: ret["situations"] = [self._parse_situation(s) for s in situation["situations"]] if "stations" in situation: ret["stations"] = [_parse_situation_station(s) for s in situation["stations"]] return ret def _parse_situation(self, data): self.sitbyid[data["id"]] = data return Playlist(id=data["id"], name=data["title"]) def create_curated_and_get_tracks(self, id): sid = self.api.create_station("station" + id, curated_station_id=id) print("create_curated: sid %s" % sid, file=sys.stderr) tracks = [_parse_track(t) for t in self.api.get_station_tracks(sid)] # print("curated tracks: %s"%tracks, file=sys.stderr) self.api.delete_stations(sid) return tracks def get_station_tracks(self, id): return [_parse_track(t) for t in self.api.get_station_tracks(id)] def get_media_url(self, song_id, quality=u"med"): url = self.api.get_stream_url(song_id, quality=quality) print("get_media_url got: %s" % url, file=sys.stderr) return url def get_album_tracks(self, album_id): data = self.api.get_album_info(album_id, include_tracks=True) album = _parse_album(data) return [_parse_track(t, album) for t in data["tracks"]] def get_promoted_tracks(self): data = self.api.get_promoted_songs() # self.dmpdata("promoted_tracks", data) return [_parse_track(t) for t in data] def get_genres(self, parent=None): data = self.api.get_genres(parent_genre_id=parent) return [_parse_genre(g) for g in data] def get_artist_info(self, artist_id, doRelated=False): ret = {"albums": [], "toptracks": [], "related": []} # Happens,some library tracks have no artistId entry if artist_id is None or artist_id == "None": print("get_artist_albums: artist_id is None", file=sys.stderr) return ret else: print("get_artist_albums: artist_id %s" % artist_id, file=sys.stderr) maxrel = 20 if doRelated else 0 maxtop = 0 if doRelated else 10 incalbs = False if doRelated else True data = self.api.get_artist_info(artist_id, include_albums=incalbs, max_top_tracks=maxtop, max_rel_artist=maxrel) # self.dmpdata("artist_info", data) if "albums" in data: ret["albums"] = [_parse_album(alb) for alb in data["albums"]] if "topTracks" in data: ret["toptracks"] = [_parse_track(t) for t in data["topTracks"]] if "related_artists" in data: ret["related"] = [_parse_artist(a) for a in data["related_artists"]] return ret def get_artist_related(self, artist_id): data = self.get_artist_info(artist_id, doRelated=True) return data["related"] def search(self, query): data = self.api.search(query, max_results=50) # self.dmpdata("Search", data) tr = [_parse_track(i["track"]) for i in data["song_hits"]] print("track ok", file=sys.stderr) ar = [_parse_artist(i["artist"]) for i in data["artist_hits"]] print("artist ok", file=sys.stderr) al = [_parse_album(i["album"]) for i in data["album_hits"]] print("album ok", file=sys.stderr) # self.dmpdata("Search playlists", data['playlist_hits']) try: pl = [_parse_splaylist(i) for i in data["playlist_hits"]] except: pl = [] print("playlist ok", file=sys.stderr) return SearchResult(artists=ar, albums=al, playlists=pl, tracks=tr)
class GMusic(object): def __init__(self): self.authenticated = False self.all_access = False self.library_loaded = False self.all_songs = [] self.letters = {} self.artists = {} self.albums = {} self.genres = {} self.tracks_by_letter = {} self.tracks_by_artist = {} self.tracks_by_album = {} self.tracks_by_genre = {} self._device = None self._webclient = Webclient(debug_logging=False) self._mobileclient = Mobileclient(debug_logging=False) self._playlists = [] self._playlist_contents = [] self._stations = [] def _get_device_id(self): if self.authenticated: devices = self._webclient.get_registered_devices() for dev in devices: if dev['type'] == 'PHONE': self._device = dev['id'][2:] break elif dev['type'] == 'IOS': self._device = dev['id'] break def _set_all_access(self): settings = self._webclient._make_call(webclient.GetSettings, '') self.all_access = True if 'isSubscription' in settings['settings'] and settings['settings']['isSubscription'] == True else False def _set_all_songs(self): if len(self.all_songs) == 0: try: self.all_songs = self._mobileclient.get_all_songs() except NotLoggedIn: if self.authenticate(): self.all_songs = self._mobileclient.get_all_songs() else: return [] else: return self.all_songs def authenticate(self, email, password): try: mcauthenticated = self._mobileclient.login(email, password) except AlreadyLoggedIn: mcauthenticated = True try: wcauthenticated = self._webclient.login(email, password) except AlreadyLoggedIn: wcauthenticated = True self.authenticated = mcauthenticated and wcauthenticated self._set_all_access() self._get_device_id() return self.authenticated def load_data(self): self._set_all_songs() for song in self.all_songs: thumb = None letter = song['title'][0] artist = song['artist'] album = song['album'] genre = song['genre'] if 'genre' in song else '(None)' if letter not in self.tracks_by_letter: self.tracks_by_letter[letter] = [] self.letters[letter] = None if artist not in self.tracks_by_artist: self.tracks_by_artist[artist] = [] self.artists[artist] = None if album not in self.tracks_by_album: self.tracks_by_album[album] = [] self.albums[album] = None if genre not in self.tracks_by_genre: self.tracks_by_genre[genre] = [] self.genres[genre] = None track = {'artist': artist, 'album': album} if 'title' in song: track['title'] = song['title'] if 'album' in song: track['album'] = song['album'] if 'artist' in song: track['artist'] = song['artist'] if 'durationMillis' in song: track['durationMillis'] = song['durationMillis'] if 'id' in song: track['id'] = song['id'] if 'trackNumber' in song: track['trackType'] = song['trackNumber'] if 'storeId' in song: track['storeId'] = song['storeId'] if 'albumArtRef' in song: track['albumArtRef'] = song['albumArtRef'] thumb = song['albumArtRef'][0]['url'] self.letters[letter] = thumb self.artists[artist] = thumb self.albums[album] = thumb self.genres[genre] = thumb self.tracks_by_letter[letter].append({'track': track, 'thumb': thumb, 'id': song['id']}) self.tracks_by_artist[artist].append({'track': track, 'thumb': thumb, 'id': song['id']}) self.tracks_by_album[album].append({'track': track, 'thumb': thumb, 'id': song['id']}) self.tracks_by_genre[genre].append({'track': track, 'thumb': thumb, 'id': song['id']}) self.library_loaded = True def get_tracks_for_type(self, type, name): type = type.lower() if type == 'artists': return self.tracks_by_artist[name] elif type == 'albums': return self.tracks_by_album[name] elif type == 'genres': return self.tracks_by_genre[name] elif type == 'songs by letter': return self.tracks_by_letter[name] else: return {} def get_song(self, id): return [x for x in self.all_songs if x['id'] == id][0] def get_all_playlists(self): if len(self._playlists) == 0: try: self._playlists = self._mobileclient.get_all_playlists() except NotLoggedIn: if self.authenticate(): self._playlists = self._mobileclient.get_all_playlists() else: return [] return self._playlists def get_all_user_playlist_contents(self, id): tracks = [] if len(self._playlist_contents) == 0: try: self._playlist_contents = self._mobileclient.get_all_user_playlist_contents() except NotLoggedIn: if self.authenticate(): self._playlist_contents = self._mobileclient.get_all_user_playlist_contents() else: return [] for playlist in self._playlist_contents: if id == playlist['id']: tracks = playlist['tracks'] break return tracks def get_shared_playlist_contents(self, token): playlist = [] try: playlist = self._mobileclient.get_shared_playlist_contents(token) except NotLoggedIn: if self.authenticate(): playlist = self._mobileclient.get_shared_playlist_contents(token) else: return [] return playlist def get_all_stations(self): if len(self._stations) == 0: try: self._stations = self._mobileclient.get_all_stations() except NotLoggedIn: if self.authenticate(): self._stations = self._mobileclient.get_all_stations() else: return [] return self._stations def get_station_tracks(self, id, num_tracks=200): tracks = [] try: tracks = self._mobileclient.get_station_tracks(id, num_tracks) except NotLoggedIn: if self.authenticate(): tracks = self._mobileclient.get_station_tracks(id, num_tracks) else: return [] return tracks def get_genres(self): genres = [] try: genres = self._mobileclient.get_genres() except NotLoggedIn: if self.authenticate(): genres = self._mobileclient.get_genres() else: return [] return genres def create_station(self, name, id): station = None try: station = self._mobileclient.create_station(name=name, genre_id=id) except NotLoggedIn: if self.authenticate(): station = self._mobileclient.create_station(name=name, genre_id=id) else: return [] return station def search_all_access(self, query, max_results=50): results = None try: results = self._mobileclient.search_all_access(query, max_results) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.search_all_access(query, max_results) else: return [] return results def get_artist_info(self, id, include_albums=True, max_top_tracks=5, max_rel_artist=5): results = None try: results = self._mobileclient.get_artist_info(id, include_albums=include_albums, max_top_tracks=max_top_tracks, max_rel_artist=max_rel_artist) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.get_artist_info(id, include_albums=include_albums, max_top_tracks=max_top_tracks, max_rel_artist=max_rel_artist) else: return [] return results def get_album_info(self, id, include_tracks=True): results = None try: results = self._mobileclient.get_album_info(id, include_tracks=include_tracks) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.get_album_info(id, include_tracks=include_tracks) else: return [] return results def add_aa_track(self, id): track = None try: track = self._mobileclient.add_aa_track(id) except NotLoggedIn: if self.authenticate(): track = self._mobileclient.add_aa_track(id) else: return None return track def add_songs_to_playlist(self, playlist_id, song_ids): tracks = None try: tracks = self._mobileclient.add_songs_to_playlist(playlist_id, song_ids) except NotLoggedIn: if self.authenticate(): tracks = self._mobileclient.add_songs_to_playlist(playlist_id, song_ids) else: return None return tracks def get_stream_url(self, id): try: stream_url = self._mobileclient.get_stream_url(id, self._device) except NotLoggedIn: if self.authenticate(): stream_url = self._mobileclient.get_stream_url(id, self._device) else: return '' except CallFailure: raise CallFailure('Could not play song with id: ' + id, 'get_stream_url') return stream_url def reset_library(self): self.library_loaded = False self.all_songs[:] = [] self._playlists[:] = [] self._playlist_contents[:] = [] self._stations[:] = [] self.letters.clear() self.artists.clear() self.albums.clear() self.genres.clear() self.tracks_by_letter.clear() self.tracks_by_artist.clear() self.tracks_by_album.clear() self.tracks_by_genre.clear()
class Gmusicdownloader: _api = None outputDir = "" def __init__(self, email, password, deviceid, outputDir=os.getcwd()): self._api = Mobileclient(False) self.outputDir = outputDir print("Logging in...", end='', flush=True) self._api.login(email, password, deviceid) if self._api.is_authenticated(): print(bColors.OKGREEN + " Logged in!" + bColors.ENDC) else: print(bColors.FAIL + " Unable to log in!" + bColors.ENDC) sys.exit(1) def escapeName(self, string): return re.sub('[<>:"/\\\|?*]|\.$', '', string) def tagTrack(self, path, track): audiofile = eyed3.load(path) audiofile.initTag() audiofile.tag.artist = track['artist'] audiofile.tag.album = track['album'] audiofile.tag.album_artist = track['albumArtist'] audiofile.tag.title = track['title'] audiofile.tag.track_num = track['trackNumber'] if 'discNumber' in track: audiofile.tag.disc_num = track['discNumber'] if 'year' in track: audiofile.tag.release_date = track['year'] if 'genre' in track: audiofile.tag.genre = track['genre'] audiofile.tag.save() def downloadCover(self, dirpath, album): if os.path.exists(dirpath + '/' + 'cover.jpg'): return resp = requests.get(album['albumArtRef']) with open(dirpath + '/' + 'cover.jpg', 'wb') as f: f.write(resp.content) def parseSelection(self, selection): try: items = [] if selection.find(',') > 0: items += selection.split(',') else: items += [selection] for i in range(len(items)): item = items[i] if item.find('-') < 0: continue rng = item.split('-') del items[i] items += [str(i) for i in range(int(rng[0]), int(rng[1]) + 1)] items = [int(i) for i in items] items.sort() return items except: raise ValueError('Invalid selection!') def downloadAlbum(self, album): dirpath = "{}/{}/{}".format(self.outputDir, self.escapeName(album['albumArtist']), self.escapeName(album['name'])) dirpath = unidecode.unidecode(dirpath) if not os.path.isdir(dirpath): os.makedirs(dirpath) self.downloadCover(dirpath, album) #Iterate over album tracks for track in album['tracks']: trackId = track['nid'] path = dirpath + "/{} {}.mp3".format( str(track['trackNumber']).zfill(2), self.escapeName(track['title'])) path = unidecode.unidecode(path) if not os.path.exists(path): stream = self._api.get_stream_url(trackId) resp = requests.get(stream) with open(path, 'wb') as f: f.write(resp.content) self.tagTrack(path, track) print("{}✔️ {} -- {} {}".format(bColors.OKGREEN, track['artist'], track['title'], bColors.ENDC)) #Iterate over selected albums def downloadSelection(self, selection, search): for albumNum in self.parseSelection(selection): if albumNum > len(search['album_hits']): raise ValueError('Invalid index: ' + str(albumNum)) album = self._api.get_album_info( search['album_hits'][albumNum - 1]['album']['albumId']) print() print(bColors.WARNING + 'Downloading ' + bColors.ENDC + bColors.FAIL + album['albumArtist'] + ' -- ' + \ album['name'] + '...' + bColors.ENDC) self.downloadAlbum(album) def searchAndDownload(self): while True: print() #Album search searchStr = input(bColors.OKBLUE + bColors.BOLD + "? Search for an album: " + bColors.ENDC) print() search = self._api.search(searchStr, 20) if len(search['album_hits']) == 0: print(bColors.FAIL + "Nothing found!" + bColors.ENDC) print() continue if 'album_hits' in search: albumCounter = 1 for album in search['album_hits']: album = album['album'] albumStr = "{}) {} -- {}".format(str(albumCounter), album['albumArtist'], album['name']) if 'year' in album: albumStr += " ({})".format(str(album['year'])) print(bColors.HEADER + albumStr + bColors.ENDC) albumCounter += 1 print() selection = input( bColors.OKBLUE + bColors.BOLD + '? Album to download (eg. 1, 2, 3.. or 1-3) or (b)ack or (e)xit: ' + bColors.ENDC) if selection == 'e': sys.exit(0) elif selection == 'b': continue self.downloadSelection(selection, search) def sync(self): library = self._api.get_all_songs() library = sorted(library, key=lambda k: (k['artist'], k['album'], k['trackNumber'])) print() for track in library: dirpath = "{}/{}/{}".format(self.outputDir, self.escapeName(track['albumArtist']), self.escapeName(track['album'])) dirpath = unidecode.unidecode(dirpath) if not os.path.isdir(dirpath): os.makedirs(dirpath) track['albumArtRef'] = track['albumArtRef'][0]['url'] self.downloadCover(dirpath, track) trackId = track['nid'] path = dirpath + "/{} {}.mp3".format( str(track['trackNumber']).zfill(2), self.escapeName(track['title'])) path = unidecode.unidecode(path) if not os.path.exists(path): stream = self._api.get_stream_url(trackId) resp = requests.get(stream) with open(path, 'wb') as f: f.write(resp.content) self.tagTrack(path, track) print("{}✔️ {} -- {} {}".format(bColors.OKGREEN, track['artist'], track['title'], bColors.ENDC))
def normalizePath(input): return vfn(input, space="keep", initCap=False).decode('utf-8').rstrip(".") login = sys.argv[1] targetDir = os.getcwd() albumId = sys.argv[2] password = getpass.getpass() eyed3.log.setLevel("ERROR") api = Mobileclient(debug_logging=False) api.login(login, password, Mobileclient.FROM_MAC_ADDRESS) album = api.get_album_info(albumId) dirName = normalizePath("%s - %s" % (album["artist"], album["name"])) dirPath = targetDir + "/" + dirName print("downloading to directory: " + dirPath) if not os.path.exists(dirPath): os.makedirs(dirPath) for song in album["tracks"]: url = api.get_stream_url(song_id=song["storeId"], quality="hi") fileName = normalizePath( "%s. %s - %s.mp3" % (song["trackNumber"], song["artist"], song["title"])) filePath = dirPath + "/" + fileName print("downloading: " + fileName) urlretrieve(url, filePath)
class GoogleMusicApi: def __init__(self): self._api = Mobileclient() def login(self, username, password, device_id): try: return self._api.login(username, password, device_id) except AlreadyLoggedIn: Logger.debug('API: Already logged in') return True def relogin(self, username, password, device_id): try: return self._api.login(username, password, device_id) except AlreadyLoggedIn: self._api.logout() return self._api.login(username, password, device_id) def logout(self): return self._api.logout() def get_registered_mobile_devices(self): devices = self._api.get_registered_devices() mobile_devices = [] for device in devices: if device['type'] == "ANDROID": # TODO: Add iOS mobile_devices.append({ 'name': device['friendlyName'], 'id': device['id'][2:] }) return mobile_devices def get_stream_url(self, track_id, quality): return self._api.get_stream_url(song_id=track_id, quality=quality) def get_library(self): return self._api.get_all_songs() def get_album_info(self, album_id): return self._api.get_album_info(album_id) def search(self, query, max_results=25): # TODO: make number of results configurable / add to settings try: return self._api.search_all_access(query, max_results) # TODO: remove when gmusicapi 9.0.1 is stable except AttributeError: # develop version of gmusicapi is installed return self._api.search(query, max_results) def get_station_tracks(self, title, seed, num_tracks=25, recently_played_ids=None): # TODO: make number of results configurable / add to settings # TODO: check for existing stations, so we don't always create new ones (maybe not necessary: stations created with same seed have the same id seed_type = seed['type'] seed = seed['seed'] station_id = '' Logger.debug('Station: Creating station (Title: {}, Seed: {}, Type:{}'.format(title, seed, seed_type)) if seed_type == 'track': station_id = self.create_station(title, track_id=seed) elif seed_type == 'artist': station_id = self.create_station(title, artist_id=seed) elif seed_type == 'album': station_id = self.create_station(title, album_id=seed) elif seed_type == 'genre': station_id = self.create_station(title, genre_id=seed) elif seed_type == 'curated': Logger.debug("Station: CuratedStationId seed, don't know what to do :(") else: Logger.error("Station: Unknown seed, don't know what to do :(") if station_id: Logger.debug('Station: ID is ' + station_id) station_tracks = self._api.get_station_tracks(station_id, num_tracks, recently_played_ids) Logger.debug('Station: Station has {} tracks'.format(len(station_tracks))) return station_tracks else: Logger.warning("Station: Could not retrieve station ID") return [] def get_feeling_lucky_station_tracks(self, num_tracks=25, recently_played_ids=None): # TODO: make number of results configurable / add to settings return self._api.get_station_tracks('IFL', num_tracks, recently_played_ids) def create_station(self, name, track_id=None, artist_id=None, album_id=None, genre_id=None, playlist_token=None): return self._api.create_station(name, track_id=track_id, artist_id=artist_id, album_id=album_id, genre_id=genre_id, playlist_token=playlist_token) def increment_track_playcount(self, track_id): self._api.increment_song_playcount(track_id)
class GMusicSession(object): def __init__(self): super(GMusicSession, self).__init__() logger.info('Mopidy uses Google Music') self.api = Mobileclient() def login(self, username, password, deviceid): if self.api.is_authenticated(): self.api.logout() try: self.api.login(username, password) except CallFailure as error: logger.error(u'Failed to login as "%s": %s', username, error) if self.api.is_authenticated(): if deviceid is None: self.deviceid = self.get_deviceid(username, password) else: self.deviceid = deviceid else: return False def logout(self): if self.api.is_authenticated(): return self.api.logout() else: return True def get_all_songs(self): if self.api.is_authenticated(): return self.api.get_all_songs() else: return {} def get_stream_url(self, song_id): if self.api.is_authenticated(): try: return self.api.get_stream_url(song_id, self.deviceid) except CallFailure as error: logger.error(u'Failed to lookup "%s": %s', song_id, error) def get_all_user_playlist_contents(self): if self.api.is_authenticated(): return self.api.get_all_user_playlist_contents() else: return {} def get_shared_playlist_contents(self, shareToken): if self.api.is_authenticated(): return self.api.get_shared_playlist_contents(shareToken) else: return {} def get_all_playlists(self): if self.api.is_authenticated(): return self.api.get_all_playlists() else: return {} def get_deviceid(self, username, password): logger.warning(u'No mobile device ID configured. ' u'Trying to detect one.') webapi = Webclient(validate=False) webapi.login(username, password) devices = webapi.get_registered_devices() deviceid = None for device in devices: if device['type'] == 'PHONE' and device['id'][0:2] == u'0x': # Omit the '0x' prefix deviceid = device['id'][2:] break webapi.logout() if deviceid is None: logger.error(u'No valid mobile device ID found. ' u'Playing songs will not work.') else: logger.info(u'Using mobile device ID %s', deviceid) return deviceid def get_track_info(self, store_track_id): if self.api.is_authenticated(): try: return self.api.get_track_info(store_track_id) except CallFailure as error: logger.error(u'Failed to get All Access track info: %s', error) def get_album_info(self, albumid, include_tracks=True): if self.api.is_authenticated(): try: return self.api.get_album_info(albumid, include_tracks) except CallFailure as error: logger.error(u'Failed to get All Access album info: %s', error)
class GMusic(object): def __init__(self): self.authenticated = False self.all_access = False self.library_loaded = False self.all_songs = [] self.letters = {} self.artists = {} self.albums = {} self.genres = {} self.tracks_by_letter = {} self.tracks_by_artist = {} self.tracks_by_album = {} self.tracks_by_genre = {} self._device = None self._webclient = Webclient(debug_logging=False) self._mobileclient = Mobileclient(debug_logging=False) self._playlists = [] self._playlist_contents = [] self._stations = [] def _get_device_id(self): if self.authenticated: devices = self._webclient.get_registered_devices() for dev in devices: if dev['type'] == 'PHONE': self._device = dev['id'][2:] break elif dev['type'] == 'IOS': self._device = dev['id'] break def _set_all_access(self): settings = self._webclient._make_call(webclient.GetSettings, '') self.all_access = True if 'isSubscription' in settings[ 'settings'] and settings['settings'][ 'isSubscription'] == True else False def _set_all_songs(self): if len(self.all_songs) == 0: try: self.all_songs = self._mobileclient.get_all_songs() except NotLoggedIn: if self.authenticate(): self.all_songs = self._mobileclient.get_all_songs() else: return [] else: return self.all_songs def authenticate(self, email, password): try: mcauthenticated = self._mobileclient.login(email, password) except AlreadyLoggedIn: mcauthenticated = True try: wcauthenticated = self._webclient.login(email, password) except AlreadyLoggedIn: wcauthenticated = True self.authenticated = mcauthenticated and wcauthenticated self._set_all_access() self._get_device_id() return self.authenticated def load_data(self): self._set_all_songs() for song in self.all_songs: thumb = None letter = song['title'][0] artist = song['artist'] album = song['album'] genre = song['genre'] if 'genre' in song else '(None)' if letter not in self.tracks_by_letter: self.tracks_by_letter[letter] = [] self.letters[letter] = None if artist not in self.tracks_by_artist: self.tracks_by_artist[artist] = [] self.artists[artist] = None if album not in self.tracks_by_album: self.tracks_by_album[album] = [] self.albums[album] = None if genre not in self.tracks_by_genre: self.tracks_by_genre[genre] = [] self.genres[genre] = None track = {'artist': artist, 'album': album} if 'title' in song: track['title'] = song['title'] if 'album' in song: track['album'] = song['album'] if 'artist' in song: track['artist'] = song['artist'] if 'durationMillis' in song: track['durationMillis'] = song['durationMillis'] if 'id' in song: track['id'] = song['id'] if 'trackNumber' in song: track['trackType'] = song['trackNumber'] if 'storeId' in song: track['storeId'] = song['storeId'] if 'albumArtRef' in song: track['albumArtRef'] = song['albumArtRef'] thumb = song['albumArtRef'][0]['url'] self.letters[letter] = thumb self.artists[artist] = thumb self.albums[album] = thumb self.genres[genre] = thumb self.tracks_by_letter[letter].append({ 'track': track, 'thumb': thumb, 'id': song['id'] }) self.tracks_by_artist[artist].append({ 'track': track, 'thumb': thumb, 'id': song['id'] }) self.tracks_by_album[album].append({ 'track': track, 'thumb': thumb, 'id': song['id'] }) self.tracks_by_genre[genre].append({ 'track': track, 'thumb': thumb, 'id': song['id'] }) self.library_loaded = True def get_tracks_for_type(self, type, name): type = type.lower() if type == 'artists': return self.tracks_by_artist[name] elif type == 'albums': return self.tracks_by_album[name] elif type == 'genres': return self.tracks_by_genre[name] elif type == 'songs by letter': return self.tracks_by_letter[name] else: return {} def get_song(self, id): return [x for x in self.all_songs if x['id'] == id][0] def get_all_playlists(self): if len(self._playlists) == 0: try: self._playlists = self._mobileclient.get_all_playlists() except NotLoggedIn: if self.authenticate(): self._playlists = self._mobileclient.get_all_playlists() else: return [] return self._playlists def get_all_user_playlist_contents(self, id): tracks = [] if len(self._playlist_contents) == 0: try: self._playlist_contents = self._mobileclient.get_all_user_playlist_contents( ) except NotLoggedIn: if self.authenticate(): self._playlist_contents = self._mobileclient.get_all_user_playlist_contents( ) else: return [] for playlist in self._playlist_contents: if id == playlist['id']: tracks = playlist['tracks'] break return tracks def get_shared_playlist_contents(self, token): playlist = [] try: playlist = self._mobileclient.get_shared_playlist_contents(token) except NotLoggedIn: if self.authenticate(): playlist = self._mobileclient.get_shared_playlist_contents( token) else: return [] return playlist def get_all_stations(self): if len(self._stations) == 0: try: self._stations = self._mobileclient.get_all_stations() except NotLoggedIn: if self.authenticate(): self._stations = self._mobileclient.get_all_stations() else: return [] return self._stations def get_station_tracks(self, id, num_tracks=200): tracks = [] try: tracks = self._mobileclient.get_station_tracks(id, num_tracks) except NotLoggedIn: if self.authenticate(): tracks = self._mobileclient.get_station_tracks(id, num_tracks) else: return [] return tracks def get_genres(self): genres = [] try: genres = self._mobileclient.get_genres() except NotLoggedIn: if self.authenticate(): genres = self._mobileclient.get_genres() else: return [] return genres def create_station(self, name, id): station = None try: station = self._mobileclient.create_station(name=name, genre_id=id) except NotLoggedIn: if self.authenticate(): station = self._mobileclient.create_station(name=name, genre_id=id) else: return [] return station def search_all_access(self, query, max_results=50): results = None try: results = self._mobileclient.search_all_access(query, max_results) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.search_all_access( query, max_results) else: return [] return results def get_artist_info(self, id, include_albums=True, max_top_tracks=5, max_rel_artist=5): results = None try: results = self._mobileclient.get_artist_info( id, include_albums=include_albums, max_top_tracks=max_top_tracks, max_rel_artist=max_rel_artist) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.get_artist_info( id, include_albums=include_albums, max_top_tracks=max_top_tracks, max_rel_artist=max_rel_artist) else: return [] return results def get_album_info(self, id, include_tracks=True): results = None try: results = self._mobileclient.get_album_info( id, include_tracks=include_tracks) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.get_album_info( id, include_tracks=include_tracks) else: return [] return results def add_aa_track(self, id): track = None try: track = self._mobileclient.add_aa_track(id) except NotLoggedIn: if self.authenticate(): track = self._mobileclient.add_aa_track(id) else: return None return track def add_songs_to_playlist(self, playlist_id, song_ids): tracks = None try: tracks = self._mobileclient.add_songs_to_playlist( playlist_id, song_ids) except NotLoggedIn: if self.authenticate(): tracks = self._mobileclient.add_songs_to_playlist( playlist_id, song_ids) else: return None return tracks def get_stream_url(self, id): try: stream_url = self._mobileclient.get_stream_url(id, self._device) except NotLoggedIn: if self.authenticate(): stream_url = self._mobileclient.get_stream_url( id, self._device) else: return '' except CallFailure: raise CallFailure('Could not play song with id: ' + id, 'get_stream_url') return stream_url
class GMusicWrapper: def __init__(self, username, password): self._api = Mobileclient() success = self._api.login(username, password, Mobileclient.FROM_MAC_ADDRESS) if not success: raise Exception("Unsuccessful login. Aborting!") def _search(self, query_type, query): results = self._api.search(query) hits_key = "%s_hits" % query_type if hits_key not in results: return [] # Ugh, Google had to make this schema nonstandard... if query_type == 'song': query_type = 'track' return map(lambda x: x[query_type], results[hits_key]) def get_artist(self, name): search = self._search("artist", name) if len(search) == 0: return False return self._api.get_artist_info(search[0]['artistId'], max_top_tracks=100) def get_album(self, name, artist_name=None): if artist_name: name = "%s %s" % (name, artist_name) search = self._search("album", name) if len(search) == 0: return False return self._api.get_album_info(search[0]['albumId']) def get_song(self, name, artist_name=None): if artist_name: name = "%s %s" % (artist_name, name) search = self._search("song", name) if len(search) == 0: return False return search[0] def get_station(self, title, artist_id=None): if artist_id != None: return self._api.create_station(title, artist_id=artist_id) def get_station_tracks(self, station_id): return self._api.get_station_tracks(station_id) def get_google_stream_url(self, song_id): return self._api.get_stream_url(song_id) def get_stream_url(self, song_id): return "%s/stream/%s" % (environ['APP_URL'], song_id) def get_all_user_playlist_contents(self): return self._api.get_all_user_playlist_contents() @classmethod def generate_api(self): return self(environ['GOOGLE_EMAIL'], environ['GOOGLE_PASSWORD'])
try: for album in api.get_artist_info(artistID, True, 0, 0)['albums']: albumIDs.append(album['albumId']) except KeyError: print("No albums for " + artist) exit() except: print("Critical error for " + artist) print(error) exit() # Use album IDs to get track IDs trackIDs = [] for albumID in albumIDs: try: for track in api.get_album_info(albumID)['tracks']: trackIDs.append(track['storeId']) except KeyError: print(albumID + ' has no tracks') except HTTPError as error: print(error) except CallFailure as error: print(error) # Download using get_url_stream downDir = input('Enter download directory: ') if not downDir.endswith('\\'): downDir += '\\' totalTracks = len(trackIDs) for i in range(totalTracks): try: trackInfo = api.get_track_info(trackIDs[0])
class GMusic(object): def __init__(self): self.authenticated = False self.all_access = False self._device = None self._webclient = Webclient(debug_logging=False) self._mobileclient = Mobileclient(debug_logging=False) self._playlists = [] self._playlist_contents = [] self._all_songs = [] self._all_artists = {} self._all_albums = {} self._all_genres = {} self._stations = [] def _get_device_id(self): if self.authenticated: devices = self._webclient.get_registered_devices() for dev in devices: if dev['type'] == 'PHONE': self._device = dev['id'][2:] break def _set_all_access(self): settings = self._webclient._make_call(webclient.GetSettings, '') self.all_access = True if 'isSubscription' in settings['settings'] and settings['settings']['isSubscription'] == True else False def authenticate(self, email, password): try: mcauthenticated = self._mobileclient.login(email, password) except AlreadyLoggedIn: mcauthenticated = True try: wcauthenticated = self._webclient.login(email, password) except AlreadyLoggedIn: wcauthenticated = True self.authenticated = mcauthenticated and wcauthenticated self._get_device_id() self._set_all_access() return self.authenticated def get_all_songs(self, id=None): if len(self._all_songs) == 0: try: self._all_songs = self._mobileclient.get_all_songs() except NotLoggedIn: if self.authenticate(): self._all_songs = self._mobileclient.get_all_songs() else: return [] if id: return [x for x in self._all_songs if x['id'] == id][0] else: return self._all_songs def get_all_artists(self): if not self._all_artists: songs = self.get_all_songs() for song in songs: artist = song['artist'] thumb = None if artist not in self._all_artists: self._all_artists[artist] = [] track = {'title': song['title'], 'album': song['album'], 'artist': artist, 'durationMillis': song['durationMillis'], 'trackType': song['trackNumber'], 'id': song['id']} if 'albumArtRef' in song: track['albumArtRef'] = song['albumArtRef'] if 'artistArtRef' in song: thumb = song['artistArtRef'][0]['url'] if 'storeId' in song: track['storeId'] = song['storeId'] self._all_artists[artist].append({'track': track, 'thumb': thumb, 'id': song['id']}) return self._all_artists def get_all_albums(self): if not self._all_albums: songs = self.get_all_songs() for song in songs: album = song['album'] thumb = None if album not in self._all_albums: self._all_albums[album] = [] track = {'title': song['title'], 'album': album, 'artist': song['artist'], 'durationMillis': song['durationMillis'], 'trackType': song['trackNumber'], 'id': song['id']} if 'albumArtRef' in song: track['albumArtRef'] = song['albumArtRef'] thumb = song['albumArtRef'][0]['url'] if 'storeId' in song: track['storeId'] = song['storeId'] self._all_albums[album].append({'track': track, 'thumb': thumb, 'id': song['id']}) return self._all_albums def get_all_genres(self): if not self._all_genres: songs = self.get_all_songs() for song in songs: genre = song['genre'] if genre not in self._all_genres: self._all_genres[genre] = [] track = {'title': song['title'], 'album': song['album'], 'artist': song['artist'], 'durationMillis': song['durationMillis'], 'trackType': song['trackNumber'], 'id': song['id']} if 'albumArtRef' in song: track['albumArtRef'] = song['albumArtRef'] if 'storeId' in song: track['storeId'] = song['storeId'] self._all_genres[genre].append({'track': track, 'id': song['id']}) return self._all_genres def get_all_playlists(self): if len(self._playlists) == 0: try: self._playlists = self._mobileclient.get_all_playlists() except NotLoggedIn: if self.authenticate(): self._playlists = self._mobileclient.get_all_playlists() else: return [] return self._playlists def get_all_user_playlist_contents(self, id): tracks = [] if len(self._playlist_contents) == 0: try: self._playlist_contents = self._mobileclient.get_all_user_playlist_contents() except NotLoggedIn: if self.authenticate(): self._playlist_contents = self._mobileclient.get_all_user_playlist_contents() else: return [] for playlist in self._playlist_contents: if id == playlist['id']: tracks = playlist['tracks'] break return tracks def get_shared_playlist_contents(self, token): playlist = [] try: playlist = self._mobileclient.get_shared_playlist_contents(token) except NotLoggedIn: if self.authenticate(): playlist = self._mobileclient.get_shared_playlist_contents(token) else: return [] return playlist def get_all_stations(self): if len(self._stations) == 0: try: self._stations = self._mobileclient.get_all_stations() except NotLoggedIn: if self.authenticate(): self._stations = self._mobileclient.get_all_stations() else: return [] return self._stations def get_station_tracks(self, id, num_tracks=200): tracks = [] try: tracks = self._mobileclient.get_station_tracks(id, num_tracks) except NotLoggedIn: if self.authenticate(): tracks = self._mobileclient.get_station_tracks(id, num_tracks) else: return [] return tracks def get_genres(self): genres = [] try: genres = self._mobileclient.get_genres() except NotLoggedIn: if self.authenticate(): genres = self._mobileclient.get_genres() else: return [] return genres def create_station(self, name, id): station = None try: station = self._mobileclient.create_station(name=name, genre_id=id) except NotLoggedIn: if self.authenticate(): station = self._mobileclient.create_station(name=name, genre_id=id) else: return [] return station def search_all_access(self, query, max_results=50): results = None try: results = self._mobileclient.search_all_access(query, max_results) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.search_all_access(query, max_results) else: return [] return results def get_artist_info(self, id, include_albums=True, max_top_tracks=5, max_rel_artist=5): results = None try: results = self._mobileclient.get_artist_info(id, include_albums=include_albums, max_top_tracks=max_top_tracks, max_rel_artist=max_rel_artist) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.get_artist_info(id, include_albums=include_albums, max_top_tracks=max_top_tracks, max_rel_artist=max_rel_artist) else: return [] return results def get_album_info(self, id, include_tracks=True): results = None try: results = self._mobileclient.get_album_info(id, include_tracks=include_tracks) except NotLoggedIn: if self.authenticate(): results = self._mobileclient.get_album_info(id, include_tracks=include_tracks) else: return [] return results def get_stream_url(self, id): try: stream_url = self._mobileclient.get_stream_url(id, self._device) except NotLoggedIn: if self.authenticate(): stream_url = self._mobileclient.get_stream_url(id, self._device) else: return '' except CallFailure: raise CallFailure('Could not play song with id: ' + id, 'get_stream_url') return stream_url
class GMusicWrapper(object): def __init__(self, username, password, logger=None): self._api = Mobileclient() self.logger = logger success = self._api.login(username, password, getenv('ANDROID_ID', Mobileclient.FROM_MAC_ADDRESS)) if not success: raise Exception("Unsuccessful login. Aborting!") try: assert literal_eval(getenv("DEBUG_FORCE_LIBRARY", "False")) self.use_store = False except (AssertionError, ValueError): # AssertionError if it's False, ValueError if it's not set / not set to a proper boolean string self.use_store = self._api.is_subscribed # Populate our library self.start_indexing() def start_indexing(self): self.library = {} self.albums = set([]) self.artists = set([]) self.indexing_thread = threading.Thread( target=self.index_library ) self.indexing_thread.start() def log(self, log_str): if self.logger != None: self.logger.debug(log_str) def _search(self, query_type, query): try: results = self._api.search(query) except CallFailure: return [] hits_key = "%s_hits" % query_type if hits_key not in results: return [] # Ugh, Google had to make this schema nonstandard... if query_type == 'song': query_type = 'track' return [x[query_type] for x in results[hits_key]] def is_indexing(self): return self.indexing_thread.is_alive() def index_library(self): """ Downloads the a list of every track in a user's library and populates self.library with storeIds -> track definitions """ self.log('Fetching library...') tracks = self.get_all_songs() for track in tracks: song_id = track['id'] self.library[song_id] = track self.albums.add(track['album']) self.artists.add(track['artist']) self.log('Fetching library complete.') def get_artist(self, name): """ Fetches information about an artist given its name """ if self.use_store: search = self._search("artist", name) if len(search) == 0: return False return self._api.get_artist_info(search[0]['artistId'], max_top_tracks=100) else: search = {} search['topTracks'] = [] # Find the best artist we have, and then match songs to that artist likely_artist, score = process.extractOne(name, self.artists) if score < 70: return False for song_id, song in self.library.items(): if 'artist' in song and song['artist'].lower() == likely_artist.lower() and 'artistId' in song: if not search['topTracks']: # First entry # Copy artist details from the first song into the general artist response try: search['artistArtRef'] = song['artistArtRef'][0]['url'] except KeyError: pass search['name'] = song['artist'] search['artistId'] = song['artistId'] search['topTracks'].append(song) random.shuffle(search['topTracks']) # This is all music, not top, but the user probably would prefer it shuffled. if not search['topTracks']: return False return search def get_album(self, name, artist_name=None): if self.use_store: if artist_name: name = "%s %s" % (name, artist_name) search = self._search("album", name) if len(search) == 0: return False return self._api.get_album_info(search[0]['albumId']) else: search = {} search['tracks'] = [] if artist_name: artist_name, score = process.extractOne(artist_name, self.artists) if score < 70: return False name, score = process.extractOne(name, self.albums) if score < 70: return False for song_id, song in self.library.items(): if 'album' in song and song['album'].lower() == name.lower(): if not artist_name or ('artist' in song and song['artist'].lower() == artist_name.lower()): if not search['tracks']: # First entry search['albumArtist'] = song['albumArtist'] search['name'] = song['album'] try: search['albumId'] = song['albumId'] except KeyError: pass search['tracks'].append(song) if not search['tracks']: return False return search def get_latest_album(self, artist_name=None): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_info = artist_info['albums'] sorted_list = sorted(album_info.__iter__(), key=lambda s: s['year'], reverse=True) for index, val in enumerate(sorted_list): album_info = self._api.get_album_info(album_id=sorted_list[index]['albumId'], include_tracks=True) if len(album_info['tracks']) >= 5: return album_info return False def get_album_by_artist(self, artist_name, album_id=None): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_info = artist_info['albums'] random.shuffle(album_info) for index, val in enumerate(album_info): album = self._api.get_album_info(album_id=album_info[index]['albumId'], include_tracks=True) if album['albumId'] != album_id and len(album['tracks']) >= 5: return album return False def get_song(self, name, artist_name=None, album_name=None): if self.use_store: if artist_name: name = "%s %s" % (artist_name, name) elif album_name: name = "%s %s" % (album_name, name) search = self._search("song", name) if len(search) == 0: return False if album_name: for i in range(0, len(search) - 1): if album_name in search[i]['album']: return search[i] return search[0] else: search = {} if not name: return False if artist_name: artist_name, score = process.extractOne(artist_name, self.artists) if score < 70: return False if album_name: album_name, score = process.extractOne(album_name, self.albums) if score < 70: return False possible_songs = {song_id: song['title'] for song_id, song in self.library.items() if (not artist_name or ('artist' in song and song['artist'].lower() == artist_name.lower())) and (not album_name or ('album' in song and song['album'].lower() == album_name.lower()))} song, score, song_id = process.extractOne(name.lower(), possible_songs) if score < 70: return False else: return self.library[song_id] def get_promoted_songs(self): return self._api.get_promoted_songs() def get_station(self, title, track_id=None, artist_id=None, album_id=None): if artist_id is not None: if album_id is not None: if track_id is not None: return self._api.create_station(title, track_id=track_id) return self._api.create_station(title, album_id=album_id) return self._api.create_station(title, artist_id=artist_id) def get_station_tracks(self, station_id): return self._api.get_station_tracks(station_id) def get_google_stream_url(self, song_id): return self._api.get_stream_url(song_id) def get_stream_url(self, song_id): return "%s/alexa/stream/%s" % (getenv('APP_URL'), song_id) def get_thumbnail(self, artist_art): return artist_art.replace("http://", "https://") def get_all_user_playlist_contents(self): return self._api.get_all_user_playlist_contents() def get_all_songs(self): return self._api.get_all_songs() def rate_song(self, song, rating): return self._api.rate_songs(song, rating) def extract_track_info(self, track): # When coming from a playlist, track info is nested under the "track" # key if 'track' in track: track = track['track'] if self.use_store and 'storeId' in track: return track, track['storeId'] elif 'id' in track: return self.library[track['id']], track['id'] elif 'trackId' in track: return self.library[track['trackId']], track['trackId'] else: return None, None def get_artist_album_list(self, artist_name): search = self._search("artist", artist_name) if len(search) == 0: return "Unable to find the artist you requested." artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_list_text = "Here's the album listing for %s: " % artist_name counter = 0 for index, val in enumerate(artist_info['albums']): if counter > 25: # alexa will time out after 10 seconds if the list takes too long to iterate through break album_info = self._api.get_album_info(album_id=artist_info['albums'][index]['albumId'], include_tracks=True) if len(album_info['tracks']) > 5: counter += 1 album_list_text += (artist_info['albums'][index]['name']) + ", " return album_list_text def get_latest_artist_albums(self, artist_name): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_list = artist_info['albums'] sorted_list = sorted(album_list.__iter__(), key=lambda s: s['year'], reverse=True) speech_text = 'The latest albums by %s are ' % artist_name counter = 0 for index, val in enumerate(sorted_list): if counter > 5: break else: album_info = self._api.get_album_info(album_id=sorted_list[index]['albumId'], include_tracks=True) if len(album_info['tracks']) >= 5: counter += 1 album_name = sorted_list[index]['name'] album_year = sorted_list[index]['year'] speech_text += '%s, released in %d, ' % (album_name, album_year) return speech_text def closest_match(self, request_name, all_matches, artist_name='', minimum_score=70): # Give each match a score based on its similarity to the requested # name self.log('Finding closest match...') best_match = None request_name = request_name.lower() + artist_name.lower() scored_matches = [] for i, match in enumerate(all_matches): try: name = match['name'].lower() except (KeyError, TypeError): i = match name = all_matches[match]['title'].lower() if artist_name != "": name += all_matches[match]['artist'].lower() scored_matches.append({ 'index': i, 'name': name, 'score': fuzz.ratio(name, request_name) }) sorted_matches = sorted(scored_matches, key=lambda a: a['score'], reverse=True) try: top_scoring = sorted_matches[0] # Make sure we have a decent match (the score is n where 0 <= n <= 100) if top_scoring['score'] >= minimum_score: best_match = all_matches[top_scoring['index']] except IndexError: pass self.log('Found %s...' % best_match) return best_match def get_genres(self, parent_genre_id=None): return self._api.get_genres(parent_genre_id) def increment_song_playcount(self, song_id, plays=1, playtime=None): return self._api.increment_song_playcount(song_id, plays, playtime) def get_song_data(self, song_id): return self._api.get_track_info(song_id) @classmethod def generate_api(cls, **kwargs): return cls(getenv('GOOGLE_EMAIL'), getenv('GOOGLE_PASSWORD'), **kwargs)
class GMusicWrapper(object): def __init__(self, username, password, logger=None): self._api = Mobileclient() self.logger = logger success = self._api.login( username, password, environ.get('ANDROID_ID', Mobileclient.FROM_MAC_ADDRESS)) if not success: raise Exception("Unsuccessful login. Aborting!") # Populate our library self.library = {} self.indexing_thread = threading.Thread(target=self.index_library) self.indexing_thread.start() def populate_library( self ): #TODO: Use this as a function to refresh the library with Alexa via voice commands. # Populate our library self.library = {} self.indexing_thread = threading.Thread(target=self.index_library) self.indexing_thread.start() def _search(self, query_type, query): try: results = self._api.search(query) except CallFailure: return [] hits_key = "%s_hits" % query_type if hits_key not in results: return [] # Ugh, Google had to make this schema nonstandard... if query_type == 'song': query_type = 'track' return [x[query_type] for x in results[hits_key]] def _search_library_for_first(self, query_type, query): #try searching the library instead of the api for trackid, trackdata in self.library.items(): if query_type in trackdata: if query.lower() in trackdata[query_type].lower(): return trackdata return None def _search_library(self, query_type, query): #try searching the library instead of the api found = [] for trackid, trackdata in self.library.items(): if query_type in trackdata: if query.lower() in trackdata[query_type].lower(): found.append(trackdata) if not found: return None return found def is_indexing(self): return self.indexing_thread.is_alive() def index_library(self): """ Downloads the a list of every track in a user's library and populates self.library with storeIds -> track definitions """ self.logger.debug('Fetching library...') tracks = self.get_all_songs() for track in tracks: song_id = track['id'] self.library[song_id] = track self.logger.debug('Done! Discovered %d tracks.' % len(self.library)) def get_artist(self, name): """ Fetches information about an artist given its name """ search = self._search("artist", name) if len(search) == 0: search_lib = self._search_library("artist", name) if search_lib is not None: self.logger.debug(search_lib) return search_lib return False return self._api.get_artist_info(search[0]['artistId'], max_top_tracks=100) def get_album(self, name, artist_name=None): if artist_name: name = "%s %s" % (name, artist_name) search = self._search("album", name) if len(search) == 0: search_lib = self._search_library("album", name) if search_lib is not None: self.logger.debug(search_lib) return search_lib return False return self._api.get_album_info(search[0]['albumId']) def get_latest_album(self, artist_name=None): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_info = artist_info['albums'] sorted_list = sorted(album_info.__iter__(), key=lambda s: s['year'], reverse=True) for index, val in enumerate(sorted_list): album_info = self._api.get_album_info( album_id=sorted_list[index]['albumId'], include_tracks=True) if len(album_info['tracks']) >= 5: return album_info return False def get_album_by_artist(self, artist_name, album_id=None): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_info = artist_info['albums'] random.shuffle(album_info) for index, val in enumerate(album_info): album = self._api.get_album_info( album_id=album_info[index]['albumId'], include_tracks=True) if album['albumId'] != album_id and len(album['tracks']) >= 5: return album return False def get_song(self, song_name, artist_name=None, album_name=None): if artist_name: name = "%s %s" % (artist_name, song_name) elif album_name: name = "%s %s" % (album_name, song_name) self.logger.debug("get_song() : name: %s" % (name)) search = self._search("song", name) self.logger.debug("result length: %d" % len(search)) if len(search) == 0: search_lib = self._search_library_for_first("title", name) if search_lib is not None: return search_lib return False if album_name: for i in range(0, len(search) - 1): if album_name in search[i]['album']: return search[i] return search[0] def get_station(self, title, track_id=None, artist_id=None, album_id=None): if artist_id is not None: if album_id is not None: if track_id is not None: return self._api.create_station(title, track_id=track_id) return self._api.create_station(title, album_id=album_id) return self._api.create_station(title, artist_id=artist_id) def get_station_tracks(self, station_id): return self._api.get_station_tracks(station_id) def get_google_stream_url(self, song_id): return self._api.get_stream_url(song_id) def get_stream_url(self, song_id): return "%s/alexa/stream/%s" % (environ['APP_URL'], song_id) def get_thumbnail(self, artist_art): # return artist_art.replace("http://", "https://") //OLD artistArtKey = 'artistArtRef' albumArtKey = 'albumArtRef' if artist_art is None: return self.default_thumbnail() elif artistArtKey in artist_art: artist_art = artist_art[artistArtKey] elif albumArtKey in artist_art: artist_art = artist_art[albumArtKey] else: return self.default_thumbnail() if type(artist_art) is list: if type(artist_art[0]) is dict: artUrl = artist_art[0]['url'] elif type(artist_art) is dict: artUrl = artist_art['url'] else: artUrl = artist_art return self.urlReplaceWithSecureHttps(artUrl) def urlReplaceWithSecureHttps(self, url): return url.replace("http://", "https://") def default_thumbnail(self): return 'https://lh3.googleusercontent.com/gdBHEk-u3YRDtuCU3iDTQ52nZd1t4GPmldYaT26Jh6EhXgp1mlhQiuLFl4eXDAXzDig5' def get_all_user_playlist_contents(self): return self._api.get_all_user_playlist_contents() def get_all_songs(self): return self._api.get_all_songs() def rate_song(self, song, rating): return self._api.rate_songs(song, rating) def extract_track_info(self, track): # When coming from a playlist, track info is nested under the "track" # key if 'track' in track: track = track['track'] if 'trackId' in track: return (self.library[track['trackId']], track['trackId']) if self.use_library_first(): #Using free version track id first if 'id' in track: return (track, track['id']) if 'storeId' in track: return track, track['storeId'] return (None, None) def get_artist_album_list(self, artist_name): search = self._search("artist", artist_name) if len(search) == 0: return "Unable to find the artist you requested." artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_list_text = "Here's the album listing for %s: " % artist_name counter = 0 for index, val in enumerate(artist_info['albums']): if counter > 25: # alexa will time out after 10 seconds if the list takes too long to iterate through break album_info = self._api.get_album_info( album_id=artist_info['albums'][index]['albumId'], include_tracks=True) if len(album_info['tracks']) > 5: counter += 1 album_list_text += ( artist_info['albums'][index]['name']) + ", " return album_list_text def get_latest_artist_albums(self, artist_name): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_list = artist_info['albums'] sorted_list = sorted(album_list.__iter__(), key=lambda s: s['year'], reverse=True) speech_text = 'The latest albums by %s are ' % artist_name counter = 0 for index, val in enumerate(sorted_list): if counter > 5: break else: album_info = self._api.get_album_info( album_id=sorted_list[index]['albumId'], include_tracks=True) if len(album_info['tracks']) >= 5: counter += 1 album_name = sorted_list[index]['name'] album_year = sorted_list[index]['year'] speech_text += '%s, released in %d, ' % (album_name, album_year) return speech_text def closest_match(self, request_name, all_matches, artist_name='', minimum_score=70): # Give each match a score based on its similarity to the requested # name self.logger.debug("The artist name is " + str(artist_name)) request_name = request_name.lower() + artist_name.lower() scored_matches = [] for i, match in enumerate(all_matches): try: name = match['name'].lower() except (KeyError, TypeError): i = match name = all_matches[match]['title'].lower() if artist_name != "": name += all_matches[match]['artist'].lower() scored_matches.append({ 'index': i, 'name': name, 'score': fuzz.ratio(name, request_name) }) sorted_matches = sorted(scored_matches, key=lambda a: a['score'], reverse=True) top_scoring = sorted_matches[0] self.logger.debug("The top scoring match was: " + str(top_scoring)) best_match = all_matches[top_scoring['index']] # Make sure the score is at least the min score value if top_scoring['score'] < minimum_score: return None return best_match def get_genres(self, parent_genre_id=None): return self._api.get_genres(parent_genre_id) def increment_song_playcount(self, song_id, plays=1, playtime=None): return self._api.increment_song_playcount(song_id, plays, playtime) def get_song_data(self, song_id): return self._api.get_track_info(song_id) def use_library_first(self): return environ['USE_LIBRARY_FIRST'].lower() == 'true' @classmethod def generate_api(cls, **kwargs): return cls(environ['GOOGLE_EMAIL'], environ['GOOGLE_PASSWORD'], **kwargs)
class MusicLibrary(object): """This class reads information about your Google Play Music library""" def __init__(self, username=None, password=None, true_file_size=False, scan=True, verbose=0): self.verbose = False if verbose > 1: self.verbose = True self.__login_and_setup(username, password) self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__gartists = {} self.__albums = [] # [Album(), ...] self.__galbums = {} self.__tracks = {} self.__playlists = {} if scan: self.rescan() self.true_file_size = true_file_size def rescan(self): """Scan the Google Play Music library""" self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__gartists = {} self.__albums = [] # [Album(), ...] self.__galbums = {} self.__tracks = {} self.__playlists = {} self.__aggregate_albums() def __login_and_setup(self, username=None, password=None): # If credentials are not specified, get them from $HOME/.gmusicfs if not username or not password: cred_path = os.path.join(os.path.expanduser('~'), '.gmusicfs') if not os.path.isfile(cred_path): raise NoCredentialException( 'No username/password was specified. No config file could ' 'be found either. Try creating %s and specifying your ' 'username/password there. Make sure to chmod 600.' % cred_path) if not oct(os.stat(cred_path)[os.path.stat.ST_MODE]).endswith('00'): raise NoCredentialException( 'Config file is not protected. Please run: ' 'chmod 600 %s' % cred_path) self.config = ConfigParser.ConfigParser() self.config.read(cred_path) username = self.config.get('credentials', 'username') password = self.config.get('credentials', 'password') global deviceId deviceId = self.config.get('credentials', 'deviceId') if not username or not password: raise NoCredentialException( 'No username/password could be read from config file' ': %s' % cred_path) if not deviceId: raise NoCredentialException( 'No deviceId could be read from config file' ': %s' % cred_path) if deviceId.startswith("0x"): deviceId = deviceId[2:] self.api = GoogleMusicAPI(debug_logging=self.verbose) log.info('Logging in...') self.api.login(username, password, deviceId) log.info('Login successful.') def __set_key_from_ginfo(self, track, ginfo, key, to_key=None): """Set track key from either album_info or artist_info""" if to_key is None: to_key = key try: int_key = int(key) except ValueError: int_key = None if (not track.has_key(key) or track[key] == "" or int_key == 0) and ginfo.has_key(to_key): track[key] = ginfo[to_key] return track def __aggregate_albums(self): """Get all the tracks and playlists in the library, parse into relevant dicts""" log.info('Gathering track information...') tracks = self.api.get_all_songs() for track in tracks: log.debug('track = %s' % pp.pformat(track)) # Get album and artist information from Google if track.has_key('albumId'): if self.__galbums.has_key(track['albumId']): album_info = self.__galbums[track['albumId']] else: log.info("Downloading album info for %s '%s'", track['albumId'], track['album']) try: album_info = self.__galbums[track['albumId']] = self.api.get_album_info(track['albumId'], include_tracks=False) except gmusicapi.exceptions.CallFailure: log.exception("Failed to download album info for %s '%s'", track['albumId'], track['album']) #album_info = {} if album_info.has_key('artistId') and len(album_info['artistId']) > 0 and album_info['artistId'][0] != "": artist_id = album_info['artistId'][0] if self.__gartists.has_key(artist_id): artist_info = self.__gartists[artist_id] else: log.info("Downloading artist info for %s '%s'", artist_id, album_info['albumArtist']) #if album_info['albumArtist'] == "Various": # print album_info artist_info = self.__gartists[artist_id] = self.api.get_artist_info(artist_id, include_albums=False, max_top_tracks=0, max_rel_artist=0) else: artist_info = {} else: album_info = {} artist_info = {} track = self.__set_key_from_ginfo(track, album_info, 'album', 'name') track = self.__set_key_from_ginfo(track, album_info, 'year') track = self.__set_key_from_ginfo(track, artist_info, 'albumArtist', 'name') # Prefer the album artist over the track artist if there is one artist_name = formatNames(track['albumArtist']) if artist_name.strip() == '': artist_name = formatNames(track['artist']) if artist_name.strip() == '': artist_name = 'Unknown' # Get the Artist object, or create one if it doesn't exist artist = self.__artists.get(artist_name.lower(), None) if not artist: artist = Artist(self, artist_name) self.__artists[artist_name.lower()] = artist # Get the Album object, or create one if it doesn't exist album = artist.get_album(formatNames(track['album'])) if not album: album = Album(self, track['album']) self.__albums.append(album) # NOTE: Current no purpose other than to count artist.add_album(album) # Add track to album album.add_track(track) # Add track to list of all tracks, indexable by track ID if 'id' in track: self.__tracks[track['id']] = track log.info('%d tracks loaded.' % len(tracks)) log.info('%d artists loaded.' % len(self.__artists)) log.info('%d albums loaded.' % len(self.__albums)) # Add all playlists playlists = self.api.get_all_user_playlist_contents() for pldata in playlists: playlist = Playlist(self, pldata) self.__playlists[playlist.dirname.lower()] = playlist log.debug('%d playlists loaded.' % len(self.__playlists)) def get_artists(self): """Return all artists in the library""" return self.__artists def get_artist(self, name): """Return the artist from the library with the specified name""" return self.__artists.get(name.lower(), None) def get_playlists(self): """Return list of all playlists in the library""" return self.__playlists.values() def get_playlist(self, name): """Return the playlist from the library with the specified name""" return self.__playlists.get(name.lower(), None) def get_track(self, trackid): """Return the track from the library with the specified track ID""" return self.__tracks.get(trackid, None) def cleanup(self): pass
class Player(object): def __init__(self, device_id): self.api = Mobileclient() self.api.logger.setLevel(logging.INFO) #print(utils.log_filepath) options = ["--aout=alsa", "-I dummy", "--fullscreen"] self.vlc = Instance(options) self.player = None self.loaded_tracks = [] self.playing = False self.repeat = Repeat.none self.random = False self.song_index = 0 self.now_playing_title = "" self.now_playing_artist = "" self.now_playing_playlist = "" # 取得したjsonの生データ self.song_library = [] self.playlist_library = [] # 整頓した楽曲ライブラリ self.songs = [] self.albums = [] self.playlists = [] self.artists = [] # play musicログイン if not os.path.exists(CREDENTIAL_FILE): self.api.perform_oauth(CREDENTIAL_FILE) self.api.oauth_login(device_id, CREDENTIAL_FILE) # 曲一覧読み込み if os.path.isfile(JSON_DIR + "songs.json"): # Load from file print("Found songs data.") with open(JSON_DIR + 'songs.json') as input_file: self.song_library = json.load(input_file) else: self.song_library = self.api.get_all_songs() # Save to file with open(JSON_DIR + 'songs.json', 'w') as output_file: json.dump(self.song_library, output_file) self.create_songs() self.create_albums() self.create_artists() # プレイリスト読み込み if os.path.isfile(JSON_DIR + "playlists.json"): # Load from file print("Found playlist data.") with open(JSON_DIR + 'playlists.json') as input_file: self.playlist_library = json.load(input_file) else: self.playlist_library = self.api.get_all_user_playlist_contents() # Save to file with open(JSON_DIR + 'playlists.json', 'w') as output_file: json.dump(self.playlist_library, output_file) #プレイリスト名編集 self.create_playlists() # 定時ライブラリ更新処理 t = threading.Timer(RELOAD_LIB_TIME, self.auto_reload) t.start() def auto_reload(self): while True: if not self.playing: break time.sleep(60) self.reload_library() print("[ music list auto reloaded ]") t = threading.Timer(RELOAD_LIB_TIME, self.auto_reload) t.start() def reload_library(self): # 曲一覧読み込み self.song_library = self.api.get_all_songs() # Save to file with open(JSON_DIR + 'songs.json', 'w') as output_file: json.dump(self.song_library, output_file) self.create_songs() self.create_albums() self.create_artists() # プレイリスト読み込み self.playlist_library = self.api.get_all_user_playlist_contents() # Save to file with open(JSON_DIR + 'playlists.json', 'w') as output_file: json.dump(self.playlist_library, output_file) #プレイリスト名編集 self.create_playlists() def create_songs(self): self.songs = [] # 曲名編集 for index, song in enumerate(self.song_library): self.songs.append({}) self.songs[index].update({"original_name": song['title']}) self.songs[index].update( {"name": cir.convert_into_romaji(song['title'])}) self.songs[index].update({"artist": song['artist']}) self.songs[index].update({"trackId": song['id']}) self.songs[index].update({"source": 1}) #print(self.songs[index]) #sleep(0.1) print("[ create_songs finished ]") def create_playlists(self): self.playlists = [] #プレイリスト名編集 for index, playlist in enumerate(self.playlist_library): self.playlists.append({}) self.playlists[index].update({"original_name": playlist['name']}) self.playlists[index].update( {"name": cir.convert_into_romaji(playlist['name'])}) self.playlists[index].update({"tracks": playlist['tracks']}) print(self.playlists[index]['name']) print("[ create_playlists finished ]") def create_albums(self): self.albums = [] # アルバムリスト作成 for song in self.song_library: album_found = False track = {} for index, album in enumerate(self.albums): # アルバムがすでに登録されていた場合 if album['original_name'] == song['album']: album_found = True track.update({"trackId": song['id']}) track.update({"source": 1}) track.update({"trackNumber": song['trackNumber']}) self.albums[index]['tracks'].append(track) #print(self.albums[index]) break if album_found: continue #新規アルバム作成 albums_len = len(self.albums) self.albums.append({}) self.albums[albums_len].update({"original_name": song['album']}) self.albums[albums_len].update( {"name": cir.convert_into_romaji(song['album'])}) track.update({"trackId": song['id']}) track.update({"source": 1}) track.update({"trackNumber": song['trackNumber']}) self.albums[albums_len].update({"tracks": [track]}) #print(self.albums[albums_len]) # tracknumberでソート for album in self.albums: album['tracks'] = sorted(album['tracks'], key=lambda x: x['trackNumber']) print(album["name"]) print("[ create_albums finished ]") def create_artists(self): self.artists = [] # アーティストリスト作成 for song in self.song_library: artist_found = False track = {} for index, artist in enumerate(self.artists): # アーティストがすでに登録されていた場合 if artist['original_name'] == song['artist']: artist_found = True track.update({"trackId": song['id']}) track.update({"source": 1}) track.update({"trackNumber": song['trackNumber']}) self.artists[index]['tracks'].append(track) break if artist_found: continue #新規アルバム作成 artists_len = len(self.artists) self.artists.append({}) self.artists[artists_len].update({"original_name": song['artist']}) self.artists[artists_len].update( {"name": cir.convert_into_romaji(song['artist'])}) track.update({"trackId": song['id']}) track.update({"source": 1}) track.update({"trackNumber": song['trackNumber']}) self.artists[artists_len].update({"tracks": [track]}) print(self.artists[artists_len]["name"]) print("[ create_artists finished ]") def load_playlist(self, name): name = name.strip().lower() print("Looking for...", name) top_diff = 0.0 top_playlist = {} # 検索 for playlist_dict in self.playlists: playlist_name = playlist_dict['name'].strip().lower() diff = difflib.SequenceMatcher(None, playlist_name, name).ratio() if diff > top_diff: print("diff match...", playlist_dict['name'], ":", diff) top_playlist = playlist_dict top_diff = diff else: pass #print("Found...", playlist_dict['name']) # 一番マッチしたものを返す if top_diff > DIFF_ARGS: self.loaded_tracks = [] print(top_diff) print("Found match...", top_playlist['name']) for track_dict in top_playlist['tracks']: self.loaded_tracks.append(track_dict) self.now_playing_playlist = top_playlist['original_name'] return top_playlist['original_name'] else: return None def load_song(self, name): name = name.strip().lower() print("Looking for...", name) top_diff = 0.0 top_song = {} for song_dict in self.songs: song_name = song_dict['name'].strip().lower() diff = difflib.SequenceMatcher(None, song_name, name).ratio() #print(diff) if diff > top_diff: print("diff match...", song_dict['name'], ":", diff) top_song = song_dict top_diff = diff else: pass #print("Found...", song_dict['name']) # 一番マッチしたものを返す if top_diff > DIFF_ARGS: self.loaded_tracks = [] print(top_diff) print("Found match...", top_song['name']) self.loaded_tracks.append(top_song) self.now_playing_playlist = "" return top_song['original_name'] else: return None def load_album(self, name): name = name.strip().lower() print("Looking for...", name) top_diff = 0.0 top_album = {} for album_dict in self.albums: album_name = album_dict['name'].strip().lower() diff = difflib.SequenceMatcher(None, album_name, name).ratio() #print(diff) if diff > top_diff: print("diff match...", album_dict['name'], ":", diff) top_album = album_dict top_diff = diff else: pass #print("Found...", album_dict['name']) # 一番マッチしたものを返す if top_diff > DIFF_ARGS: self.loaded_tracks = [] print(top_diff) print("Found match...", top_album['name']) for track_dict in top_album['tracks']: self.loaded_tracks.append(track_dict) self.now_playing_playlist = top_album['original_name'] return top_album['original_name'] else: return None def load_artist(self, name): name = name.strip().lower() print("Looking for...", name) top_diff = 0.0 top_artist = {} for artist_dict in self.artists: artist_name = artist_dict['name'].strip().lower() diff = difflib.SequenceMatcher(None, artist_name, name).ratio() #print(diff) if diff > top_diff: print("diff match...", artist_dict['name'], ":", diff) top_artist = artist_dict top_diff = diff else: pass # 一番マッチしたものを返す if top_diff > DIFF_ARGS: self.loaded_tracks = [] print(top_diff) print("Found match...", top_artist['name']) for track_dict in top_artist['tracks']: self.loaded_tracks.append(track_dict) self.now_playing_playlist = top_artist['original_name'] return top_artist['original_name'] else: return None def load_cloud(self, name, isArtist=True, isSong=True, isAlbum=True): search = self.api.search(name) # アーティストのトップ曲を流す if search["artist_hits"] and isArtist: for index in range(len(search["artist_hits"])): artist_id = search["artist_hits"][index]["artist"]["artistId"] artist = self.api.get_artist_info(artist_id, max_top_tracks=MAX_TRACK, include_albums=False, max_rel_artist=0) if "topTracks" in artist.keys(): break if "topTracks" in artist.keys(): self.loaded_tracks = [] for track_dict in artist["topTracks"]: track_dict.update({"track": track_dict}) track_dict.update({"trackId": track_dict["storeId"]}) track_dict.update({"source": "2"}) self.loaded_tracks.append(track_dict) self.now_playing_playlist = "" return artist["name"] # 単曲を流す(複数にしたほうがいいかも) elif search["song_hits"] and isSong: self.loaded_tracks = [] for index, track_dict in enumerate(search["song_hits"]): if index >= MAX_TRACK: break track_dict.update({"trackId": track_dict["track"]["storeId"]}) track_dict.update({"source": "2"}) self.loaded_tracks.append(track_dict) self.now_playing_playlist = "" return self.loaded_tracks[0]["track"]["title"] # アルバムを流す(正確さに欠ける) elif search["album_hits"] and isAlbum: album_id = search["album_hits"][0]["album"]["albumId"] album = self.api.get_album_info(album_id) self.loaded_tracks = [] for track_dict in album["tracks"]: track_dict.update({"track": track_dict}) track_dict.update({"trackId": track_dict["storeId"]}) track_dict.update({"source": "2"}) self.loaded_tracks.append(track_dict) self.now_playing_playlist = album["name"] return album["name"] # ステーション(ここまで回ってこない気が・・・) elif search["station_hits"]: pass return None def end_callback(self, event, track_index): # ランダム再生時処理 if self.random: self.song_index = random.randint(0, len(self.loaded_tracks) - 1) self.play_song(self.loaded_tracks[self.song_index]) event_manager = self.player.event_manager() event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, self.song_index + 1) return # 一曲リピート if self.repeat == Repeat.song: self.play_song(self.loaded_tracks[track_index - 1]) event_manager = self.player.event_manager() event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, track_index) return # 通常再生・プレイリストリピート if track_index < len(self.loaded_tracks): self.song_index = track_index self.play_song(self.loaded_tracks[track_index]) event_manager = self.player.event_manager() event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, track_index + 1) else: if self.repeat == Repeat.playlist: self.start_playlist() else: self.playing = False self.song_index = 0 def start_playlist(self): if len(self.loaded_tracks) > 0: # ランダム再生時処理 if self.random: self.song_index = random.randint(0, len(self.loaded_tracks) - 1) else: # 通常再生 self.song_index = 0 self.play_song(self.loaded_tracks[self.song_index]) event_manager = self.player.event_manager() event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, self.song_index + 1) return True return False def play_song(self, song_dict): stream_url = self.api.get_stream_url(song_dict['trackId']) self.player = self.vlc.media_player_new() media = self.vlc.media_new(stream_url) self.player.set_media(media) self.player.play() self.playing = True if (song_dict['source'] == '2'): self.now_playing_artist, self.now_playing_title = self.get_song_details( song_dict) else: self.now_playing_artist, self.now_playing_title = self.get_local_song_details( song_dict['trackId']) print("Playing...", self.now_playing_artist, " - ", self.now_playing_title) def stop(self): if self.player != None: self.player.stop() self.player = None self.playing = False self.repeat = Repeat.none self.random = False self.song_index = 0 self.now_playing_title = "" self.now_playing_artist = "" def pause(self): if self.player == None: return False if self.playing == True: self.player.set_pause(1) self.playing = False return True return False def resume(self): if self.player == None: return False if self.playing == False: self.player.set_pause(0) self.playing = True return True return False def next(self): if self.player == None: return False # ランダム if self.random: self.song_index = random.randint(0, len(self.loaded_tracks) - 1) self.player.stop() self.play_song(self.loaded_tracks[self.song_index]) event_manager = self.player.event_manager() event_manager.event_detach(EventType.MediaPlayerEndReached) event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, self.song_index + 1) return True # 通常 if self.song_index + 1 < len(self.loaded_tracks): self.song_index += 1 self.player.stop() self.play_song(self.loaded_tracks[self.song_index]) event_manager = self.player.event_manager() event_manager.event_detach(EventType.MediaPlayerEndReached) event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, self.song_index + 1) return True else: if self.repeat == Repeat.playlist: self.start_playlist() return True return False def prev(self): if self.player == None: return False if self.song_index - 1 <= 0: self.song_index -= 1 self.player.stop() self.play_song(self.loaded_tracks[self.song_index]) event_manager = self.player.event_manager() event_manager.event_detach(EventType.MediaPlayerEndReached) event_manager.event_attach(EventType.MediaPlayerEndReached, self.end_callback, self.song_index + 1) return True return False def get_local_song_details(self, track_id): for song_dict in self.song_library: if track_id == song_dict['id']: return song_dict['artist'], song_dict['title'] def get_song_details(self, song_dict): return song_dict['track']['albumArtist'], song_dict['track']['title'] def set_volume(self, volume): if self.player: self.player.audio_set_volume(volume)
class GMusicSession(object): def __init__(self): super(GMusicSession, self).__init__() logger.info('Mopidy uses Google Music') self.api = Mobileclient() def login(self, username, password, deviceid): if self.api.is_authenticated(): self.api.logout() try: self.api.login(username, password) except CallFailure as error: logger.error(u'Failed to login as "%s": %s', username, error) if self.api.is_authenticated(): if deviceid is None: self.deviceid = self.get_deviceid(username, password) else: self.deviceid = deviceid else: return False def logout(self): if self.api.is_authenticated(): return self.api.logout() else: return True def get_all_songs(self): if self.api.is_authenticated(): return self.api.get_all_songs() else: return {} def get_stream_url(self, song_id): if self.api.is_authenticated(): try: return self.api.get_stream_url(song_id, self.deviceid) except CallFailure as error: logger.error(u'Failed to lookup "%s": %s', song_id, error) def get_all_playlist_contents(self): if self.api.is_authenticated(): return self.api.get_all_user_playlist_contents() else: return {} def get_shared_playlist_contents(self, shareToken): if self.api.is_authenticated(): return self.api.get_shared_playlist_contents(shareToken) else: return {} def get_all_playlists(self): if self.api.is_authenticated(): return self.api.get_all_playlists() else: return {} def get_deviceid(self, username, password): logger.warning(u'No mobile device ID configured. ' u'Trying to detect one.') webapi = Webclient(validate=False) webapi.login(username, password) devices = webapi.get_registered_devices() deviceid = None for device in devices: if device['type'] == 'PHONE' and device['id'][0:2] == u'0x': # Omit the '0x' prefix deviceid = device['id'][2:] break webapi.logout() if deviceid is None: logger.error(u'No valid mobile device ID found. ' u'Playing songs will not work.') else: logger.info(u'Using mobile device ID %s', deviceid) return deviceid def get_track_info(self, store_track_id): if self.api.is_authenticated(): try: return self.api.get_track_info(store_track_id) except CallFailure as error: logger.error(u'Failed to get All Access track info: %s', error) def get_album_info(self, albumid, include_tracks=True): if self.api.is_authenticated(): try: return self.api.get_album_info(albumid, include_tracks) except CallFailure as error: logger.error(u'Failed to get All Access album info: %s', error)
class GMusicWrapper(object): def __init__(self, username, password, logger=None): self._api = Mobileclient() self.logger = logger success = self._api.login( username, password, getenv('ANDROID_ID', Mobileclient.FROM_MAC_ADDRESS)) if not success: raise Exception("Unsuccessful login. Aborting!") # Populate our library self.library = {} self.indexing_thread = threading.Thread(target=self.index_library) self.indexing_thread.start() def log(self, log_str): self.logger.debug(log_str) def _search(self, query_type, query): try: results = self._api.search(query) except CallFailure: return [] hits_key = "%s_hits" % query_type if hits_key not in results: return [] # Ugh, Google had to make this schema nonstandard... if query_type == 'song': query_type = 'track' return [x[query_type] for x in results[hits_key]] def is_indexing(self): return self.indexing_thread.is_alive() def index_library(self): """ Downloads the a list of every track in a user's library and populates self.library with storeIds -> track definitions """ self.log('Fetching library...') tracks = self.get_all_songs() for track in tracks: song_id = track['id'] self.library[song_id] = track self.log('Fetching library...') def get_artist(self, name): """ Fetches information about an artist given its name """ search = self._search("artist", name) if len(search) == 0: return False return self._api.get_artist_info(search[0]['artistId'], max_top_tracks=100) def get_album(self, name, artist_name=None): if artist_name: name = "%s %s" % (name, artist_name) search = self._search("album", name) if len(search) == 0: return False return self._api.get_album_info(search[0]['albumId']) def get_latest_album(self, artist_name=None): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_info = artist_info['albums'] sorted_list = sorted(album_info.__iter__(), key=lambda s: s['year'], reverse=True) for index, val in enumerate(sorted_list): album_info = self._api.get_album_info( album_id=sorted_list[index]['albumId'], include_tracks=True) if len(album_info['tracks']) >= 5: return album_info return False def get_album_by_artist(self, artist_name, album_id=None): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_info = artist_info['albums'] random.shuffle(album_info) for index, val in enumerate(album_info): album = self._api.get_album_info( album_id=album_info[index]['albumId'], include_tracks=True) if album['albumId'] != album_id and len(album['tracks']) >= 5: return album return False def get_song(self, name, artist_name=None, album_name=None): if artist_name: name = "%s %s" % (artist_name, name) elif album_name: name = "%s %s" % (album_name, name) search = self._search("song", name) if len(search) == 0: return False if album_name: for i in range(0, len(search) - 1): if album_name in search[i]['album']: return search[i] return search[0] def get_promoted_songs(self): return self._api.get_promoted_songs() def get_station(self, title, track_id=None, artist_id=None, album_id=None): if artist_id is not None: if album_id is not None: if track_id is not None: return self._api.create_station(title, track_id=track_id) return self._api.create_station(title, album_id=album_id) return self._api.create_station(title, artist_id=artist_id) def get_station_tracks(self, station_id): return self._api.get_station_tracks(station_id) def get_google_stream_url(self, song_id): return self._api.get_stream_url(song_id) def get_stream_url(self, song_id): return "%s/alexa/stream/%s" % (getenv('APP_URL'), song_id) def get_thumbnail(self, artist_art): return artist_art.replace("http://", "https://") def get_all_user_playlist_contents(self): return self._api.get_all_user_playlist_contents() def get_all_songs(self): return self._api.get_all_songs() def rate_song(self, song, rating): return self._api.rate_songs(song, rating) def extract_track_info(self, track): # When coming from a playlist, track info is nested under the "track" # key if 'track' in track: track = track['track'] if 'storeId' in track: return track, track['storeId'] elif 'trackId' in track: return self.library[track['trackId']], track['trackId'] else: return None, None def get_artist_album_list(self, artist_name): search = self._search("artist", artist_name) if len(search) == 0: return "Unable to find the artist you requested." artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_list_text = "Here's the album listing for %s: " % artist_name counter = 0 for index, val in enumerate(artist_info['albums']): if counter > 25: # alexa will time out after 10 seconds if the list takes too long to iterate through break album_info = self._api.get_album_info( album_id=artist_info['albums'][index]['albumId'], include_tracks=True) if len(album_info['tracks']) > 5: counter += 1 album_list_text += ( artist_info['albums'][index]['name']) + ", " return album_list_text def get_latest_artist_albums(self, artist_name): search = self._search("artist", artist_name) if len(search) == 0: return False artist_info = self._api.get_artist_info(search[0]['artistId'], include_albums=True) album_list = artist_info['albums'] sorted_list = sorted(album_list.__iter__(), key=lambda s: s['year'], reverse=True) speech_text = 'The latest albums by %s are ' % artist_name counter = 0 for index, val in enumerate(sorted_list): if counter > 5: break else: album_info = self._api.get_album_info( album_id=sorted_list[index]['albumId'], include_tracks=True) if len(album_info['tracks']) >= 5: counter += 1 album_name = sorted_list[index]['name'] album_year = sorted_list[index]['year'] speech_text += '%s, released in %d, ' % (album_name, album_year) return speech_text def closest_match(self, request_name, all_matches, artist_name='', minimum_score=70): # Give each match a score based on its similarity to the requested # name self.log('Fetching library...') request_name = request_name.lower() + artist_name.lower() scored_matches = [] for i, match in enumerate(all_matches): try: name = match['name'].lower() except (KeyError, TypeError): i = match name = all_matches[match]['title'].lower() if artist_name != "": name += all_matches[match]['artist'].lower() scored_matches.append({ 'index': i, 'name': name, 'score': fuzz.ratio(name, request_name) }) sorted_matches = sorted(scored_matches, key=lambda a: a['score'], reverse=True) top_scoring = sorted_matches[0] self.log('Fetching library...') best_match = all_matches[top_scoring['index']] # Make sure we have a decent match (the score is n where 0 <= n <= 100) if top_scoring['score'] < minimum_score: return None return best_match def get_genres(self, parent_genre_id=None): return self._api.get_genres(parent_genre_id) def increment_song_playcount(self, song_id, plays=1, playtime=None): return self._api.increment_song_playcount(song_id, plays, playtime) def get_song_data(self, song_id): return self._api.get_track_info(song_id) @classmethod def generate_api(cls, **kwargs): return cls(getenv('GOOGLE_EMAIL'), getenv('GOOGLE_PASSWORD'), **kwargs)
class GoogleMusicClient(object): def __init__(self, username, password): self._username = username self._password = password self._apiClient = None def initConnection(self): self._apiClient = Mobileclient(debug_logging=False) if not self._apiClient.login(self._username, self._password): raise RuntimeError("Could not connect %s to Google Music." % \ self._username) def addPlaylist(self, playlist): plid = self._apiClient.create_playlist(playlist.name) tids = [] for track in playlist.tracks: aaid = self._getAllAccessTrackId(track) if aaid: try: tid = self._apiClient.add_aa_track(aaid) if tid: tids.append(tid) else: LOGGER.warning( "Could not add track %s to library.", str(track)) except: LOGGER.error("Could not add track %s to library.", str(track)) continue else: LOGGER.warning("Track %s not found.", str(track)) self._apiClient.add_songs_to_playlist(plid, tids) def addAlbum(self, album): aaid = self._getAllAccessAlbumId(album) if aaid: albumInfo = self._apiClient.get_album_info(aaid, include_tracks=True) for track in albumInfo["tracks"]: try: self._apiClient.add_aa_track(track[Track.GM_ID_KEY]) except: LOGGER.error("Could not add track %s to library.", str(track)) continue else: LOGGER.warning("Album %s not found.", str(album)) def _getAllAccessFirstResult(self, resource): queryStr = re.sub("[-:\(\)\",]","", "%s %s" % (resource.name, resource.artist)) queryStr = re.sub("\s+", "+", queryStr) searchResults = self._apiClient.search_all_access(queryStr) gmusicResources = searchResults.get(resource.GM_HITS_KEY) if gmusicResources: firstResult = gmusicResources[0][resource.GM_NAME] return firstResult[resource.GM_ID_KEY] else: return None def _getAllAccessTrackId(self, track): return self._getAllAccessFirstResult(track) def _getAllAccessAlbumId(self, album): return self._getAllAccessFirstResult(album)