From 298e1aab0d62b72bfc858b7855074ca129d9b5f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20Sch=C3=BCle?=
Date: Sat, 18 Apr 2026 18:07:27 +0200
Subject: [PATCH 1/4] docs(run): source env vars before running
---
.gitignore | 3 ++-
README.md | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index 8a3f9a3..2b2629b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.venv
.idea
-qr_codes
\ No newline at end of file
+qr_codes
+.env
\ No newline at end of file
diff --git a/README.md b/README.md
index 2034fb5..509d4d5 100644
--- a/README.md
+++ b/README.md
@@ -39,9 +39,9 @@ If you find this DIY Hitster project useful, consider giving it a star :) !
## ▶️ 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.
From 2c0cc52d7c4d67728acf8172e2f19212c57c57ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20Sch=C3=BCle?=
Date: Sat, 18 Apr 2026 18:08:04 +0200
Subject: [PATCH 2/4] conf(poetry): do not build package when installing deps
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
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"
From 5dd4f91c97fb9bc37603c5b27ad5e8f88c639fa9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20Sch=C3=BCle?=
Date: Sat, 18 Apr 2026 18:33:13 +0200
Subject: [PATCH 3/4] fix(api): add oauth & use "items" endpoint
"tracks" endpoint is deprecated
see https://www.reddit.com/r/spotifyapi/comments/1rbxfwa/spotify_web_api_returns_403_forbidden_for/
---
README.md | 13 +++++++++++
main.py | 66 +++++++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 67 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index 509d4d5..a6a0080 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,12 @@ 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
@@ -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"))
From 52d4098ac5979865d57d880dda197e714e290ac3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20Sch=C3=BCle?=
Date: Sat, 18 Apr 2026 18:34:34 +0200
Subject: [PATCH 4/4] conf(gitignore): add some more cache & result data
---
.gitignore | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 2b2629b..3c57e3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,9 @@
.venv
.idea
qr_codes
-.env
\ No newline at end of file
+.env
+.cache
+.spotify_token_cache
+hitster.pdf
+overview.pdf
+songs.json