Ejemplo n.º 1
0
Archivo: gp.py Proyecto: vale981/clay
class _GP(object):
    """
    Interface to :class:`gmusicapi.Mobileclient`. Implements
    asynchronous API calls, caching and some other perks.

    Singleton.
    """
    # TODO: Switch to urwid signals for more explicitness?
    caches_invalidated = EventHook()

    def __init__(self):
        # self.is_debug = os.getenv('CLAY_DEBUG')
        self.mobile_client = Mobileclient()
        self.mobile_client._make_call = self._make_call_proxy(
            self.mobile_client._make_call)
        # if self.is_debug:
        #     self.debug_file = open('/tmp/clay-api-log.json', 'w')
        #     self._last_call_index = 0
        self.cached_tracks = None
        self.cached_liked_songs = LikedSongs()
        self.cached_playlists = None
        self.cached_stations = None

        self.invalidate_caches()

        self.auth_state_changed = EventHook()

    def _make_call_proxy(self, func):
        """
        Return a function that wraps *fn* and logs args & return values.
        """
        def _make_call(protocol, *args, **kwargs):
            """
            Wrapper function.
            """
            logger.debug('GP::{}(*{}, **{})'.format(protocol.__name__, args,
                                                    kwargs))
            result = func(protocol, *args, **kwargs)
            # self._last_call_index += 1
            # call_index = self._last_call_index
            # self.debug_file.write(json.dumps([
            #     call_index,
            #     protocol.__name__, args, kwargs,
            #     result
            # ]) + '\n')
            # self.debug_file.flush()
            return result

        return _make_call

    def invalidate_caches(self):
        """
        Clear cached tracks & playlists & stations.
        """
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.caches_invalidated.fire()

    @synchronized
    def login(self, email, password, device_id, **_):
        """
        Log in into Google Play Music.
        """
        self.mobile_client.logout()
        self.invalidate_caches()
        # prev_auth_state = self.is_authenticated
        result = self.mobile_client.login(email, password, device_id)
        # if prev_auth_state != self.is_authenticated:
        self.auth_state_changed.fire(self.is_authenticated)
        return result

    login_async = asynchronous(login)

    @synchronized
    def use_authtoken(self, authtoken, device_id):
        """
        Try to use cached token to log into Google Play Music.
        """
        # pylint: disable=protected-access
        self.mobile_client.session._authtoken = authtoken
        self.mobile_client.session.is_authenticated = True
        self.mobile_client.android_id = device_id
        del self.mobile_client.is_subscribed
        if self.mobile_client.is_subscribed:
            self.auth_state_changed.fire(True)
            return True
        del self.mobile_client.is_subscribed
        self.mobile_client.android_id = None
        self.mobile_client.session.is_authenticated = False
        self.auth_state_changed.fire(False)
        return False

    use_authtoken_async = asynchronous(use_authtoken)

    def get_authtoken(self):
        """
        Return currently active auth token.
        """
        # pylint: disable=protected-access
        return self.mobile_client.session._authtoken

    @synchronized
    def get_all_tracks(self):
        """
        Cache and return all tracks from "My library".

        Each track will have "id" and "storeId" keys.
        """
        if self.cached_tracks:
            return self.cached_tracks
        data = self.mobile_client.get_all_songs()
        self.cached_tracks = Track.from_data(data, Track.SOURCE_LIBRARY, True)

        return self.cached_tracks

    get_all_tracks_async = asynchronous(get_all_tracks)

    def get_stream_url(self, stream_id):
        """
        Returns playable stream URL of track by id.
        """
        return self.mobile_client.get_stream_url(stream_id)

    get_stream_url_async = asynchronous(get_stream_url)

    def increment_song_playcount(self, track_id):
        """
        Increments the playcount of the song given by track_id
        by one.
        """
        return gp.mobile_client.increment_song_playcount(track_id)

    increment_song_playcount_async = asynchronous(increment_song_playcount)

    @synchronized
    def get_all_user_station_contents(self, **_):
        """
              Return list of :class:`.Station` instances.
              """
        if self.cached_stations:
            return self.cached_stations
        self.get_all_tracks()

        self.cached_stations = Station.from_data(
            self.mobile_client.get_all_stations(), True)
        return self.cached_stations

    get_all_user_station_contents_async = (  # pylint: disable=invalid-name
        asynchronous(get_all_user_station_contents))

    @synchronized
    def get_all_user_playlist_contents(self, **_):
        """
        Return list of :class:`.Playlist` instances.
        """
        if self.cached_playlists:
            return [self.cached_liked_songs] + self.cached_playlists

        self.get_all_tracks()

        self.cached_playlists = Playlist.from_data(
            self.mobile_client.get_all_user_playlist_contents(), True)
        return [self.cached_liked_songs] + self.cached_playlists

    get_all_user_playlist_contents_async = (  # pylint: disable=invalid-name
        asynchronous(get_all_user_playlist_contents))

    def get_cached_tracks_map(self):
        """
        Return a dictionary of tracks where keys are strings with track IDs
        and values are :class:`.Track` instances.
        """
        return {track.id: track for track in self.cached_tracks}

    def get_track_by_id(self, any_id):
        """
        Return track by id or store_id.
        """
        for track in self.cached_tracks:
            if any_id in (track.library_id, track.store_id,
                          track.playlist_item_id):
                return track
        return None

    def search(self, query):
        """
        Find tracks and return an instance of :class:`.SearchResults`.
        """
        results = self.mobile_client.search(query)
        return SearchResults.from_data(results)

    search_async = asynchronous(search)

    def add_to_my_library(self, track):
        """
        Add a track to my library.
        """
        result = self.mobile_client.add_store_tracks(track.id)
        if result:
            self.invalidate_caches()
        return result

    def remove_from_my_library(self, track):
        """
        Remove a track from my library.
        """
        result = self.mobile_client.delete_songs(track.id)
        if result:
            self.invalidate_caches()
        return result

    @property
    def is_authenticated(self):
        """
        Return True if user is authenticated on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_authenticated()

    @property
    def is_subscribed(self):
        """
        Return True if user is subscribed on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_subscribed
Ejemplo n.º 2
0
                        track_info = {"filename": filename}
                        search_terms = []
                        for term in ['ALBUM', 'ARTIST', 'TITLE']:
                            got = False
                            if term in tags and len(tags[term]):
                                search_terms.append(tags[term])

                        do_again = True
                        skip = False
                        while do_again:
                            do_again = False
                            if len(search_terms):
                                # got from tags; trying to use it
                                debug(f"search terms: {search_terms}")
                                search = api.search(' '.join(search_terms))
                            else:
                                error(f"failed to find search terms for {filename}")
                                error(f"trying with path name")
                                tmp_terms = re.split('\./+|/+', filename)
                                search_terms = []
                                for term in tmp_terms:
                                    if not len(term):
                                        continue
                                    m = re.match('^\d+[\s\-_.]+([^\s]+.*)(?:\.mp3?)$', term, re.I)
                                    if m:
                                        term = m.group(1)
                                    search_terms.append(term)
                                debug(f"search terms: {search_terms}")
                                search = api.search(' '.join(search_terms))
                                print(f"found {len(search['song_hits'])} tracks:")
Ejemplo n.º 3
0
class _GP(object):
    """
    Interface to :class:`gmusicapi.Mobileclient`. Implements
    asynchronous API calls, caching and some other perks.

    Singleton.
    """
    # TODO: Switch to urwid signals for more explicitness?
    caches_invalidated = EventHook()
    parsed_songs = EventHook()

    def __init__(self):
        # self.is_debug = os.getenv('CLAY_DEBUG')
        self.mobile_client = Mobileclient()
        self.mobile_client._make_call = self._make_call_proxy(
            self.mobile_client._make_call)
        # if self.is_debug:
        #     self.debug_file = open('/tmp/clay-api-log.json', 'w')
        #     self._last_call_index = 0
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artists = {}
        self.cached_albums = {}
        self.liked_songs = LikedSongs()

        self.invalidate_caches()

        self.auth_state_changed = EventHook()

    def _make_call_proxy(self, func):
        """
        Return a function that wraps *fn* and logs args & return values.
        """
        def _make_call(protocol, *args, **kwargs):
            """
            Wrapper function.
            """
            logger.debug('GP::{}(*{}, **{})'.format(protocol.__name__, args,
                                                    kwargs))
            result = func(protocol, *args, **kwargs)
            # self._last_call_index += 1
            # call_index = self._last_call_index
            # self.debug_file.write(json.dumps([
            #     call_index,
            #     protocol.__name__, args, kwargs,
            #     result
            # ]) + '\n')
            # self.debug_file.flush()
            return result

        return _make_call

    def invalidate_caches(self):
        """
        Clear cached tracks & playlists & stations.
        """
        self.cached_tracks = None
        self.cached_playlists = None
        self.cached_stations = None
        self.cached_artist = None
        self.caches_invalidated.fire()

    @synchronized
    def login(self, email, password, device_id, **_):
        """
        Log in into Google Play Music.
        """
        self.mobile_client.logout()
        self.invalidate_caches()
        from os.path import exists
        CRED_FILE = "/home/thor/.config/clay/google_auth.cred"
        if not exists(CRED_FILE):
            from oauth2client.client import FlowExchangeError
            try:
                self.mobile_client.perform_oauth(CRED_FILE, open_browser=True)
            except FlowExchangeError:
                raise RuntimeError("OAuth authentication failed, try again")
        result = self.mobile_client.oauth_login(
            self.mobile_client.FROM_MAC_ADDRESS, CRED_FILE)
        self.auth_state_changed.fire(self.is_authenticated)
        return result

    login_async = asynchronous(login)

    @synchronized
    def get_artist_info(self, artist_id):
        """
        Get the artist info
        """
        return self.mobile_client.get_artist_info(artist_id,
                                                  max_rel_artist=0,
                                                  max_top_tracks=15)

    @synchronized
    def get_album_tracks(self, album_id):
        """
        Get album tracks
        """
        return self.mobile_client.get_album_info(album_id,
                                                 include_tracks=True)['tracks']

    @synchronized
    def add_album_song(self, id_, album_name, track):
        """
        Adds an album to an artist and adds the specified track to it

        Args:
            id_ (`str`): the album ID (currently the same as the album title)
            album_name (`str`): the name of the album
            track (`clay.gp.Track`): the track in the album
        """
        if album_name == '':
            id_ = track.artist
            album_name = "Unknown Album"

        if id_ not in self.cached_albums:
            self.cached_albums[id_] = Album(track.album_artist, {
                'albumId': id_,
                'name': album_name
            })

        self.cached_albums[id_].add_track(track)

        return self.cached_albums[id_]

    @synchronized
    def add_artist(self, artist_id, name):
        """
        Creates or lookup an artist object and return it.

        Args:
           artist_id (`str`): The Artist id given by Google Play Music

        Returns:
           The artist class
        """
        name = ("Unknown Artist" if name == '' else name)
        lname = name.lower()
        if lname not in self.cached_artists:
            self.cached_artists[lname] = Artist(artist_id, name)

        return self.cached_artists[lname]

    @synchronized
    def use_authtoken(self, authtoken, device_id):
        """
        Try to use cached token to log into Google Play Music.
        """
        self.mobile_client.session._authtoken = authtoken
        self.mobile_client.session.is_authenticated = True
        self.mobile_client.android_id = device_id
        del self.mobile_client.is_subscribed
        if self.mobile_client.is_subscribed:
            self.auth_state_changed.fire(True)
            return True
        del self.mobile_client.is_subscribed
        self.mobile_client.android_id = None
        self.mobile_client.session.is_authenticated = False
        self.auth_state_changed.fire(False)
        return False

    use_authtoken_async = asynchronous(use_authtoken)

    def get_authtoken(self):
        """
        Return currently active auth token.
        """
        return self.mobile_client.session._authtoken

    @synchronized
    def get_all_tracks(self):
        """
        Cache and return all tracks from "My library".

        Each track will have "id" and "storeId" keys.
        """
        if self.cached_tracks:
            return self.cached_tracks
        data = self.mobile_client.get_all_songs()
        self.cached_tracks = Track.from_data(data, Source.library, True)
        self.parsed_songs.fire()

        return self.cached_tracks

    get_all_tracks_async = asynchronous(get_all_tracks)

    def get_stream_url(self, stream_id):
        """
        Returns playable stream URL of track by id.
        """
        return self.mobile_client.get_stream_url(stream_id)

    get_stream_url_async = asynchronous(get_stream_url)

    def increment_song_playcount(self, track_id):
        """
        increments the playcount of a song with a given `track_id` by one

        Args:
           track_id (`int`): The track id of the song to increment the playcount

        Returns:
           Nothing
        """
        gp.mobile_client.increment_song_playcount(track_id)

    increment_song_playcount_async = asynchronous(increment_song_playcount)

    @synchronized
    def get_all_user_station_contents(self, **_):
        """
              Return list of :class:`.Station` instances.
              """
        if self.cached_stations:
            return self.cached_stations
        self.get_all_tracks()

        self.cached_stations = Station.from_data(
            self.mobile_client.get_all_stations(), True)
        self.cached_stations.insert(0, IFLStation())
        return self.cached_stations

    get_all_user_station_contents_async = (
        asynchronous(get_all_user_station_contents))

    @synchronized
    def get_all_user_playlist_contents(self, **_):
        """
        Return list of :class:`.Playlist` instances.
        """
        if self.cached_playlists:
            return self.cached_playlists

        self.get_all_tracks()

        self.cached_playlists = Playlist.from_data(
            self.mobile_client.get_all_user_playlist_contents(), True)
        self.refresh_liked_songs()
        self.cached_playlists.insert(0, self.liked_songs)
        return self.cached_playlists

    get_all_user_playlist_contents_async = (
        asynchronous(get_all_user_playlist_contents))

    def refresh_liked_songs(self, **_):
        """
        Refresh the liked songs playlist
        """
        self.liked_songs.refresh_tracks(self.mobile_client.get_top_songs())

    refresh_liked_songs_async = asynchronous(refresh_liked_songs)

    def get_cached_tracks_map(self):
        """
        Return a dictionary of tracks where keys are strings with track IDs
        and values are :class:`.Track` instances.
        """
        return {track.id: track for track in self.cached_tracks}

    def get_track_by_id(self, any_id):
        """
        Return track by id or store_id.
        """
        for track in self.cached_tracks:
            if any_id in (track.id_, track.nid, track.store_id):
                return track
        return None

    def search(self, query):
        """
        Find tracks and return an instance of :class:`.SearchResults`.
        """
        results = self.mobile_client.search(query)
        return SearchResults.from_data(results)

    search_async = asynchronous(search)

    def add_to_my_library(self, track):
        """
        Add a track to my library.
        """
        result = self.mobile_client.add_store_tracks(track.id)
        if result:
            self.invalidate_caches()
        return result

    def remove_from_my_library(self, track):
        """
        Remove a track from my library.
        """
        result = self.mobile_client.delete_songs(track.id)
        if result:
            self.invalidate_caches()
        return result

    @property
    def is_authenticated(self):
        """
        Return True if user is authenticated on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_authenticated()

    @property
    def is_subscribed(self):
        """
        Return True if user is subscribed on Google Play Music, false otherwise.
        """
        return self.mobile_client.is_subscribed
Ejemplo n.º 4
0
class GoogleClient(object):
    def __init__(self):

        self.api = Mobileclient()

    def _get_playlist_id(self):
        """
        Gets user input for the playlist URL and attempts to parse it
        to get the Playlist ID
        """
        playlist_shared_url = input(
            "Enter the shared URL for the Google playlist:  ")
        try:
            playlist_id = parse.unquote(
                playlist_shared_url.split('playlist/')[1])
        except:
            playlist_id = None
        return playlist_id

    def _search_for_track(self, album, artist, track_name):
        """
        Searches Google for a track matching the provided album, artist, and name
        Returns the track ID if found, else None
        """
        query = track_name + ',' + artist + ',' + album
        result = self.api.search(query)
        song_hits = result['song_hits']
        # TODO this search has gotta get better...
        for h in song_hits:
            if h['track']['title'] == track_name:
                if h['track']['album'] == album or h['track'][
                        'artist'] == artist:
                    return h['track']['storeId']
        # TODO Return the best match if no full match is made
        return None

    def _delete_playlist(self, playlist_id):
        """
        Unfollow a playlist so that it does not appear in your account
        Mostly useful for testing, but maybe this should be used if 
        an exception occurs in the track add process?
        Return playlist ID
        """
        return (self.api.delete_playlist(playlist_id) == playlist_id)

    def authenticate(self):

        email = input("Enter your Google email address:  ")
        password = getpass.getpass(
            "Enter the password for your Google account:  ")
        # TODO store email locally
        return self.api.login(email, password, Mobileclient.FROM_MAC_ADDRESS)

    def get_playlist_tracks(self, playlist_id=None):
        # TODO Get playlist namea s well!!!

        if not playlist_id:
            playlist_id = self._get_playlist_id()

        tracks = self.api.get_shared_playlist_contents(playlist_id)
        tracks = [{
            "track": t['track']['title'],
            "album": t['track']['album'],
            "artist": t['track']['artist']
        } for t in tracks]
        return {"name": None, "tracks": tracks}

    def search_for_tracklist(self, tracklist):
        """
        Searches Google for a provided list of tracks
        Track list should be the format provided by indexing 'tracks' in the 
        dict returned from get_playlist_tracks()
        """
        found, not_found = [], []
        for t in tracklist:
            result = self._search_for_track(album=t['album'],
                                            artist=t['artist'],
                                            track_name=t['track'])
            if result:
                found.append(result)
            else:
                not_found.append(t)

        return {"found": found, "not_found": not_found}

    def create_playlist(self, playlist_name):
        """
        Create a new playlist with the provided name
        "prompt" param is mostly just here for testing
        Return the generated playlist ID
        """
        return self.api.create_playlist(name=playlist_name)

    def add_tracks_to_playlist(self, playlist_id, track_list):
        """
        Add all track in provided track_list to desired playlist
        track_list should be a list of Google track IDs, 
        provided by the 'found' index of search_for_tracks()
        """
        return self.api.add_songs_to_playlist(playlist_id=playlist_id,
                                              song_ids=track_list)