Example #1
0
class SpotifyApi(object):
    """Interface to make API calls."""

    def __init__(self, username):
        self.username = username
        """Email or user id."""

        self.auth = Authenticator(username)
        """Handles OAuth 2.0 authentication."""

        self._uri_cache = UriCache(username)
        """Cache of Spotify URIs."""

        self.me = None
        """The Spotify user information."""

        # Get authorization.
        self.auth.authenticate()

        # Get user information and validate it.
        self.me = self.get_api_v1("me")

        if not self.me:
            raise RuntimeError("Could not get account information.")

        if (self.me['email'] != username) and (self.me['id'] != username):
            raise RuntimeError(
                "\n\n\nInvalid email/user id entered!\n"
                "You entered: {}\n"
                "You signed in as: {} (email), {} (user id)".format(username,
                                                                    self.me['email'],
                                                                    self.me['id'])
            )

    def get_email(self):
        return self.me['email']

    def get_id(self):
        return self.me['id']

    def get_display_name(self):
        return self.me['display_name']

    def get_username(self):
        return self.username

    def is_premium(self):
        return self.me['product'] == "premium"

    def get_market(self):
        return self.me['country']

    @common.async
    def play(self, track=None, context_uri=None, uris=None, device=None):
        """Play a Spotify track.

        Args:
            track (str, int): The track uri or position.
            context_uri (str): The context uri.
            uris (iter): Collection of uris to play.
            device (Device): A device to play.
        """
        data = {}

        # Special case when playing a set of uris.
        if uris:
            data['uris'] = uris
            if common.is_int(track):
                data["offset"] = {"position": track}
        elif context_uri:
            # Set the context that we are playing in.
            data["context_uri"] = context_uri

            if common.is_int(track):
                data["offset"] = {"position": track}
            elif isinstance(track, basestring):
                data["offset"] = {"uri": track}

        # No context given, just play the track.
        elif track is not None and not context_uri:
            if isinstance(track, basestring) and track.startswith("spotify:track"):
                data['uris'] = [track]

        params = {}
        if device and device['id']:
            params["device_id"] = device['id']

        self.put_api_v1("me/player/play", params, data)

    @common.async
    def transfer_playback(self, device, play=False):
        """Transfer playback to a different Device.

        Args:
            device (Device): The Device to transfer playback to.
            play (bool): Whether to ensure playback happens on new device.
        """
        data = {"device_ids": [device['id']],
                "play": play}
        self.put_api_v1("me/player", data=data)

    @common.async
    def pause(self):
        """Pause the player."""
        self.put_api_v1("me/player/pause")

    @common.async
    def next(self):
        """Play the next song."""
        self.post_api_v1("me/player/next")

    @common.async
    def previous(self):
        """Play the previous song."""
        self.post_api_v1("me/player/previous")

    @common.async
    def shuffle(self, shuffle):
        """Set the player to shuffle.

        Args:
            shuffle (bool): Whether to shuffle or not.
        """
        q = urllib.urlencode({"state": shuffle})
        url = "me/player/shuffle"
        self.put_api_v1(url, q)

    @common.async
    def repeat(self, repeat):
        """Set the player to repeat.

        Args:
            repeat (bool): Whether to repeat or not.
        """
        q = urllib.urlencode({"state": repeat})
        url = "me/player/repeat"
        self.put_api_v1(url, q)

    @common.async
    def volume(self, volume):
        """Set the player volume.

        Args:
            volume (int): Volume level. 0 - 100 (inclusive).
        """
        q = urllib.urlencode({"volume_percent": volume})
        url = "me/player/volume"
        self.put_api_v1(url, q)

    def get_player_state(self):
        """Returns the player state.

        The following information is returned:
            device, repeat_state, shuffle_state, context,
            timestamp, progress_ms, is_playing, item.

        Returns:
            dict: Information containing the player state.
        """
        return self.get_api_v1("me/player")

    def get_currently_playing(self):
        """Get the currently playing Track.

        Returns:
            Track: The currently playing Track or NoneTrack.
        """
        playing = self.get_api_v1("me/player/currently-playing")
        if playing:
            track = playing['item']
            if track:
                playing.update(track)
                return Track(playing)
            else:
                return NoneTrack
        else:
            return NoneTrack

    def get_user_info(self, user_id):
        """Returns a dict of the a users info.

        The following information is returned:
            display_name, external_urls, followers, href, id,
            images, type, uri.

        user_id (str): The user_id.

        Returns:
            dict: Users information.
        """
        return self.get_api_v1("users/{}".format(user_id))

    def get_devices(self):
        """Return a list of devices with Spotify players running.

        Returns:
            list: The Devices.
        """
        results = self.get_api_v1("me/player/devices")
        if results and "devices" in results:
            return tuple(Device(device) for device in results['devices'])
        else:
            return []

    def search(self, types, query, limit=20):
        """Calls Spotify's search api.

        Args:
            types (tuple): Strings of the type of search (i.e, 'artist',
                'track', 'album')
            query (str): The search query.
            limit (int): Limit of the amount of results to return per
                type of search in 'types'.

        Returns:
            dict: Collection of Artist, Album, and Track objects.
        """
        type_str = ",".join(types)
        params = {'type': type_str,
                  'q': query,
                  'limit': limit}

        results = self.get_api_v1("search", params)

        cast = {
            'artists': Artist,
            'tracks': Track,
            'albums': Album,
        }

        combined = []
        if results:
            for type in types:
                # Results are plural (i.e, 'artists', 'albums', 'tracks')
                type = type + 's'
                combined.extend([cast[type](info) for info in results[type]['items']])

        return combined

    @uri_cache
    def get_albums_from_artist(self, artist,
                               type=("album", "single", "appears_on", "compilation"),
                               market=None):
        """Get Albums from a certain Artist.

        Args:
            artist (Artist): The Artist.
            type (iter): Which types of albums to return.
            market (str): The market. Default is None which means use the account.
        Returns:
            tuple: The Albums.
        """
        q = {"include_groups": ",".join(type),
             "market": market or self.get_market(),
             "limit": 50}
        url = "artists/{}/albums".format(artist['id'])
        page = self.get_api_v1(url, q)
        albums = self.extract_page(page)

        return tuple(Album(album) for album in albums)

    @uri_cache
    def get_top_tracks_from_artist(self, artist, market=None):
        """Get top tracks from a certain Artist.

        This also returns a pseudo-track to play the Artist context.

        Args:
            artist (Artist): The Artist to get Tracks from.
            market (str): The market. Default is None which means use the account.

        Returns:
            tuple: The Tracks.
        """
        q = {"country": market or self.get_market()}
        url = "artists/{}/top-tracks".format(artist['id'])
        result = self.get_api_v1(url, q)

        if result:
            return tuple(Track(t) for t in result["tracks"])
        else:
            return []

    @uri_cache
    def get_selections_from_artist(self, artist, progress=None):
        """Return the selection from an Artist.

        This includes the top tracks and albums from the artist.

        Args:
            artist (Artist): The Artist.
            progress (Progress): Progress associated with this call.

        Returns:
            iter: The Tracks and Albums.
        """
        selections = []

        selections.extend(self.get_top_tracks_from_artist(artist))
        if selections:
            progress.set_percent(0.5)

        selections.extend(self.get_albums_from_artist(artist))
        if progress:
            progress.set_percent(1)

        return selections

    @uri_cache
    def get_all_tracks_from_artist(self, artist, progress=None):
        """Return all tracks from an Artist.

        This includes the top tracks and albums from the artist.

        Args:
            artist (Artist): The Artist.
            progress (Progress): Progress associated with this call.

        Returns:
            iter: The Tracks.
        """
        albums = self.get_albums_from_artist(artist)
        if albums:
            n = len(albums)
            tracks = []
            for i, a in enumerate(albums):
                for t in self.get_tracks_from_album(a):
                    tracks.append(Track(t))
                if progress:
                    progress.set_percent(float(i)/n)
            tracks = (t for t in tracks if artist['name'] in str(t))
            return tuple(tracks)

    @uri_cache
    def get_tracks_from_album(self, album, progress=None):
        """Get Tracks from a certain Album.

        Args:
            album (Album): The Album to get Tracks from.

        Returns:
            tuple: The Tracks.
        """
        q = {"limit": 50}
        url = "albums/{}/tracks".format(album['id'])
        page = self.get_api_v1(url, q)
        tracks = []
        for track in self.extract_page(page, progress):
            track['album'] = album
            tracks.append(Track(track))
        return tuple(tracks)

    @uri_cache
    def get_tracks_from_playlist(self, playlist, progress=None):
        """Get Tracks from a certain Playlist.

        Args:
            playlist (Playlist): The Playlist to get Tracks from.
            progress (Progress): Progress associated with this call.

        Returns:
            tuple: The Tracks.
        """
        # Special case for the "Saved" Playlist
        if playlist['uri'] == common.SAVED_TRACKS_CONTEXT_URI:
            return self._get_saved_tracks(progress)
        else:
            q = {"limit": 50}
            url = "users/{}/playlists/{}/tracks".format(playlist['owner']['id'],
                                                        playlist['id'])
            page = self.get_api_v1(url, q)
            result = [Track(track["track"]) for track in self.extract_page(page, progress)]

        return tuple(result)

    @uri_cache
    def convert_context(self, context, progress=None):
        """Convert a Context to an Album, Playlist, or Artist.

        Args:
            context (dict): The Context to convert from.
            progress (Progress): Progress associated with this call.

        Returns:
            SpotifyObject: Album, Artist, or Playlist.
        """
        context_type = context["type"]
        if context_type == "artist":
             return self.get_artist_from_context(context)
        elif context_type == common.ALL_ARTIST_TRACKS_CONTEXT_TYPE:
            return self.get_artist_from_context(context)
        elif context_type == "album":
            return self.get_album_from_context(context)
        elif context_type == "playlist":
            return self.get_playlist_from_context(context)

    @uri_cache
    def get_artist_from_context(self, context):
        """Return an Artist from a Context.

        Args:
            context (dict): The Context to convert from.

        Returns:
            Artist: The Artist.
        """
        artist_id = id_from_uri(context["uri"])
        result = self.get_api_v1("artists/{}".format(artist_id))
        return Artist(result or {})

    @uri_cache
    def get_album_from_context(self, context):
        """Return an Album from a Context.

        Args:
            context (dict): The Context to convert from.

        Returns:
            Album: The Album.
        """
        album_id = id_from_uri(context["uri"])
        result = self.get_api_v1("albums/{}".format(album_id))
        return Album(result or {})

    @uri_cache
    def get_playlist_from_context(self, context):
        """Return an Playlist from a Context.

        Args:
            context (dict): The Context to convert from.

        Returns:
            Playlist: The Playlist.
        """
        if context["uri"] == common.SAVED_TRACKS_CONTEXT_URI:
            # TODO: Consider creating a common/factory function for
            # obtaining the Saved PLaylist.
            return Playlist({
                "uri":common.SAVED_TRACKS_CONTEXT_URI,
                "name": "Saved"
                })

        playlist_id = id_from_uri(context["uri"])
        result = self.get_api_v1("playlists/{}".format(playlist_id))
        return Playlist(result or {})

    def add_track_to_playlist(self, track, playlist):
        """Add a Track to a Playlist.

        Args:
            track (Track): The Track to add.
            playlist (Playlist): The Playlist to add the Track to.

        Returns:
            tuple: The new set of Tracks with the new Track added.
        """
        # Add the track.
        if playlist['uri'] == common.SAVED_TRACKS_CONTEXT_URI:
            q = {"ids": [track['id']]}
            url = "me/tracks"
            self.put_api_v1(url, q)
        else:
            q = {"uris": [track['uri']]}
            url = "playlists/{}/tracks".format(playlist['id'])
            self.post_api_v1(url, q)

        # Clear out current Cache.
        return self.get_tracks_from_playlist(playlist, force_clear=True)

    def _get_saved_tracks(self, progress=None):
        """Get the Tracks from the "Saved" songs.

        Args:
            progress (Progress): Progress associated with this call.

        Returns:
            tuple: The Tracks.
        """
        q = {"limit": 50}
        url = "me/tracks"
        page = self.get_api_v1(url, q)
        return tuple(Track(saved["track"]) for saved in self.extract_page(page, progress))

    def get_user(self, user_id=None):
        """Return a User from an id.

        Args:
            user_id (str): The user id.

        Returns:
            User: The User.
        """
        if not user_id:
            user_id = self.get_id()

        result = self.get_api_v1("users/{}".format(user_id))
        if result:
            return User(result)
        else:
            return {}

    @uri_cache
    def get_user_playlists(self, user, progress=None):
        """Get the Playlists from the current user.

        Args:
            user (User): The User.
            progress (Progress): Progress associated with this call.

        Return:
            tuple: The Playlists.
        """
        q = {"limit": 50}
        url = "users/{}/playlists".format(user['id'])
        page = self.get_api_v1(url, q)
        return tuple([Playlist(p) for p in self.extract_page(page, progress)])

    def extract_page(self, page, progress=None):
        """Extract all items from a page.

        Args:
            page (dict): The page object.
            progress (Progress): Progress associated with this call.

        Returns:
            list: All of the items.
        """

        if page and "items" in page:
            i, n = 0, page['total']
            lists = []
            lists.extend(page['items'])
            while page['next'] is not None:
                page = self.get_api_v1(page['next'].split('/v1/')[-1])
                lists.extend(page['items'])
                if progress:
                    progress.set_percent(float(len(lists))/n)
            return lists
        else:
            return {}

    @needs_authentication
    def get_api_v1(self, endpoint, params=None):
        """Spotify v1 GET request.

        Args:
            endpoint (str): The API endpoint.
            params (dict): Query parameters (Default is None).

        Returns:
            dict: The JSON information.
        """
        headers = {"Authorization": "%s %s" % (self.auth.token_type, self.auth.access_token)}
        url = "https://api.spotify.com/v1/{}".format(endpoint)
        resp = requests.get(url, params=params, headers=headers)
        resp.raise_for_status()

        data = json.loads(common.ascii(resp.text)) if resp.text else {}
        if not data:
            logger.info("GET returned no data")

        return data

    @needs_authentication
    def put_api_v1(self, endpoint, params=None, data=None):
        """Spotify v1 PUT request.

        Args:
            endpoint (str): The API endpoint.
            params (dict): Query parameters (Default is None).
            data (dict): Body data (Default is None).

        Returns:
            Reponse: The HTTP Reponse.
        """
        headers = {"Authorization": "%s %s" % (self.auth.token_type, self.auth.access_token),
                   "Content-Type": "application/json"}
        api_url = "https://api.spotify.com/v1/{}".format(endpoint)
        resp = requests.put(api_url, headers=headers, params=params, json=data)
        resp.raise_for_status()
        return resp

    @needs_authentication
    def post_api_v1(self, endpoint, params=None):
        """Spotify v1 POST request.

        Args:
            endpoint (str): The API endpoint.
            params (dict): Query parameters (Default is None).

        Returns:
            Reponse: The HTTP Reponse.
        """
        headers = {"Authorization": "%s %s" % (self.auth.token_type, self.auth.access_token),
                   "Content-Type": "application/json"}
        api_url = "https://api.spotify.com/v1/{}".format(endpoint)
        resp = requests.post(api_url, headers=headers, params=params)
        resp.raise_for_status()
        return common.ascii(resp.text)