Authenticated sessions¶
Some Spotify data — lyrics, podcast transcripts — is only available to a
logged-in account. Rather than ask for your password (Spotify gates login
behind bot detection, CAPTCHAs, and 2FA, and a stored password is a liability),
SpotifyScraper captures the sp_dc session cookie from a real browser you log
into by hand, then persists it so later runs are headless.
Your sp_dc cookie is a credential
A captured sp_dc cookie is roughly a login session for your account.
Treat it like a password. SpotifyScraper never logs or prints it, stores
it in an owner-only (0600) file, and keeps it out of every repr and
exception message — but you are responsible for the machine it lives on. Use
it only with your own account.
Browser-assisted login¶
client.login() opens a headed Chromium window at the Spotify login page.
You sign in by hand; the library polls the browser's cookies until sp_dc
appears, wires it into the client, and (by default) saves it for reuse:
from spotify_scraper import SpotifyClient
with SpotifyClient() as client:
client.login() # opens a real browser; you log in
lyrics = client.get_lyrics("4uLU6hMCjMI75M1A2tKUQC")
login() needs the browser extra and a real display, so it is a
desktop/interactive step:
pip install "spotifyscraper[browser]"
playwright install chromium
Without the extra, login() raises a clear ImportError naming both install
steps. No username or password is ever collected — only the resulting cookie.
When you close the window, do not click "Log out" (that invalidates the
cookie you just captured).
By default login(reuse=True) skips the browser when a valid saved session
already exists — so the same login() call is safe to run repeatedly, opening
a browser only the first time (or after the session expires). Pass reuse=False
to force a fresh capture. The validity check is local (file present, securely
permissioned, parses, not past its known expiry, and — for the keyring backend —
the secret is still retrievable), so reuse never makes a network call.
The async client mirrors it: await client.login().
Reusing a saved session (headless)¶
Once a session is saved, reconnect later with no browser — ideal for servers and cron jobs:
from spotify_scraper import SpotifyClient
with SpotifyClient.from_saved_session() as client:
transcript = client.get_transcript("07gKzPFkbvGF0cHoeG7ARS")
from_saved_session() reads the saved cookie and forwards any client kwargs
(proxy, timeout, rate_limit, …). If no usable session exists it raises
SessionError. Browser-captured sessions also record the cookie's real
expiry, so tooling can tell you how long the session is good for.
Where the cookie is stored¶
The session lives in a per-user config directory (overridable with
SPOTIFYSCRAPER_CONFIG_DIR, then XDG_CONFIG_HOME, then the OS default). Two
backends are available, and the choice is always explicit — never inferred
from what happens to be installed:
store= |
Where the secret lives | Needs |
|---|---|---|
"file" (default) |
An owner-only (0600) JSON file | nothing |
"keyring" |
The OS keyring (Keychain / Credential Locker / Secret Service); only metadata in the file | the keyring extra |
client.login(store="keyring") # secret -> OS keyring
client = SpotifyClient.from_saved_session(store="keyring")
pip install "spotifyscraper[keyring]"
Requesting store="keyring" without the extra raises an ImportError naming
spotifyscraper[keyring]. On a host with no usable keyring (e.g. a headless
Linux box with no Secret Service), the keyring backend falls back to the file
backend with a warning rather than crashing.
Revoking a saved session¶
logout() (or clear_session()) removes the saved session locally; it is
idempotent when nothing is saved:
SpotifyClient.logout() # or logout(store="keyring")
This only deletes the local copy. To revoke the cookie everywhere, change
your Spotify password, which invalidates existing sp_dc cookies.
Inspecting the saved session¶
SpotifyClient.session_info() reports a saved session's status without
exposing the cookie — useful for a health check before a headless run. It
returns a cookie-free SessionInfo (exists, valid,
saved_at_ms, sp_dc_expires_ms, reason, plus a JSON-safe to_dict()) and
never raises for a missing, corrupt, insecure, or expired session:
info = SpotifyClient.session_info() # or session_info(store="keyring")
if not info.valid:
print("Need to log in:", info.reason)
spotify_scraper.auth.session.SessionInfo
dataclass
¶
SessionInfo(
exists: bool,
valid: bool,
saved_at_ms: int | None = None,
sp_dc_expires_ms: int | None = None,
reason: str | None = None,
)
A cookie-free snapshot of a saved session, safe to print or log.
Unlike :class:Session, this object never carries the sp_dc secret: it
holds only a validity verdict plus non-secret metadata, so it is safe to
surface to a CLI or a caller asking "is there a usable saved session, and how
long is it good for?". reason carries the existing cookie-free hint when
the session is absent, insecure, corrupt, or expired.
Attributes:
| Name | Type | Description |
|---|---|---|
exists |
bool
|
Whether a saved session file is present at all. |
valid |
bool
|
Whether the session exists, is securely permissioned (POSIX),
parses, and has not passed |
saved_at_ms |
int | None
|
When the session was saved (Unix ms), if known. |
sp_dc_expires_ms |
int | None
|
The cookie's expiry (Unix ms), if it was captured. |
reason |
str | None
|
A cookie-free explanation when the session is unusable. |
to_dict
¶
to_dict() -> dict[str, Any]
Return a JSON-safe dict of the verdict and metadata (no secret).
Unlike :meth:Session.to_dict, this is always cookie-free, so it is safe
to log, print, or serialize.
Knowing your account¶
With a session, get_account() returns the logged-in account's product state —
tier, country, and locale — and is_premium() is a convenience for the common
check:
from spotify_scraper import SpotifyClient
with SpotifyClient.from_saved_session() as client:
account = client.get_account()
print(account.product, account.country) # e.g. "premium" CA
if client.is_premium():
...
get_account() needs cookies (it raises AuthenticationError immediately
otherwise) and takes no argument — the product-state body is a flat per-account
object. The derived is_premium is a property, so it is excluded from
Account.to_dict() (which mirrors the wire body).
Command line¶
spotifyscraper login # reuse a valid session, else open the browser
spotifyscraper login --no-reuse # force a fresh browser capture
spotifyscraper login --store keyring # store the secret in the OS keyring
spotifyscraper session # print the saved session's status (no cookie)
spotifyscraper logout # clear the saved session
Only paths and cookie-free status are printed — never the cookie.