Example #1
0
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)
Example #2
0
log.debug('Migration type is ' + migration_type)
log.info('Exporting data from ' + export_username)

if migration_type != 'stations':
    log.info('Retrieving tracks from ' + export_username)
    export_tracks = export_api.get_all_songs()
    # strip out any tracks that are not available on All Access
    all_tracks = [
        t for t in export_tracks
        if track_has_aa_data(t) and not t.get('deleted')
    ]

if migration_type == 'all' or migration_type == 'ratings':
    log.info('Retrieving thumbs up/promoted tracks from ' + export_username)
    export_thumbs_up = export_api.get_promoted_songs()
    # strip out any tracks that are not available on All Access
    thumbs_up_tracks = [t for t in export_thumbs_up if track_has_aa_data(t)]

if migration_type == 'all' or migration_type == 'playlists':
    log.info('Retrieving playlists from ' + export_username)
    export_playlists = export_api.get_all_user_playlist_contents()
    playlists = [p for p in export_playlists if not p.get('deleted')]

if migration_type == 'all' or migration_type == 'stations':
    log.info('Retrieving stations from ' + export_username)
    export_stations = export_api.get_all_stations()
    radio_stations = [s for s in export_stations if not s.get('deleted')]

log.info('Export complete')
export_api.logout()
Example #3
0
class tizgmusicproxy(object):
    """A class for logging into a Google Play Music account and retrieving song
    URLs.

    """

    all_songs_album_title = "All Songs"
    thumbs_up_playlist_name = "Thumbs Up"

    def __init__(self, email, password, device_id):
        self.__gmusic = Mobileclient()
        self.__email = email
        self.__device_id = device_id
        self.logged_in = False
        self.queue = list()
        self.queue_index = -1
        self.play_queue_order = list()
        self.play_modes = TizEnumeration(["NORMAL", "SHUFFLE"])
        self.current_play_mode = self.play_modes.NORMAL
        self.now_playing_song = None

        userdir = os.path.expanduser('~')
        tizconfig = os.path.join(userdir, ".config/tizonia/." + email + ".auth_token")
        auth_token = ""
        if os.path.isfile(tizconfig):
            with open(tizconfig, "r") as f:
                auth_token = pickle.load(f)
                if auth_token:
                    # 'Keep track of the auth token' workaround. See:
                    # https://github.com/diraimondo/gmusicproxy/issues/34#issuecomment-147359198
                    print_msg("[Google Play Music] [Authenticating] : " \
                              "'with cached auth token'")
                    self.__gmusic.android_id = device_id
                    self.__gmusic.session._authtoken = auth_token
                    self.__gmusic.session.is_authenticated = True
                    try:
                        self.__gmusic.get_registered_devices()
                    except CallFailure:
                        # The token has expired. Reset the client object
                        print_wrn("[Google Play Music] [Authenticating] : " \
                                  "'auth token expired'")
                        self.__gmusic = Mobileclient()
                        auth_token = ""

        if not auth_token:
            attempts = 0
            print_nfo("[Google Play Music] [Authenticating] : " \
                      "'with user credentials'")
            while not self.logged_in and attempts < 3:
                self.logged_in = self.__gmusic.login(email, password, device_id)
                attempts += 1

            with open(tizconfig, "a+") as f:
                f.truncate()
                pickle.dump(self.__gmusic.session._authtoken, f)

        self.library = CaseInsensitiveDict()
        self.song_map = CaseInsensitiveDict()
        self.playlists = CaseInsensitiveDict()
        self.stations = CaseInsensitiveDict()

    def logout(self):
        """ Reset the session to an unauthenticated, default state.

        """
        self.__gmusic.logout()

    def set_play_mode(self, mode):
        """ Set the playback mode.

        :param mode: curren tvalid values are "NORMAL" and "SHUFFLE"

        """
        self.current_play_mode = getattr(self.play_modes, mode)
        self.__update_play_queue_order()

    def current_song_title_and_artist(self):
        """ Retrieve the current track's title and artist name.

        """
        logging.info("current_song_title_and_artist")
        song = self.now_playing_song
        if song:
            title = to_ascii(self.now_playing_song.get('title'))
            artist = to_ascii(self.now_playing_song.get('artist'))
            logging.info("Now playing %s by %s", title, artist)
            return artist, title
        else:
            return '', ''

    def current_song_album_and_duration(self):
        """ Retrieve the current track's album and duration.

        """
        logging.info("current_song_album_and_duration")
        song = self.now_playing_song
        if song:
            album = to_ascii(self.now_playing_song.get('album'))
            duration = to_ascii \
                       (self.now_playing_song.get('durationMillis'))
            logging.info("album %s duration %s", album, duration)
            return album, int(duration)
        else:
            return '', 0

    def current_track_and_album_total(self):
        """Return the current track number and the total number of tracks in the
        album, if known.

        """
        logging.info("current_track_and_album_total")
        song = self.now_playing_song
        track = 0
        total = 0
        if song:
            try:
                track = self.now_playing_song['trackNumber']
                total = self.now_playing_song['totalTrackCount']
                logging.info("track number %s total tracks %s", track, total)
            except KeyError:
                logging.info("trackNumber or totalTrackCount : not found")
        else:
            logging.info("current_song_track_number_"
                         "and_total_tracks : not found")
        return track, total

    def current_song_year(self):
        """ Return the current track's year of publication.

        """
        logging.info("current_song_year")
        song = self.now_playing_song
        year = 0
        if song:
            try:
                year = song['year']
                logging.info("track year %s", year)
            except KeyError:
                logging.info("year : not found")
        else:
            logging.info("current_song_year : not found")
        return year

    def clear_queue(self):
        """ Clears the playback queue.

        """
        self.queue = list()
        self.queue_index = -1

    def enqueue_artist(self, arg):
        """ Search the user's library for tracks from the given artist and adds
        them to the playback queue.

        :param arg: an artist
        """
        try:
            self.__update_local_library()
            artist = None
            if arg not in self.library.keys():
                for name, art in self.library.iteritems():
                    if arg.lower() in name.lower():
                        artist = art
                        print_wrn("[Google Play Music] '{0}' not found. " \
                                  "Playing '{1}' instead." \
                                  .format(arg.encode('utf-8'), \
                                          name.encode('utf-8')))
                        break
                if not artist:
                    # Play some random artist from the library
                    random.seed()
                    artist = random.choice(self.library.keys())
                    artist = self.library[artist]
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))
            else:
                artist = self.library[arg]
            tracks_added = 0
            for album in artist:
                tracks_added += self.__enqueue_tracks(artist[album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(artist)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))

    def enqueue_album(self, arg):
        """ Search the user's library for albums with a given name and adds
        them to the playback queue.

        """
        try:
            self.__update_local_library()
            album = None
            artist = None
            tentative_album = None
            tentative_artist = None
            for library_artist in self.library:
                for artist_album in self.library[library_artist]:
                    print_nfo("[Google Play Music] [Album] '{0}'." \
                              .format(to_ascii(artist_album)))
                    if not album:
                        if arg.lower() == artist_album.lower():
                            album = artist_album
                            artist = library_artist
                            break
                    if not tentative_album:
                        if arg.lower() in artist_album.lower():
                            tentative_album = artist_album
                            tentative_artist = library_artist
                if album:
                    break

            if not album and tentative_album:
                album = tentative_album
                artist = tentative_artist
                print_wrn("[Google Play Music] '{0}' not found. " \
                          "Playing '{1}' instead." \
                          .format(arg.encode('utf-8'), \
                          album.encode('utf-8')))
            if not album:
                # Play some random album from the library
                random.seed()
                artist = random.choice(self.library.keys())
                album = random.choice(self.library[artist].keys())
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            if not album:
                raise KeyError("Album not found : {0}".format(arg))

            self.__enqueue_tracks(self.library[artist][album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(album)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))

    def enqueue_playlist(self, arg):
        """Search the user's library for playlists with a given name
        and adds the tracks of the first match to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            self.__update_local_library()
            self.__update_playlists()
            self.__update_playlists_unlimited()
            playlist = None
            playlist_name = None
            for name, plist in self.playlists.items():
                print_nfo("[Google Play Music] [Playlist] '{0}'." \
                          .format(to_ascii(name)))
            if arg not in self.playlists.keys():
                for name, plist in self.playlists.iteritems():
                    if arg.lower() in name.lower():
                        playlist = plist
                        playlist_name = name
                        print_wrn("[Google Play Music] '{0}' not found. " \
                                  "Playing '{1}' instead." \
                                  .format(arg.encode('utf-8'), \
                                          to_ascii(name)))
                        break
                if not playlist:
                    # Play some random playlist from the library
                    random.seed()
                    playlist_name = random.choice(self.playlists.keys())
                    playlist = self.playlists[playlist_name]
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))
            else:
                playlist_name = arg
                playlist = self.playlists[arg]

            self.__enqueue_tracks(playlist)
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(playlist_name)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))

    def enqueue_station_unlimited(self, arg):
        """Search the user's library for a station with a given name
        and add its tracks to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            # First try to find a suitable station in the user's library
            self.__enqueue_user_station_unlimited(arg)

            if not len(self.queue):
                # If no suitable station is found in the user's library, then
                # search google play unlimited for a potential match.
                self.__enqueue_station_unlimited(arg)

            if not len(self.queue):
                raise KeyError

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def enqueue_genre_unlimited(self, arg):
        """Search Unlimited for a genre with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving genres] : '{0}'. " \
                  .format(self.__email))

        try:
            all_genres = list()
            root_genres = self.__gmusic.get_genres()
            second_tier_genres = list()
            for root_genre in root_genres:
                second_tier_genres += self.__gmusic.get_genres(root_genre['id'])
            all_genres += root_genres
            all_genres += second_tier_genres
            for genre in all_genres:
                print_nfo("[Google Play Music] [Genre] '{0}'." \
                          .format(to_ascii(genre['name'])))
            genre = dict()
            if arg not in all_genres:
                genre = next((g for g in all_genres \
                              if arg.lower() in to_ascii(g['name']).lower()), \
                             None)

            tracks_added = 0
            while not tracks_added:
                if not genre and len(all_genres):
                    # Play some random genre from the search results
                    random.seed()
                    genre = random.choice(all_genres)
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))

                genre_name = genre['name']
                genre_id = genre['id']
                station_id = self.__gmusic.create_station(genre_name, \
                                                          None, None, None, genre_id)
                num_tracks = 200
                tracks = self.__gmusic.get_station_tracks(station_id, num_tracks)
                tracks_added = self.__enqueue_tracks(tracks)
                logging.info("Added %d tracks from %s to queue", tracks_added, genre_name)
                if not tracks_added:
                    # This will produce another iteration in the loop
                    genre = None

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(genre['name'])))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Genre not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_situation_unlimited(self, arg):
        """Search Unlimited for a situation with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving situations] : '{0}'. " \
                  .format(self.__email))

        try:

            self.__enqueue_situation_unlimited(arg)

            if not len(self.queue):
                raise KeyError

            logging.info("Added %d tracks from %s to queue", \
                         len(self.queue), arg)

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_artist_unlimited(self, arg):
        """Search Unlimited for an artist and adds the artist's 200 top tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            artist = self.__gmusic_search(arg, 'artist')

            include_albums = False
            max_top_tracks = 200
            max_rel_artist = 0
            artist_tracks = dict()
            if artist:
                artist_tracks = self.__gmusic.get_artist_info \
                                (artist['artist']['artistId'],
                                 include_albums, max_top_tracks,
                                 max_rel_artist)['topTracks']
            if not artist_tracks:
                raise KeyError

            tracks_added = self.__enqueue_tracks(artist_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_album_unlimited(self, arg):
        """Search Unlimited for an album and add its tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            album = self.__gmusic_search(arg, 'album')
            album_tracks = dict()
            if album:
                album_tracks = self.__gmusic.get_album_info \
                               (album['album']['albumId'])['tracks']
            if not album_tracks:
                raise KeyError

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format((album['album']['name']).encode('utf-8')))

            tracks_added = self.__enqueue_tracks(album_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_tracks_unlimited(self, arg):
        """ Search Unlimited for a track name and adds all the matching tracks
        to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        try:
            max_results = 200
            track_hits = self.__gmusic.search(arg, max_results)['song_hits']
            if not len(track_hits):
                # Do another search with an empty string
                track_hits = self.__gmusic.search("", max_results)['song_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            tracks = list()
            for hit in track_hits:
                tracks.append(hit['track'])
            tracks_added = self.__enqueue_tracks(tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_promoted_tracks_unlimited(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        try:
            tracks = self.__gmusic.get_promoted_songs()
            count = 0
            for track in tracks:
                store_track = self.__gmusic.get_track_info(track['storeId'])
                if u'id' not in store_track.keys():
                    store_track[u'id'] = store_track['nid']
                self.queue.append(store_track)
                count += 1
            if count == 0:
                print_wrn("[Google Play Music] Operation requires " \
                          "an Unlimited subscription.")
            logging.info("Added %d Unlimited promoted tracks to queue", \
                         count)
            self.__update_play_queue_order()
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def next_url(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        if len(self.queue):
            self.queue_index += 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                next_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(next_song)
            else:
                self.queue_index = -1
                return self.next_url()
        else:
            return ''

    def prev_url(self):
        """ Retrieve the url of the previous track in the playback queue.

        """
        if len(self.queue):
            self.queue_index -= 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                prev_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(prev_song)
            else:
                self.queue_index = len(self.queue)
                return self.prev_url()
        else:
            return ''

    def __update_play_queue_order(self):
        """ Update the queue playback order.

        A sequential order is applied if the current play mode is "NORMAL" or a
        random order if current play mode is "SHUFFLE"

        """
        total_tracks = len(self.queue)
        if total_tracks:
            if not len(self.play_queue_order):
                # Create a sequential play order, if empty
                self.play_queue_order = range(total_tracks)
            if self.current_play_mode == self.play_modes.SHUFFLE:
                random.shuffle(self.play_queue_order)
            print_nfo("[Google Play Music] [Tracks in queue] '{0}'." \
                      .format(total_tracks))

    def __retrieve_track_url(self, song):
        """ Retrieve a song url

        """
        song_url = self.__gmusic.get_stream_url(song['id'], self.__device_id)
        try:
            self.now_playing_song = song
            return song_url
        except AttributeError:
            logging.info("Could not retrieve the song url!")
            raise

    def __update_local_library(self):
        """ Retrieve the songs and albums from the user's library

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        songs = self.__gmusic.get_all_songs()
        self.playlists[self.thumbs_up_playlist_name] = list()

        # Retrieve the user's song library
        for song in songs:
            if "rating" in song and song['rating'] == "5":
                self.playlists[self.thumbs_up_playlist_name].append(song)

            song_id = song['id']
            song_artist = song['artist']
            song_album = song['album']

            self.song_map[song_id] = song

            if song_artist == "":
                song_artist = "Unknown Artist"

            if song_album == "":
                song_album = "Unknown Album"

            if song_artist not in self.library:
                self.library[song_artist] = CaseInsensitiveDict()
                self.library[song_artist][self.all_songs_album_title] = list()

            if song_album not in self.library[song_artist]:
                self.library[song_artist][song_album] = list()

            self.library[song_artist][song_album].append(song)
            self.library[song_artist][self.all_songs_album_title].append(song)

        # Sort albums by track number
        for artist in self.library.keys():
            logging.info("Artist : %s", to_ascii(artist))
            for album in self.library[artist].keys():
                logging.info("   Album : %s", to_ascii(album))
                if album == self.all_songs_album_title:
                    sorted_album = sorted(self.library[artist][album],
                                          key=lambda k: k['title'])
                else:
                    sorted_album = sorted(self.library[artist][album],
                                          key=lambda k: k.get('trackNumber',
                                                              0))
                self.library[artist][album] = sorted_album

    def __update_stations_unlimited(self):
        """ Retrieve stations (Unlimited)

        """
        self.stations.clear()
        stations = self.__gmusic.get_all_stations()
        self.stations[u"I'm Feeling Lucky"] = 'IFL'
        for station in stations:
            station_name = station['name']
            logging.info("station name : %s", to_ascii(station_name))
            self.stations[station_name] = station['id']

    def __enqueue_user_station_unlimited(self, arg):
        """ Enqueue a user station (Unlimited)

        """
        print_msg("[Google Play Music] [Station search "\
                  "in user's library] : '{0}'. " \
                  .format(self.__email))
        self.__update_stations_unlimited()
        station_name = arg
        station_id = None
        for name, st_id in self.stations.iteritems():
            print_nfo("[Google Play Music] [Station] '{0}'." \
                      .format(to_ascii(name)))
        if arg not in self.stations.keys():
            for name, st_id in self.stations.iteritems():
                if arg.lower() in name.lower():
                    station_id = st_id
                    station_name = name
                    break
        else:
            station_id = self.stations[arg]

        num_tracks = 200
        tracks = list()
        if station_id:
            try:
                tracks = self.__gmusic.get_station_tracks(station_id, \
                                                          num_tracks)
            except KeyError:
                raise RuntimeError("Operation requires an "
                                   "Unlimited subscription.")
            tracks_added = self.__enqueue_tracks(tracks)
            if tracks_added:
                if arg != station_name:
                    print_wrn("[Google Play Music] '{0}' not found. " \
                              "Playing '{1}' instead." \
                              .format(arg.encode('utf-8'), name.encode('utf-8')))
                logging.info("Added %d tracks from %s to queue", tracks_added, arg)
                self.__update_play_queue_order()
            else:
                print_wrn("[Google Play Music] '{0}' has no tracks. " \
                          .format(station_name))

        if not len(self.queue):
            print_wrn("[Google Play Music] '{0}' " \
                      "not found in the user's library. " \
                      .format(arg.encode('utf-8')))

    def __enqueue_station_unlimited(self, arg, max_results=200, quiet=False):
        """Search for a station and enqueue all of its tracks (Unlimited)

        """
        if not quiet:
            print_msg("[Google Play Music] [Station search in "\
                      "Google Play Music] : '{0}'. " \
                      .format(arg.encode('utf-8')))
        try:
            station_name = arg
            station_id = None
            station = self.__gmusic_search(arg, 'station', max_results, quiet)

            if station:
                station = station['station']
                station_name = station['name']
                seed = station['seed']
                seed_type = seed['seedType']
                track_id = seed['trackId'] if seed_type == u'2' else None
                artist_id = seed['artistId'] if seed_type == u'3' else None
                album_id = seed['albumId'] if seed_type == u'4' else None
                genre_id = seed['genreId'] if seed_type == u'5' else None
                playlist_token = seed['playlistShareToken'] if seed_type == u'8' else None
                curated_station_id = seed['curatedStationId'] if seed_type == u'9' else None
                num_tracks = max_results
                tracks = list()
                try:
                    station_id \
                        = self.__gmusic.create_station(station_name, \
                                                       track_id, \
                                                       artist_id, \
                                                       album_id, \
                                                       genre_id, \
                                                       playlist_token, \
                                                       curated_station_id)
                    tracks \
                        = self.__gmusic.get_station_tracks(station_id, \
                                                           num_tracks)
                except KeyError:
                    raise RuntimeError("Operation requires an "
                                       "Unlimited subscription.")
                tracks_added = self.__enqueue_tracks(tracks)
                if tracks_added:
                    if not quiet:
                        print_wrn("[Google Play Music] [Station] : '{0}'." \
                                  .format(station_name.encode('utf-8')))
                    logging.info("Added %d tracks from %s to queue", \
                                 tracks_added, arg.encode('utf-8'))
                    self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def __enqueue_situation_unlimited(self, arg):
        """Search for a situation and enqueue all of its tracks (Unlimited)

        """
        print_msg("[Google Play Music] [Situation search in "\
                  "Google Play Music] : '{0}'. " \
                  .format(arg.encode('utf-8')))
        try:
            situation_hits = self.__gmusic.search(arg)['situation_hits']

            if not len(situation_hits):
                # Do another search with an empty string
                situation_hits = self.__gmusic.search("")['situation_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            situation = next((hit for hit in situation_hits \
                              if 'best_result' in hit.keys()), None)

            num_tracks = 200
            if not situation and len(situation_hits):
                max_results = num_tracks / len(situation_hits)
                for hit in situation_hits:
                    situation = hit['situation']
                    print_nfo("[Google Play Music] [Situation] '{0} : {1}'." \
                              .format((hit['situation']['title']).encode('utf-8'),
                                      (hit['situation']['description']).encode('utf-8')))

                    self.__enqueue_station_unlimited(situation['title'], max_results, True)

            if not situation:
                raise KeyError

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))

    def __enqueue_tracks(self, tracks):
        """ Add tracks to the playback queue

        """
        count = 0
        for track in tracks:
            if u'id' not in track.keys():
                track[u'id'] = track['nid']
            self.queue.append(track)
            count += 1
        return count

    def __update_playlists(self):
        """ Retrieve the user's playlists

        """
        plists = self.__gmusic.get_all_user_playlist_contents()
        for plist in plists:
            plist_name = plist['name']
            logging.info("playlist name : %s", to_ascii(plist_name))
            tracks = plist['tracks']
            tracks.sort(key=itemgetter('creationTimestamp'))
            self.playlists[plist_name] = list()
            for track in tracks:
                try:
                    song = self.song_map[track['trackId']]
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def __update_playlists_unlimited(self):
        """ Retrieve shared playlists (Unlimited)

        """
        plists_subscribed_to = [p for p in self.__gmusic.get_all_playlists() \
                                if p.get('type') == 'SHARED']
        for plist in plists_subscribed_to:
            share_tok = plist['shareToken']
            playlist_items \
                = self.__gmusic.get_shared_playlist_contents(share_tok)
            plist_name = plist['name']
            logging.info("shared playlist name : %s", to_ascii(plist_name))
            self.playlists[plist_name] = list()
            for item in playlist_items:
                try:
                    song = item['track']
                    song['id'] = item['trackId']
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def __gmusic_search(self, query, query_type, max_results=200, quiet=False):
        """ Search Google Play (Unlimited)

        """

        search_results = self.__gmusic.search(query, max_results)[query_type + '_hits']
        result = next((hit for hit in search_results \
                            if 'best_result' in hit.keys()), None)

        if not result and len(search_results):
            secondary_hit = None
            for hit in search_results:
                if not quiet:
                    print_nfo("[Google Play Music] [{0}] '{1}'." \
                              .format(query_type.capitalize(),
                                      (hit[query_type]['name']).encode('utf-8')))
                if query.lower() == \
                   to_ascii(hit[query_type]['name']).lower():
                    result = hit
                    break
                if query.lower() in \
                   to_ascii(hit[query_type]['name']).lower():
                    secondary_hit = hit
            if not result and secondary_hit:
                result = secondary_hit

        if not result and not len(search_results):
            # Do another search with an empty string
            search_results = self.__gmusic.search("")[query_type + '_hits']

        if not result and len(search_results):
            # Play some random result from the search results
            random.seed()
            result = random.choice(search_results)
            if not quiet:
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(query.encode('utf-8')))

        return result
Example #4
0
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"]])
Example #5
0
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)
Example #6
0
class tizgmusicproxy(object):
    """A class for logging into a Google Play Music account and retrieving song
    URLs.

    """

    all_songs_album_title = "All Songs"
    thumbs_up_playlist_name = "Thumbs Up"

    # pylint: disable=too-many-instance-attributes,too-many-public-methods
    def __init__(self, email, password, device_id):
        self.__gmusic = Mobileclient()
        self.__email = email
        self.__device_id = device_id
        self.logged_in = False
        self.queue = list()
        self.queue_index = -1
        self.play_queue_order = list()
        self.play_modes = TizEnumeration(["NORMAL", "SHUFFLE"])
        self.current_play_mode = self.play_modes.NORMAL
        self.now_playing_song = None

        userdir = os.path.expanduser('~')
        tizconfig = os.path.join(userdir,
                                 ".config/tizonia/." + email + ".auth_token")
        auth_token = ""
        if os.path.isfile(tizconfig):
            with open(tizconfig, "r") as f:
                auth_token = pickle.load(f)
                if auth_token:
                    # 'Keep track of the auth token' workaround. See:
                    # https://github.com/diraimondo/gmusicproxy/issues/34#issuecomment-147359198
                    print_msg("[Google Play Music] [Authenticating] : " \
                              "'with cached auth token'")
                    self.__gmusic.android_id = device_id
                    self.__gmusic.session._authtoken = auth_token
                    self.__gmusic.session.is_authenticated = True
                    try:
                        self.__gmusic.get_registered_devices()
                    except CallFailure:
                        # The token has expired. Reset the client object
                        print_wrn("[Google Play Music] [Authenticating] : " \
                                  "'auth token expired'")
                        self.__gmusic = Mobileclient()
                        auth_token = ""

        if not auth_token:
            attempts = 0
            print_nfo("[Google Play Music] [Authenticating] : " \
                      "'with user credentials'")
            while not self.logged_in and attempts < 3:
                self.logged_in = self.__gmusic.login(email, password,
                                                     device_id)
                attempts += 1

            with open(tizconfig, "a+") as f:
                f.truncate()
                pickle.dump(self.__gmusic.session._authtoken, f)

        self.library = CaseInsensitiveDict()
        self.song_map = CaseInsensitiveDict()
        self.playlists = CaseInsensitiveDict()
        self.stations = CaseInsensitiveDict()

    def logout(self):
        """ Reset the session to an unauthenticated, default state.

        """
        self.__gmusic.logout()

    def set_play_mode(self, mode):
        """ Set the playback mode.

        :param mode: curren tvalid values are "NORMAL" and "SHUFFLE"

        """
        self.current_play_mode = getattr(self.play_modes, mode)
        self.__update_play_queue_order()

    def current_song_title_and_artist(self):
        """ Retrieve the current track's title and artist name.

        """
        logging.info("current_song_title_and_artist")
        song = self.now_playing_song
        if song:
            title = to_ascii(self.now_playing_song.get('title'))
            artist = to_ascii(self.now_playing_song.get('artist'))
            logging.info("Now playing %s by %s", title, artist)
            return artist, title
        else:
            return '', ''

    def current_song_album_and_duration(self):
        """ Retrieve the current track's album and duration.

        """
        logging.info("current_song_album_and_duration")
        song = self.now_playing_song
        if song:
            album = to_ascii(self.now_playing_song.get('album'))
            duration = to_ascii \
                       (self.now_playing_song.get('durationMillis'))
            logging.info("album %s duration %s", album, duration)
            return album, int(duration)
        else:
            return '', 0

    def current_track_and_album_total(self):
        """Return the current track number and the total number of tracks in the
        album, if known.

        """
        logging.info("current_track_and_album_total")
        song = self.now_playing_song
        track = 0
        total = 0
        if song:
            try:
                track = self.now_playing_song['trackNumber']
                total = self.now_playing_song['totalTrackCount']
                logging.info("track number %s total tracks %s", track, total)
            except KeyError:
                logging.info("trackNumber or totalTrackCount : not found")
        else:
            logging.info("current_song_track_number_"
                         "and_total_tracks : not found")
        return track, total

    def current_song_year(self):
        """ Return the current track's year of publication.

        """
        logging.info("current_song_year")
        song = self.now_playing_song
        year = 0
        if song:
            try:
                year = song['year']
                logging.info("track year %s", year)
            except KeyError:
                logging.info("year : not found")
        else:
            logging.info("current_song_year : not found")
        return year

    def clear_queue(self):
        """ Clears the playback queue.

        """
        self.queue = list()
        self.queue_index = -1

    def enqueue_tracks(self, arg):
        """ Search the user's library for tracks and add
        them to the playback queue.

        :param arg: a track search term
        """
        try:
            songs = self.__gmusic.get_all_songs()

            track_hits = list()
            for song in songs:
                song_title = song['title']
                if arg.lower() in song_title.lower():
                    track_hits.append(song)
                    print_nfo("[Google Play Music] [Track] '{0}'." \
                              .format(to_ascii(song_title)))

            if not len(track_hits):
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))
                random.seed()
                track_hits = random.sample(songs, MAX_TRACKS)
                for hit in track_hits:
                    song_title = hit['title']
                    print_nfo("[Google Play Music] [Track] '{0}'." \
                              .format(to_ascii(song_title)))

            if not len(track_hits):
                raise KeyError

            tracks_added = self.__enqueue_tracks(track_hits)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)

            self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Track not found : {0}".format(arg))

    def enqueue_artist(self, arg):
        """ Search the user's library for tracks from the given artist and add
        them to the playback queue.

        :param arg: an artist
        """
        try:
            self.__update_local_library()
            artist = None
            artist_dict = None
            if arg not in self.library.keys():
                for name, art in self.library.iteritems():
                    if arg.lower() in name.lower():
                        artist = name
                        artist_dict = art
                        if arg.lower() != name.lower():
                            print_wrn("[Google Play Music] '{0}' not found. " \
                                      "Playing '{1}' instead." \
                                      .format(arg.encode('utf-8'), \
                                              name.encode('utf-8')))
                        break
                if not artist:
                    # Play some random artist from the library
                    random.seed()
                    artist = random.choice(self.library.keys())
                    artist_dict = self.library[artist]
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))
            else:
                artist = arg
                artist_dict = self.library[arg]
            tracks_added = 0
            for album in artist_dict:
                tracks_added += self.__enqueue_tracks(artist_dict[album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(artist)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))

    def enqueue_album(self, arg):
        """ Search the user's library for albums with a given name and add
        them to the playback queue.

        """
        try:
            self.__update_local_library()
            album = None
            artist = None
            tentative_album = None
            tentative_artist = None
            for library_artist in self.library:
                for artist_album in self.library[library_artist]:
                    print_nfo("[Google Play Music] [Album] '{0}'." \
                              .format(to_ascii(artist_album)))
                    if not album:
                        if arg.lower() == artist_album.lower():
                            album = artist_album
                            artist = library_artist
                            break
                    if not tentative_album:
                        if arg.lower() in artist_album.lower():
                            tentative_album = artist_album
                            tentative_artist = library_artist
                if album:
                    break

            if not album and tentative_album:
                album = tentative_album
                artist = tentative_artist
                print_wrn("[Google Play Music] '{0}' not found. " \
                          "Playing '{1}' instead." \
                          .format(arg.encode('utf-8'), \
                          album.encode('utf-8')))
            if not album:
                # Play some random album from the library
                random.seed()
                artist = random.choice(self.library.keys())
                album = random.choice(self.library[artist].keys())
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            if not album:
                raise KeyError("Album not found : {0}".format(arg))

            self.__enqueue_tracks(self.library[artist][album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(album)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))

    def enqueue_playlist(self, arg):
        """Search the user's library for playlists with a given name
        and add the tracks of the first match to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            self.__update_local_library()
            self.__update_playlists()
            self.__update_playlists_unlimited()
            playlist = None
            playlist_name = None
            for name, plist in self.playlists.items():
                print_nfo("[Google Play Music] [Playlist] '{0}'." \
                          .format(to_ascii(name)))
            if arg not in self.playlists.keys():
                for name, plist in self.playlists.iteritems():
                    if arg.lower() in name.lower():
                        playlist = plist
                        playlist_name = name
                        if arg.lower() != name.lower():
                            print_wrn("[Google Play Music] '{0}' not found. " \
                                      "Playing '{1}' instead." \
                                      .format(arg.encode('utf-8'), \
                                              to_ascii(name)))
                            break
            else:
                playlist_name = arg
                playlist = self.playlists[arg]

            random.seed()
            x = 0
            while (not playlist or not len(playlist)) and x < 3:
                x += 1
                # Play some random playlist from the library
                playlist_name = random.choice(self.playlists.keys())
                playlist = self.playlists[playlist_name]
                print_wrn("[Google Play Music] '{0}' not found or found empty. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            if not len(playlist):
                raise KeyError

            self.__enqueue_tracks(playlist)
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(playlist_name)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError(
                "Playlist not found or found empty : {0}".format(arg))

    def enqueue_podcast(self, arg):
        """Search Google Play Music for a podcast series and add its tracks to the
        playback queue ().

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving podcasts] : '{0}'. " \
                  .format(self.__email))

        try:

            self.__enqueue_podcast(arg)

            if not len(self.queue):
                raise KeyError

            logging.info("Added %d episodes from '%s' to queue", \
                         len(self.queue), arg)
            self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Podcast not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_station_unlimited(self, arg):
        """Search the user's library for a station with a given name
        and add its tracks to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            # First try to find a suitable station in the user's library
            self.__enqueue_user_station_unlimited(arg)

            if not len(self.queue):
                # If no suitable station is found in the user's library, then
                # search google play unlimited for a potential match.
                self.__enqueue_station_unlimited(arg)

            if not len(self.queue):
                raise KeyError

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def enqueue_genre_unlimited(self, arg):
        """Search Unlimited for a genre with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving genres] : '{0}'. " \
                  .format(self.__email))

        try:
            all_genres = list()
            root_genres = self.__gmusic.get_genres()
            second_tier_genres = list()
            for root_genre in root_genres:
                second_tier_genres += self.__gmusic.get_genres(
                    root_genre['id'])
            all_genres += root_genres
            all_genres += second_tier_genres
            for genre in all_genres:
                print_nfo("[Google Play Music] [Genre] '{0}'." \
                          .format(to_ascii(genre['name'])))
            genre = dict()
            if arg not in all_genres:
                genre = next((g for g in all_genres \
                              if arg.lower() in to_ascii(g['name']).lower()), \
                             None)

            tracks_added = 0
            while not tracks_added:
                if not genre and len(all_genres):
                    # Play some random genre from the search results
                    random.seed()
                    genre = random.choice(all_genres)
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))

                genre_name = genre['name']
                genre_id = genre['id']
                station_id = self.__gmusic.create_station(genre_name, \
                                                          None, None, None, genre_id)
                num_tracks = MAX_TRACKS
                tracks = self.__gmusic.get_station_tracks(
                    station_id, num_tracks)
                tracks_added = self.__enqueue_tracks(tracks)
                logging.info("Added %d tracks from %s to queue", tracks_added,
                             genre_name)
                if not tracks_added:
                    # This will produce another iteration in the loop
                    genre = None

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(genre['name'])))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Genre not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_situation_unlimited(self, arg):
        """Search Unlimited for a situation with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving situations] : '{0}'. " \
                  .format(self.__email))

        try:

            self.__enqueue_situation_unlimited(arg)

            if not len(self.queue):
                raise KeyError

            logging.info("Added %d tracks from %s to queue", \
                         len(self.queue), arg)

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_artist_unlimited(self, arg):
        """Search Unlimited for an artist and add the artist's 200 top tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            artist = self.__gmusic_search(arg, 'artist')

            include_albums = False
            max_top_tracks = MAX_TRACKS
            max_rel_artist = 0
            artist_tracks = dict()
            if artist:
                artist_tracks = self.__gmusic.get_artist_info \
                                (artist['artist']['artistId'],
                                 include_albums, max_top_tracks,
                                 max_rel_artist)['topTracks']

            if not artist_tracks:
                raise KeyError

            for track in artist_tracks:
                song_title = track['title']
                print_nfo("[Google Play Music] [Track] '{0}'." \
                          .format(to_ascii(song_title)))

            tracks_added = self.__enqueue_tracks(artist_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_album_unlimited(self, arg):
        """Search Unlimited for an album and add its tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            album = self.__gmusic_search(arg, 'album')
            album_tracks = dict()
            if album:
                album_tracks = self.__gmusic.get_album_info \
                               (album['album']['albumId'])['tracks']
            if not album_tracks:
                raise KeyError

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format((album['album']['name']).encode('utf-8')))

            tracks_added = self.__enqueue_tracks(album_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_tracks_unlimited(self, arg):
        """ Search Unlimited for a track name and add all the matching tracks
        to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        try:
            max_results = MAX_TRACKS
            track_hits = self.__gmusic.search(arg, max_results)['song_hits']
            if not len(track_hits):
                # Do another search with an empty string
                track_hits = self.__gmusic.search("", max_results)['song_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            tracks = list()
            for hit in track_hits:
                tracks.append(hit['track'])
            tracks_added = self.__enqueue_tracks(tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_playlist_unlimited(self, arg):
        """Search Unlimited for a playlist name and add all its tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving playlists] : '{0}'. " \
                  .format(self.__email))

        try:
            playlist_tracks = list()

            playlist_hits = self.__gmusic_search(arg, 'playlist')
            if playlist_hits:
                playlist = playlist_hits['playlist']
                playlist_contents = self.__gmusic.get_shared_playlist_contents(
                    playlist['shareToken'])
            else:
                raise KeyError

            print_nfo("[Google Play Music] [Playlist] '{}'." \
                      .format(playlist['name']).encode('utf-8'))

            for item in playlist_contents:
                print_nfo("[Google Play Music] [Playlist Track] '{} by {} (Album: {}, {})'." \
                          .format((item['track']['title']).encode('utf-8'),
                                  (item['track']['artist']).encode('utf-8'),
                                  (item['track']['album']).encode('utf-8'),
                                  (item['track']['year'])))
                track = item['track']
                playlist_tracks.append(track)

            if not playlist_tracks:
                raise KeyError

            tracks_added = self.__enqueue_tracks(playlist_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_promoted_tracks_unlimited(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        try:
            tracks = self.__gmusic.get_promoted_songs()
            count = 0
            for track in tracks:
                store_track = self.__gmusic.get_track_info(track['storeId'])
                if u'id' not in store_track.keys():
                    store_track[u'id'] = store_track['storeId']
                self.queue.append(store_track)
                count += 1
            if count == 0:
                print_wrn("[Google Play Music] Operation requires " \
                          "an Unlimited subscription.")
            logging.info("Added %d Unlimited promoted tracks to queue", \
                         count)
            self.__update_play_queue_order()
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def next_url(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        if len(self.queue):
            self.queue_index += 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                next_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(next_song)
            else:
                self.queue_index = -1
                return self.next_url()
        else:
            return ''

    def prev_url(self):
        """ Retrieve the url of the previous track in the playback queue.

        """
        if len(self.queue):
            self.queue_index -= 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                prev_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(prev_song)
            else:
                self.queue_index = len(self.queue)
                return self.prev_url()
        else:
            return ''

    def __update_play_queue_order(self):
        """ Update the queue playback order.

        A sequential order is applied if the current play mode is "NORMAL" or a
        random order if current play mode is "SHUFFLE"

        """
        total_tracks = len(self.queue)
        if total_tracks:
            if not len(self.play_queue_order):
                # Create a sequential play order, if empty
                self.play_queue_order = range(total_tracks)
            if self.current_play_mode == self.play_modes.SHUFFLE:
                random.shuffle(self.play_queue_order)
            print_nfo("[Google Play Music] [Tracks in queue] '{0}'." \
                      .format(total_tracks))

    def __retrieve_track_url(self, song):
        """ Retrieve a song url

        """
        if song.get('episodeId'):
            song_url = self.__gmusic.get_podcast_episode_stream_url(
                song['episodeId'], self.__device_id)
        else:
            song_url = self.__gmusic.get_stream_url(song['id'],
                                                    self.__device_id)

        try:
            self.now_playing_song = song
            return song_url
        except AttributeError:
            logging.info("Could not retrieve the song url!")
            raise

    def __update_local_library(self):
        """ Retrieve the songs and albums from the user's library

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        songs = self.__gmusic.get_all_songs()
        self.playlists[self.thumbs_up_playlist_name] = list()

        # Retrieve the user's song library
        for song in songs:
            if "rating" in song and song['rating'] == "5":
                self.playlists[self.thumbs_up_playlist_name].append(song)

            song_id = song['id']
            song_artist = song['artist']
            song_album = song['album']

            self.song_map[song_id] = song

            if song_artist == "":
                song_artist = "Unknown Artist"

            if song_album == "":
                song_album = "Unknown Album"

            if song_artist not in self.library:
                self.library[song_artist] = CaseInsensitiveDict()
                self.library[song_artist][self.all_songs_album_title] = list()

            if song_album not in self.library[song_artist]:
                self.library[song_artist][song_album] = list()

            self.library[song_artist][song_album].append(song)
            self.library[song_artist][self.all_songs_album_title].append(song)

        # Sort albums by track number
        for artist in self.library.keys():
            logging.info("Artist : %s", to_ascii(artist))
            for album in self.library[artist].keys():
                logging.info("   Album : %s", to_ascii(album))
                if album == self.all_songs_album_title:
                    sorted_album = sorted(self.library[artist][album],
                                          key=lambda k: k['title'])
                else:
                    sorted_album = sorted(
                        self.library[artist][album],
                        key=lambda k: k.get('trackNumber', 0))
                self.library[artist][album] = sorted_album

    def __update_stations_unlimited(self):
        """ Retrieve stations (Unlimited)

        """
        self.stations.clear()
        stations = self.__gmusic.get_all_stations()
        self.stations[u"I'm Feeling Lucky"] = 'IFL'
        for station in stations:
            station_name = station['name']
            logging.info("station name : %s", to_ascii(station_name))
            self.stations[station_name] = station['id']

    def __enqueue_user_station_unlimited(self, arg):
        """ Enqueue a user station (Unlimited)

        """
        print_msg("[Google Play Music] [Station search "\
                  "in user's library] : '{0}'. " \
                  .format(self.__email))
        self.__update_stations_unlimited()
        station_name = arg
        station_id = None
        for name, st_id in self.stations.iteritems():
            print_nfo("[Google Play Music] [Station] '{0}'." \
                      .format(to_ascii(name)))
        if arg not in self.stations.keys():
            for name, st_id in self.stations.iteritems():
                if arg.lower() in name.lower():
                    station_id = st_id
                    station_name = name
                    break
        else:
            station_id = self.stations[arg]

        num_tracks = MAX_TRACKS
        tracks = list()
        if station_id:
            try:
                tracks = self.__gmusic.get_station_tracks(station_id, \
                                                          num_tracks)
            except KeyError:
                raise RuntimeError("Operation requires an "
                                   "Unlimited subscription.")
            tracks_added = self.__enqueue_tracks(tracks)
            if tracks_added:
                if arg.lower() != station_name.lower():
                    print_wrn("[Google Play Music] '{0}' not found. " \
                              "Playing '{1}' instead." \
                              .format(arg.encode('utf-8'), name.encode('utf-8')))
                logging.info("Added %d tracks from %s to queue", tracks_added,
                             arg)
                self.__update_play_queue_order()
            else:
                print_wrn("[Google Play Music] '{0}' has no tracks. " \
                          .format(station_name))

        if not len(self.queue):
            print_wrn("[Google Play Music] '{0}' " \
                      "not found in the user's library. " \
                      .format(arg.encode('utf-8')))

    def __enqueue_station_unlimited(self,
                                    arg,
                                    max_results=MAX_TRACKS,
                                    quiet=False):
        """Search for a station and enqueue all of its tracks (Unlimited)

        """
        if not quiet:
            print_msg("[Google Play Music] [Station search in "\
                      "Google Play Music] : '{0}'. " \
                      .format(arg.encode('utf-8')))
        try:
            station_name = arg
            station_id = None
            station = self.__gmusic_search(arg, 'station', max_results, quiet)

            if station:
                station = station['station']
                station_name = station['name']
                seed = station['seed']
                seed_type = seed['seedType']
                track_id = seed['trackId'] if seed_type == u'2' else None
                artist_id = seed['artistId'] if seed_type == u'3' else None
                album_id = seed['albumId'] if seed_type == u'4' else None
                genre_id = seed['genreId'] if seed_type == u'5' else None
                playlist_token = seed[
                    'playlistShareToken'] if seed_type == u'8' else None
                curated_station_id = seed[
                    'curatedStationId'] if seed_type == u'9' else None
                num_tracks = max_results
                tracks = list()
                try:
                    station_id \
                        = self.__gmusic.create_station(station_name, \
                                                       track_id, \
                                                       artist_id, \
                                                       album_id, \
                                                       genre_id, \
                                                       playlist_token, \
                                                       curated_station_id)
                    tracks \
                        = self.__gmusic.get_station_tracks(station_id, \
                                                           num_tracks)
                except KeyError:
                    raise RuntimeError("Operation requires an "
                                       "Unlimited subscription.")
                tracks_added = self.__enqueue_tracks(tracks)
                if tracks_added:
                    if not quiet:
                        print_wrn("[Google Play Music] [Station] : '{0}'." \
                                  .format(station_name.encode('utf-8')))
                    logging.info("Added %d tracks from %s to queue", \
                                 tracks_added, arg.encode('utf-8'))
                    self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def __enqueue_situation_unlimited(self, arg):
        """Search for a situation and enqueue all of its tracks (Unlimited)

        """
        print_msg("[Google Play Music] [Situation search in "\
                  "Google Play Music] : '{0}'. " \
                  .format(arg.encode('utf-8')))
        try:
            situation_hits = self.__gmusic.search(arg)['situation_hits']

            # If the search didn't return results, just do another search with
            # an empty string
            if not len(situation_hits):
                situation_hits = self.__gmusic.search("")['situation_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            # Try to find a "best result", if one exists
            situation = next((hit for hit in situation_hits \
                              if 'best_result' in hit.keys() \
                              and hit['best_result'] == True), None)

            num_tracks = MAX_TRACKS

            # If there is no best result, then get a selection of tracks from
            # each situation. At least we'll play some music.
            if not situation and len(situation_hits):
                max_results = num_tracks / len(situation_hits)
                for hit in situation_hits:
                    situation = hit['situation']
                    print_nfo("[Google Play Music] [Situation] '{0} : {1}'." \
                              .format((hit['situation']['title']).encode('utf-8'),
                                      (hit['situation']['description']).encode('utf-8')))
                    self.__enqueue_station_unlimited(situation['title'],
                                                     max_results, True)
            elif situation:
                # There is at list one sitution, enqueue its tracks.
                situation = situation['situation']
                max_results = num_tracks
                self.__enqueue_station_unlimited(situation['title'],
                                                 max_results, True)

            if not situation:
                raise KeyError

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))

    def __enqueue_podcast(self, arg):
        """Search for a podcast series and enqueue all of its tracks.

        """
        print_msg("[Google Play Music] [Podcast search in "\
                  "Google Play Music] : '{0}'. " \
                  .format(arg.encode('utf-8')))
        try:
            podcast_hits = self.__gmusic_search(arg,
                                                'podcast',
                                                10,
                                                quiet=False)

            if not podcast_hits:
                print_wrn(
                    "[Google Play Music] [Podcast] 'Search returned zero results'."
                )
                print_wrn(
                    "[Google Play Music] [Podcast] 'Are you in a supported region "
                    "(currently only US and Canada) ?'")

            # Use the first podcast retrieved. At least we'll play something.
            podcast = dict()
            if podcast_hits and len(podcast_hits):
                podcast = podcast_hits['series']

            episodes_added = 0
            if podcast:
                # There is a podcast, enqueue its episodes.
                print_nfo("[Google Play Music] [Podcast] 'Playing '{0}' by {1}'." \
                          .format((podcast['title']).encode('utf-8'),
                                  (podcast['author']).encode('utf-8')))
                print_nfo("[Google Play Music] [Podcast] '{0}'." \
                          .format((podcast['description'][0:150]).encode('utf-8')))
                series = self.__gmusic.get_podcast_series_info(
                    podcast['seriesId'])
                episodes = series['episodes']
                for episode in episodes:
                    print_nfo("[Google Play Music] [Podcast Episode] '{0} : {1}'." \
                              .format((episode['title']).encode('utf-8'),
                                      (episode['description'][0:80]).encode('utf-8')))
                episodes_added = self.__enqueue_tracks(episodes)

            if not podcast or not episodes_added:
                raise KeyError

        except KeyError:
            raise KeyError(
                "Podcast not found or no episodes found: {0}".format(arg))

    def __enqueue_tracks(self, tracks):
        """ Add tracks to the playback queue

        """
        count = 0
        for track in tracks:
            if u'id' not in track.keys() and track.get('storeId'):
                track[u'id'] = track['storeId']
            self.queue.append(track)
            count += 1
        return count

    def __update_playlists(self):
        """ Retrieve the user's playlists

        """
        plists = self.__gmusic.get_all_user_playlist_contents()
        for plist in plists:
            plist_name = plist.get('name')
            tracks = plist.get('tracks')
            if plist_name and tracks:
                logging.info("playlist name : %s", to_ascii(plist_name))
                tracks.sort(key=itemgetter('creationTimestamp'))
                self.playlists[plist_name] = list()
                for track in tracks:
                    song_id = track.get('trackId')
                    if song_id:
                        song = self.song_map.get(song_id)
                        if song:
                            self.playlists[plist_name].append(song)

    def __update_playlists_unlimited(self):
        """ Retrieve shared playlists (Unlimited)

        """
        plists_subscribed_to = [p for p in self.__gmusic.get_all_playlists() \
                                if p.get('type') == 'SHARED']
        for plist in plists_subscribed_to:
            share_tok = plist['shareToken']
            playlist_items \
                = self.__gmusic.get_shared_playlist_contents(share_tok)
            plist_name = plist['name']
            logging.info("shared playlist name : %s", to_ascii(plist_name))
            self.playlists[plist_name] = list()
            for item in playlist_items:
                try:
                    song = item['track']
                    song['id'] = item['trackId']
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def __gmusic_search(self,
                        query,
                        query_type,
                        max_results=MAX_TRACKS,
                        quiet=False):
        """ Search Google Play (Unlimited)

        """

        search_results = self.__gmusic.search(query, max_results)[query_type +
                                                                  '_hits']

        # This is a workaround. Some podcast results come without these two
        # keys in the dictionary
        if query_type == "podcast" and len(search_results) \
           and not search_results[0].get('navigational_result'):
            for res in search_results:
                res[u'best_result'] = False
                res[u'navigational_result'] = False
                res[query_type] = res['series']

        result = ''
        if query_type != "playlist":
            result = next((hit for hit in search_results \
                           if 'best_result' in hit.keys() \
                           and hit['best_result'] == True), None)

        if not result and len(search_results):
            secondary_hit = None
            for hit in search_results:
                name = ''
                if hit[query_type].get('name'):
                    name = hit[query_type].get('name')
                elif hit[query_type].get('title'):
                    name = hit[query_type].get('title')
                if not quiet:
                    print_nfo("[Google Play Music] [{0}] '{1}'." \
                              .format(query_type.capitalize(),
                                      (name).encode('utf-8')))
                if query.lower() == \
                   to_ascii(name).lower():
                    result = hit
                    break
                if query.lower() in \
                   to_ascii(name).lower():
                    secondary_hit = hit
            if not result and secondary_hit:
                result = secondary_hit

        if not result and not len(search_results):
            # Do another search with an empty string
            search_results = self.__gmusic.search("")[query_type + '_hits']

        if not result and len(search_results):
            # Play some random result from the search results
            random.seed()
            result = random.choice(search_results)
            if not quiet:
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(query.encode('utf-8')))

        return result
Example #7
0
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)
Example #8
0
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)
Example #9
0
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):
        # 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:
            searchLib = self._search_library("artist", name)
            if searchLib is not None:
                self.logger.debug(searchLib)
                return searchLib
            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:
            searchLib = self._search_library("album", name)
            if searchLib is not None:
                self.logger.debug(searchLib)
                return searchLib
            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)

        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:
            searchLib = self._search_library_for_first("title", name)
            if searchLib is not None:
                return searchLib
            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" % (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)
Example #10
0
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']])