diff --git a/.gitignore b/.gitignore index 8a3f9a3..3c57e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ .venv .idea -qr_codes \ No newline at end of file +qr_codes +.env +.cache +.spotify_token_cache +hitster.pdf +overview.pdf +songs.json diff --git a/README.md b/README.md index 2034fb5..a6a0080 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,18 @@ If you find this DIY Hitster project useful, consider giving it a star :) ! export CLIENT_SECRET="your_client_secret" export PLAYLIST_ID="your_playlist_id" ``` + - If your playlist is private/collaborative, use user auth mode: + ```bash + export SPOTIFY_AUTH_MODE="user" + export SPOTIFY_REDIRECT_URI="http://127.0.0.1:8888/callback" + ``` + Then add that redirect URI in your Spotify app settings. ## ▶️ Usage -1. Run the `main.py` script: +1. Run the `main.py` script (put the exports into a .env fie first): ```bash - poetry run python main.py + source .env && poetry run python main.py ``` 2. Print the `hitster.pdf` file: - Open the `hitster.pdf` file and duplex print it. @@ -62,3 +68,10 @@ If you find this DIY Hitster project useful, consider giving it a star :) ! - Spotify Playlist: Change the `PLAYLIST_ID` environment variable to use a different Spotify playlist. - Card Dimensions: Change the `card_size`, `rows`, and `cols` variables in `hitster.typ` to adjust the card dimensions and page layout. - Marking Size: Change the `marking_padding` to adjust the space for cutting. + +## Troubleshooting + +- `403 Forbidden` while reading a playlist usually means your playlist is not publicly accessible to client-credentials auth. +- Fix options: + - make the playlist public, or + - use `SPOTIFY_AUTH_MODE="user"` with `SPOTIFY_REDIRECT_URI` and log in once in the browser. diff --git a/main.py b/main.py index 816efb2..f76d0b5 100644 --- a/main.py +++ b/main.py @@ -7,9 +7,11 @@ from matplotlib.backends.backend_pdf import PdfPages import qrcode +import requests import spotipy import typst from spotipy import SpotifyClientCredentials +from spotipy.oauth2 import SpotifyOAuth import qrcode.image.svg import logging import calendar @@ -25,6 +27,42 @@ def get_env_var(key): return value +def get_env_var_optional(key): + return os.getenv(key) + + +def create_spotify_client(): + auth_mode = get_env_var_optional("SPOTIFY_AUTH_MODE") or "client_credentials" + + if auth_mode == "user": + # User auth can access private/collaborative playlists. + client_id = get_env_var("CLIENT_ID") + client_secret = get_env_var("CLIENT_SECRET") + redirect_uri = get_env_var("SPOTIFY_REDIRECT_URI") + scope = ( + get_env_var_optional("SPOTIFY_SCOPE") + or "playlist-read-private playlist-read-collaborative" + ) + + return spotipy.Spotify( + auth_manager=SpotifyOAuth( + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + scope=scope, + cache_path=".spotify_token_cache", + open_browser=True, + ) + ) + + return spotipy.Spotify( + auth_manager=SpotifyClientCredentials( + client_id=get_env_var("CLIENT_ID"), + client_secret=get_env_var("CLIENT_SECRET"), + ) + ) + + def resolve_date(date_str): date_parts = date_str.split("-")[::-1] parts = [""] * (3 - len(date_parts)) + date_parts @@ -38,12 +76,21 @@ def resolve_date(date_str): def get_playlist_songs(sp, playlist_id): songs = [] - results = sp.playlist_tracks(playlist_id) + next_url = f"https://api.spotify.com/v1/playlists/{playlist_id}/items?limit=100&additional_types=track" + + while next_url: + token = sp.auth_manager.get_access_token(as_dict=False) + response = requests.get( + next_url, + headers={"Authorization": f"Bearer {token}"}, + timeout=30, + ) + response.raise_for_status() + results = response.json() - while results: - for item in results["items"]: - track = item["track"] - if track: + for item in results.get("items", []): + track = item.get("track") or item.get("item") + if track and track.get("type") == "track": day, month, year = resolve_date(track["album"]["release_date"]) songs.append( { @@ -58,7 +105,7 @@ def get_playlist_songs(sp, playlist_id): } ) - results = sp.next(results) if results["next"] else None + next_url = results.get("next") random.shuffle(songs) return songs @@ -93,12 +140,7 @@ def generate_overview_pdf(songs, output_pdf): def main(): - sp = spotipy.Spotify( - auth_manager=SpotifyClientCredentials( - client_id=get_env_var("CLIENT_ID"), - client_secret=get_env_var("CLIENT_SECRET"), - ) - ) + sp = create_spotify_client() logging.info("Starting Spotify song retrieval") songs = get_playlist_songs(sp, get_env_var("PLAYLIST_ID")) diff --git a/pyproject.toml b/pyproject.toml index 216313e..cdc2c07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "" authors = ["fjlein "] readme = "README.md" +package-mode = false [tool.poetry.dependencies] python = "^3.12"