{
  "markdown": "# SpotifyScraper\n\n[![Live demo](https://img.shields.io/badge/Live_demo-try_it_now-1DB954?logo=spotify&logoColor=white)](https://aliakhtari.com/spotify/)\n[![PyPI version](https://img.shields.io/pypi/v/spotifyscraper.svg)](https://pypi.org/project/spotifyscraper/)\n[![Python versions](https://img.shields.io/pypi/pyversions/spotifyscraper.svg)](https://pypi.org/project/spotifyscraper/)\n[![Downloads](https://static.pepy.tech/badge/spotifyscraper/month)](https://pepy.tech/project/spotifyscraper)\n[![CI](https://github.com/AliAkhtari78/SpotifyScraper/actions/workflows/ci.yml/badge.svg)](https://github.com/AliAkhtari78/SpotifyScraper/actions/workflows/ci.yml)\n[![Docs](https://readthedocs.org/projects/spotifyscraper/badge/?version=latest)](https://spotifyscraper.readthedocs.io)\n[![Container](https://img.shields.io/badge/ghcr.io-container-2496ED?logo=docker&logoColor=white)](https://github.com/AliAkhtari78/SpotifyScraper/pkgs/container/spotifyscraper)\n[![Maintained with Claude Code](https://img.shields.io/badge/maintained%20with-Claude%20Code-d97757?logo=anthropic&logoColor=white)](https://github.com/AliAkhtari78/SpotifyScraper#reliability--maintenance)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/AliAkhtari78/SpotifyScraper/blob/master/LICENSE)\n[![GitHub stars](https://img.shields.io/github/stars/AliAkhtari78/SpotifyScraper?style=flat&logo=github&color=1DB954)](https://github.com/AliAkhtari78/SpotifyScraper/stargazers)\n\n**Extract public Spotify data — tracks, albums, artists, playlists, and podcasts — without the official API or an API key.**\n\n> 🎧 **[Try it live in your browser →](https://aliakhtari.com/spotify/)** — paste any Spotify link and watch SpotifyScraper pull typed data, cover art, and a preview, with the exact Python that does it. ([How it was built](https://aliakhtari.com/work/spotify-scraper/).)\n\nSpotifyScraper bootstraps an anonymous token from Spotify's own public embed\npages and reads the same JSON endpoints the web player uses, returning typed,\nimmutable models. v3 is a ground-up rewrite focused on reliability and a clean,\nmodern API. Public data needs no login; the opt-in **logged-in** features\n(lyrics, podcast transcripts, and account info) add your own Spotify `sp_dc`\ncookie — never a password or an API key.\n\n> **Upgrading from v2?** See the [migration guide](https://spotifyscraper.readthedocs.io/en/latest/migration/). The previous line lives on the [`v2.x` branch](https://github.com/AliAkhtari78/SpotifyScraper/tree/v2.x).\n\n## SpotifyScraper vs. the official API ([spotipy](https://github.com/spotipy-dev/spotipy))\n\n`spotipy` wraps Spotify's **official Web API** — the right choice when you need to\nwrite to a user's account or read private/library data. SpotifyScraper reads the\n**public** data the web player already exposes, so it skips the setup entirely.\n\n|                                   | **SpotifyScraper** | **spotipy** (official API) |\n| --------------------------------- | :----------------: | :------------------------: |\n| API key / app registration        |    ❌ not needed    |        ✅ required          |\n| OAuth flow                         |    ❌ not needed    |   ✅ required for most data |\n| Rate-limit quota / billing         |        none        |       Spotify quota        |\n| Sync **and** async                 |         ✅          |         sync only          |\n| Fully typed, immutable models      |         ✅          |          partial           |\n| Lyrics & podcast transcripts       |    ✅ (cookie)      |            ❌              |\n| MCP server for Claude / LLM agents |         ✅          |            ❌              |\n| Write / playback / private data    | ❌ (read-only public) |          ✅             |\n\nUse **spotipy** for authenticated writes and private, market-accurate data; use\n**SpotifyScraper** for fast, key-free access to public metadata, lyrics, and\npreviews — plus a drop-in **MCP server** for AI agents.\n\n> **Hit by the official-API deprecations?** Spotify's `audio-features`,\n> `recommendations`, and `related-artists` endpoints have returned `403` for new\n> apps since Nov 2024. SpotifyScraper still returns **related artists and\n> recommendations** with no API key. (It can't bring back `audio-features` —\n> Spotify removed that data entirely, from every tool.)\n\n## Install\n\n```bash\npip install spotifyscraper                 # core (only depends on httpx)\npip install \"spotifyscraper[media]\"        # + cover/preview embedding (mutagen)\npip install \"spotifyscraper[browser]\"      # + Playwright browser fallback & login\npip install \"spotifyscraper[cli]\"          # + the spotifyscraper command-line tool\npip install \"spotifyscraper[keyring]\"      # + store the login cookie in the OS keyring\npip install \"spotifyscraper[mcp]\"          # + the spotifyscraper-mcp MCP server for LLM hosts\npip install \"spotifyscraper[all]\"          # everything\n```\n\nPython 3.10+.\n\n## Quickstart\n\n```python\nfrom spotify_scraper import SpotifyClient\n\nwith SpotifyClient() as client:\n    track = client.get_track(\"https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC\")\n    print(track.name, \"—\", track.artists[0].name)\n    print(track.duration_ms, \"ms |\", track.preview_url)\n\n    print(track.to_dict())          # JSON-safe dict, if you prefer dicts\n```\n\nEvery entity has its own method — `get_track`, `get_album`, `get_artist`,\n`get_playlist`, `get_episode`, `get_show` — each accepting a URL, URI, or bare\nID.\n\n### Async\n\n```python\nimport asyncio\nfrom spotify_scraper import AsyncSpotifyClient\n\nasync def main():\n    async with AsyncSpotifyClient() as client:\n        track, album = await asyncio.gather(\n            client.get_track(\"4uLU6hMCjMI75M1A2tKUQC\"),\n            client.get_album(\"6N9PS4QXF1D0OWPk0Sxtb4\"),\n        )\n        print(track.name, \"|\", album.name)\n\nasyncio.run(main())\n```\n\n### Download a cover and preview\n\n```python\nfrom spotify_scraper import SpotifyClient\n\nwith SpotifyClient() as client:\n    track = client.get_track(\"4uLU6hMCjMI75M1A2tKUQC\")\n    client.download_cover(track, dest=\"covers/\")\n    client.download_preview(track, dest=\"previews/\", embed_cover=True)  # needs [media]\n```\n\n### Localized display names\n\nPass `locale` — a BCP-47 **language** tag: a bare language subtag (`\"de\"`,\n`\"ja\"`) or a language-region tag (`\"ja-JP\"`) — to localize the *language* of\ndisplay names. Set it per client or override it per call:\n\n```python\nwith SpotifyClient(locale=\"ja-JP\") as client:        # default for every call\n    track = client.get_track(\"4uLU6hMCjMI75M1A2tKUQC\")\n    other = client.get_track(\"4uLU6hMCjMI75M1A2tKUQC\", locale=\"de-DE\")  # per-call wins\n```\n\nIt is sent as the `Accept-Language` header and changes only how names are\n*spelled*. It is **not** a country/market code — a bare `\"US\"` is meaningless as\na language and is ignored — and it does **not** filter regional **availability**\nor vary **preview URLs**: anonymous Spotify resolves country from the request IP,\nand its pathfinder silently ignores a `market` variable. True market/availability\nfiltering requires the authenticated Web API, which this library does not\nimplement; for region-specific results, point the client's `proxy` at the target\nregion. See the\n[localization guide](https://spotifyscraper.readthedocs.io/en/latest/guides/localization/).\n\n## Features\n\n- **All core entities + podcasts** — tracks, albums, artists, playlists, shows, episodes.\n- **Search** across every entity type, returning one typed `SearchResults`.\n- **Charts & discovery** — editorial charts, related artists, full paginated discography, and album recommendations.\n- **Cover colors & Canvas** — extract an artwork's theming palette and download a track's looping Canvas video.\n- **Credits & concerts** — performers/writers/producers and an artist's upcoming live events.\n- **Public user profiles** — `get_user()` (name, follower counts, public playlists).\n- **MCP server** — expose everything to Claude/LLMs via `spotifyscraper-mcp` (batch tools + a one-call `get_track_visuals`); also ships as a container on ghcr.io.\n- **Localized display names** — pass a BCP-47 language tag (`locale`) to set the language of names.\n- **Lyrics & podcast transcripts** — cookie-authenticated, time-synced, one token for both.\n- **Browser-assisted login + session persistence** — log in once, then run headless (no stored passwords).\n- **Account-aware** — `get_account()` / `is_premium()`, plus cookie-free `session_info()`.\n- **Batch helpers** — plural `get_*s([...])` with partial-failure-safe results and managed concurrency.\n- **Sync & async** clients sharing one sans-io core.\n- **Typed, frozen models** with JSON-safe `to_dict()` / `from_dict()`.\n- **Two-tier resilience** — Spotify's GraphQL API with automatic fallback to the embed page.\n- **One core dependency** (`httpx`); media and browser support are optional extras.\n- **Optional response cache** — opt-in, persistent, token-safe (only token-free pathfinder GETs).\n- **Anti-ban built in** — per-host rate limiting, retries with backoff, UA rotation, proxies.\n- **Browser fallback** via Playwright when you need a real browser.\n\n## Command line\n\nWith the `cli` extra installed, a `spotifyscraper` command is available:\n\n```bash\nspotifyscraper track 4uLU6hMCjMI75M1A2tKUQC          # entity metadata as JSON\nspotifyscraper playlist <id> --max-tracks 50 --pretty\nspotifyscraper download preview <id> -o ./previews --embed-cover\n```\n\nEvery command emits JSON, so it composes with tools like `jq`. See the\n[CLI guide](https://spotifyscraper.readthedocs.io/en/latest/guides/cli/).\n\n## Batch helpers\n\nEach getter has a plural sibling (`get_tracks`, `get_albums`, …) that fetches\nmany inputs and returns one `BatchItem` per input — index-aligned, and a dead\ninput never aborts the rest:\n\n```python\nitems = client.get_tracks([\"4uLU6hMCjMI75M1A2tKUQC\", \"bad-id\"])\nok = [i.result for i in items if i.ok]\nfailed = {i.value: i.error for i in items if not i.ok}\n```\n\nThe async client runs them concurrently, bounded by `max_concurrency` (default\n5). See the [batch guide](https://spotifyscraper.readthedocs.io/en/latest/guides/batch/).\n\n## Response caching\n\nFor repeated lookups, enable an opt-in persistent cache. It only stores\n**token-free** pathfinder responses — never the embed pages that carry the\nanonymous token — so no credential is ever written to disk:\n\n```python\nfrom spotify_scraper import SpotifyClient, CacheConfig, FileCache\n\nwith SpotifyClient(cache=CacheConfig(store=FileCache())) as client:\n    client.get_track(\"4uLU6hMCjMI75M1A2tKUQC\")   # first call hits the network\n    client.get_track(\"4uLU6hMCjMI75M1A2tKUQC\")   # served from the cache\n```\n\nDefault TTL is 24h; the `FileCache` is stdlib-only and the backend is pluggable.\nSee the [caching guide](https://spotifyscraper.readthedocs.io/en/latest/guides/caching/).\n\n## Search\n\n`search()` runs one anonymous, aggregate query across every entity type and\nreturns a typed `SearchResults`:\n\n```python\nfrom spotify_scraper import SpotifyClient\n\nwith SpotifyClient() as client:\n    results = client.search(\"daft punk\", types=(\"track\", \"artist\"), limit=5)\n    print(results.total, \"track matches\")\n    for track in results.tracks:\n        print(track.name, \"—\", track.artists[0].name)\n```\n\nHits are sparse (pass an `id` to `get_album()`/`get_show()` for the full entity);\n`total` is the track-match count. See the\n[search guide](https://spotifyscraper.readthedocs.io/en/latest/guides/search/).\n\n## Lyrics & transcripts\n\nLyrics and podcast transcripts need a Spotify account cookie (`sp_dc`); the\nlibrary handles the token handshake for you, and one cookie powers both:\n\n```python\nfrom spotify_scraper import SpotifyClient\n\nwith SpotifyClient(cookies=\"cookies.txt\") as client:   # or cookies={\"sp_dc\": \"...\"}\n    lyrics = client.get_lyrics(\"4uLU6hMCjMI75M1A2tKUQC\")\n    for line in lyrics.lines:\n        print(line.start_ms, line.text)\n\n    transcript = client.get_transcript(\"07gKzPFkbvGF0cHoeG7ARS\")   # a podcast episode\n    for line in transcript.lines:\n        print(line.start_ms, line.text)\n```\n\nYour cookie is sent only to Spotify and never logged. An episode with no\ntranscript raises `NotFoundError`. See the\n[lyrics & cookies guide](https://spotifyscraper.readthedocs.io/en/latest/guides/lyrics-and-cookies/).\n\n## Browser-assisted login\n\nDon't want to copy a cookie by hand? `login()` opens a real browser, you sign in\nonce, and the captured `sp_dc` is persisted (no password is ever collected or\nstored). Later runs reconnect headlessly — ideal for servers:\n\n```python\nfrom spotify_scraper import SpotifyClient\n\nwith SpotifyClient() as client:\n    client.login()                              # reuse a valid session, else open a browser\n    print(client.get_lyrics(\"4uLU6hMCjMI75M1A2tKUQC\").sync_type)\n\n# A later, headless run — no browser needed:\nwith SpotifyClient.from_saved_session() as client:\n    account = client.get_account()              # who am I?\n    print(account.product, account.country, client.is_premium())\n    transcript = client.get_transcript(\"07gKzPFkbvGF0cHoeG7ARS\")\n```\n\n`login()` reuses a valid saved session by default (browser only the first time);\n`from_saved_session()` never needs the `browser` extra. The cookie is stored in\nan owner-only file, or the OS keyring with `store=\"keyring\"` (the `keyring`\nextra). `get_account()`/`is_premium()` report the logged-in account, and\n`SpotifyClient.session_info()` checks a saved session without exposing the\ncookie. See the\n[authenticated sessions guide](https://spotifyscraper.readthedocs.io/en/latest/guides/authentication/).\n\n## Roadmap\n\n**Shipped**\n\n| Version | Scope |\n|---------|-------|\n| **3.0** | The library: all entities, pagination, media downloads, browser fallback, docs |\n| **3.1** | Command-line interface |\n| **3.2** | Cookie-authenticated lyrics |\n| **3.3** | Cookie-authenticated podcast transcripts (`get_transcript`); browser-assisted login, session persistence & account-awareness (`get_account`/`is_premium`) |\n| **3.4** | [Search](https://github.com/AliAkhtari78/SpotifyScraper/issues/129) across every entity type (`search()`) · display-language [localization](https://github.com/AliAkhtari78/SpotifyScraper/issues/130) (`locale`) |\n| **3.5** | Optional [response cache](https://github.com/AliAkhtari78/SpotifyScraper/issues/131) (`cache=CacheConfig(...)`) · [batch helpers](https://github.com/AliAkhtari78/SpotifyScraper/issues/132) with managed concurrency |\n| **3.6** | **Visual & discovery**: cover colors, Canvas videos, charts, related artists, paginated discography, recommendations, public profiles, track credits, concerts · a best-in-class **MCP server** + container image |\n| **3.7** | MCP **batch tools** (`get_tracks`/`get_albums`/…) · `get_track_visuals` convenience tool for visual front-ends |\n| **3.8** | Maintenance: dependency, toolchain & CI modernization (all Actions on current majors, SHA-pinned) · docs & PyPI backlinks |\n| **3.9** | Official **MCP registry** publishing (+ Glama/mcp.so/PulseMCP/Smithery discovery) · \"vs spotipy\" comparison · one-time, opt-out CLI star hint |\n\n**What's next** — future ideas are tracked in the GitHub\n[milestones](https://github.com/AliAkhtari78/SpotifyScraper/milestones) and\n[issues](https://github.com/AliAkhtari78/SpotifyScraper/issues) — 👍 or weigh in\non the ones that matter most to you. Scope is subject to change.\n\n## Reliability & maintenance\n\nThis library rides Spotify's own public endpoints, so it can break when Spotify\nchanges them. To keep it dependable:\n\n- A **daily canary** runs the live test suite against Spotify. When an endpoint\n  shifts, it automatically opens a `spotify-breakage` issue (and closes it on\n  recovery), so regressions surface before they reach you.\n- Breakages are triaged and fixed promptly with the help of **[Claude Code](https://claude.com/claude-code)**\n  (Anthropic's coding agent), under the maintainer's review — the same\n  agent-assisted workflow that keeps this project moving. Persisted-query hashes\n  live in a single file (`api/pathfinder.py`), so a Spotify rotation is a\n  one-line update.\n- Every change runs through `ruff` + `mypy --strict` + a hermetic test suite\n  (85% coverage floor) across Python 3.10–3.13 on Linux, macOS, and Windows.\n\nIf something is broken for you, please\n[open an issue](https://github.com/AliAkhtari78/SpotifyScraper/issues) — the\nmonitoring has often caught it already.\n\n## Documentation\n\nFull docs, guides, and the API reference: **<https://spotifyscraper.readthedocs.io>**\n\nThe MCP server also ships as a container:\n`docker run -p 8000:8000 ghcr.io/aliakhtari78/spotifyscraper` (set `SPOTIFY_SP_DC`\nto enable the authenticated tools).\n\n## Legal\n\nSpotifyScraper is an unofficial, independent project, not affiliated with\nSpotify. It reads publicly available data and the ~30-second previews Spotify\npublishes; it does not download full tracks or circumvent DRM. Use it for\neducational and personal purposes, and in line with Spotify's Terms of Service.\nSee the [legal notice](https://spotifyscraper.readthedocs.io/en/latest/legal/).\n\n## Contributing\n\nContributions are welcome — see [CONTRIBUTING.md](https://github.com/AliAkhtari78/SpotifyScraper/blob/master/CONTRIBUTING.md). The project\nis developed spec-first with [OpenSpec](https://github.com/Fission-AI/OpenSpec);\nspecs live in [`openspec/`](https://github.com/AliAkhtari78/SpotifyScraper/tree/master/openspec).\n\n## Star history\n\nIf SpotifyScraper saved you the official-API OAuth dance, a ⭐ helps other\ndevelopers find it — and tells me which features to keep building.\n\n<a href=\"https://star-history.com/#AliAkhtari78/SpotifyScraper&Date\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=AliAkhtari78/SpotifyScraper&type=Date&theme=dark\" />\n    <img alt=\"Star history of AliAkhtari78/SpotifyScraper\" src=\"https://api.star-history.com/svg?repos=AliAkhtari78/SpotifyScraper&type=Date\" width=\"600\" />\n  </picture>\n</a>\n\n## License\n\n[MIT](https://github.com/AliAkhtari78/SpotifyScraper/blob/master/LICENSE) © [Ali Akhtari](https://aliakhtari.com) — full-stack AI engineer ([aliakhtari.com](https://aliakhtari.com)).\n\n<!-- mcp-name: io.github.AliAkhtari78/spotifyscraper -->\n",
  "bytes": 18164,
  "sha": "8eeacffdab40d746c5b7e6ad20f0fd3a4f3151ed0887bedbd46703e8b11e5a39",
  "repo_slug": "aliakhtari78/spotifyscraper",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_aliakhtari78_spotifyscraper_b5039778/readme"
}