Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ poetry add git+https://github.com/Matatika/tap-spotify

### Accepted Config Options

Name | Required | Default | Description
--- | --- | --- | ---
`client_id` | Yes | | Your `tap-spotify` app client ID
`client_secret` | Yes | | Your `tap-spotify` app client secret
`refresh_token` | Yes | | Your `tap-spotify` app refresh token
Name | Description
--- | ---
`client_id` | Your `tap-spotify` app client ID
`client_secret` | Your `tap-spotify` app client secret
`refresh_token` | Your `tap-spotify` app refresh token
`access_token` | Your `tap-spotify` app access token

### Valid Configurations

Option(s) | Description
--- | ---
`access_token` | Use supplied access token
`client_id`<br>`client_secret`<br>`refresh_token` | Use OAuth credentials to generate access tokens internally
`client_id`<br>`client_secret`<br>`refresh_token`<br>`access_token` | Use supplied access token and fallback to OAuth credentials if invalid or expired

A full list of supported settings and capabilities for this
tap is available by running:
Expand All @@ -38,11 +47,11 @@ tap-spotify --about

Before using `tap-spotify`, you will need to create an [app](https://developer.spotify.com/documentation/web-api/concepts/apps) from your [Spotify developer dashboard](https://developer.spotify.com/dashboard). We recommend restricting your use of this app to `tap-spotify` only. Provide an name, description and a redirect URI of `https://alecchen.dev/spotify-refresh-token` (explained below).

#### Get a Refresh Token
Use [this web app](https://alecchen.dev/spotify-refresh-token?scope=user-top-read&scope=user-library-read) made by [Alec Chen](https://alecchen.dev/) to get a refresh token with your Spotify app credentials:
#### Get an Access Token and/or Refresh Token
Use [this web app](https://alecchen.dev/spotify-refresh-token?scope=user-top-read&scope=user-library-read) made by [Alec Chen](https://alecchen.dev/) to get an access token and/or refresh token with your Spotify app credentials:
- Provide your app client ID and secret in the appropriate fields
- Click 'Submit' and follow the Spotify login flow
- Copy the refresh token
- Copy the access token and/or refresh token

THe following token scopes are required (and are pre-selected for you when following the above web app link):
- [`user-top-read`](https://developer.spotify.com/documentation/web-api/concepts/scopes#user-top-read)
Expand Down
2 changes: 2 additions & 0 deletions meltano.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ plugins:
kind: password
- name: refresh_token
kind: password
- name: access_token
kind: password
loaders:
- name: target-jsonl
variant: andyh1203
Expand Down
6 changes: 3 additions & 3 deletions tap_spotify/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ class SpotifyAuthenticator(OAuthAuthenticator, metaclass=SingletonMeta):
def oauth_request_body(self):
return {
"grant_type": "refresh_token",
"refresh_token": self.config["refresh_token"],
"client_id": self.config["client_id"],
"client_secret": self.config["client_secret"],
"refresh_token": self.config.get("refresh_token"),
"client_id": self.client_id,
"client_secret": self.client_secret,
}

@classmethod
Expand Down
42 changes: 41 additions & 1 deletion tap_spotify/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""REST client handling, including SpotifyStream base class."""

from datetime import datetime
from typing import Optional
from urllib.parse import ParseResult, parse_qsl

from memoization import cached
from singer_sdk.exceptions import FatalAPIError, RetriableAPIError
from singer_sdk.streams import RESTStream

from tap_spotify.auth import SpotifyAuthenticator
Expand All @@ -19,11 +21,49 @@ class SpotifyStream(RESTStream):
@property
@cached
def authenticator(self):
return SpotifyAuthenticator.create_for_stream(self)
authenticator = SpotifyAuthenticator.create_for_stream(self)

access_token = self.config.get("access_token")

if access_token:
authenticator.access_token = access_token
# indicate user-supplied access token
authenticator.last_refreshed = datetime.now()

return authenticator

def get_new_paginator(self):
return BodyLinkPaginator()

def get_url_params(self, context, next_page_token: Optional[ParseResult]):
params = super().get_url_params(context, next_page_token)
return dict(parse_qsl(next_page_token.query)) if next_page_token else params

def _request(self, prepared_request, context):
# reapply authenticator if retrying with oauth credentials
if not self.authenticator.last_refreshed:
prepared_request.prepare_auth(self.authenticator)

self.logger.info(prepared_request.headers)
return super()._request(prepared_request, context)

def validate_response(self, response):
try:
super().validate_response(response)
except FatalAPIError as e:
if (
response.status_code == 401
# attempted with user-supplied access token
and not self.authenticator.expires_in
# oauth credentials provided
and self.authenticator.client_id
and self.authenticator.client_secret
and self.authenticator.refresh_token
):
# retry with oauth credentials
self.authenticator.last_refreshed = None

msg = self.response_error_message(response)
raise RetriableAPIError(msg, response)

raise e
9 changes: 6 additions & 3 deletions tap_spotify/tap.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,23 @@ class TapSpotify(Tap):
th.Property(
"client_id",
th.StringType,
required=True,
description="App client ID",
),
th.Property(
"client_secret",
th.StringType,
required=True,
secret=True,
description="App client secret",
),
th.Property(
"refresh_token",
th.StringType,
required=True,
secret=True,
description="Refresh token",
),
th.Property(
"access_token",
th.StringType,
secret=True,
description="Refresh token",
),
Expand Down