├── .envrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── flake.lock ├── flake.nix └── wrangler ├── package.json ├── src └── index.ts ├── tsconfig.json └── wrangler.toml /.envrc: -------------------------------------------------------------------------------- 1 | if command -v nix-shell &> /dev/null 2 | then 3 | use flake 4 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Global Ignore 2 | **/node_modules/ 3 | **/pnpm-lock.yaml 4 | **/package-lock.json 5 | **/yarn.lock 6 | .env 7 | .direnv 8 | 9 | # Wrangler Ignore 10 | wrangler/.dev.vars -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BlankParticle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > #### As of writing this, I got the information that Spotify has made lyrics a premium feature, so this API will not work with throw away accounts anymore. Its not worth the risk to use this API with a Spotify Premium account. Thus the repo has been archived 3 | 4 | 5 | # 🎧 Spot API 6 | > **Warning:** Spotify doesn't provide any official lyrics API, this project is supposed to provide lyrics by requesting to spotify's WebPlayer lyrics API. So I can't host it for you. You can't use this API for any commercial use, this API is supposed to be self hosted and used in personal sites like your portfolio. I, [BlankParticle](https://github.com/BlankParticle) or any other entity mentioned in the project will not responsible for anything. 7 | 8 | ## 📣 Context 9 | I was creating my Portfolio when it occurred to me that I could add a **Spotify Widget** in my portfolio, I saw some examples but they all looked the same. So as a responsible developer, I tried to create a widget with lyrics, but spotify didn't have any lyrics API thus this Project was born. 10 | 11 | ## 📜 The Blog Article 12 | Read more about How I created this project at [my blog article](https://blog.blankparticle.in/creating-a-self-hosted-spotify-lyrics-api-using-cloudflare-workers). 13 | 14 | ## 🏗️ Project Structure 15 | Initially, this was a [Svelte Kit](https://kit.svelte.dev) Project, I used Svelte API routes with [Vercel](https://vercel.com) as a hosting. But later I thought that maybe I can create this as a Project with standalone serverless functions. 16 | 17 | You can find code for different cloud providers in their respective folders. 18 | 19 | ### 💡 Planned Platforms 20 | - [x] Cloudflare Workers (wrangler) 21 | - [ ] Vercel 22 | - [ ] Netlify 23 | 24 | ## 🎋 Self Hosting your API 25 | 26 | First clone this repo using git 27 | 28 | ```bash 29 | git clone https://github.com/BlankParticle/spotapi.git 30 | cd spotapi 31 | ``` 32 | Now you need Your [Spotify User Cookie](https://blog.blankparticle.in/creating-a-self-hosted-spotify-lyrics-api-using-cloudflare-workers#heading-obtaining-the-spdc-cookie), You can make a new spotify account. You only need the `sp_dc` cookie. Be sure not to leak this cookie because this can be used to login into your spotify account. 33 | 34 | > **Note:** This cookie may expire if you logout from your account or after 1 year of use, You must check for that and provide a new cookie when that happens. 35 | 36 | Now save this cookie in a `.env` file in project root 37 | ```bash 38 | SPOTIFY_COOKIE=your-sp_dc-cookie 39 | ``` 40 | Then install NodeJs and `npm` (or `pnpm`/`yarn`) if haven't already. 41 | 42 | ### ☁️ Cloudflare Worker 43 | #### Test Locally before deploying 44 | ```bash 45 | cp .env wrangler/.dev.vars 46 | cd wrangler 47 | npm install # or use "pnpm install" or "yarn" 48 | npm run dev # or use "pnpm dev" or "yarn dev" 49 | ``` 50 | 51 | If all that works then you can go to and test your worker. 52 | 53 | #### Deploy to cloudflare worker 54 | If you don't have a cloudflare account, [create one](https://dash.cloudflare.com/). 55 | Now type, 56 | ```bash 57 | npm run add-secret # or use "pnpm add-secret" or "yarn add-secret" 58 | ``` 59 | then login (if prompted), and put your cookie. 60 | 61 | Now you can run, 62 | ```bash 63 | npm run deploy # or use "pnpm run deploy" or "yarn run deploy" 64 | ``` 65 | And, You have your self hosted Spotify Lyrics API. 66 | 67 | ### 🚧 Other Platforms 68 | I don't have time to figure out Vercel functions, I would be happy if someone makes a PR with Vercel function, same with Netlify 69 | 70 | ## 📝 License 71 | Copyright © 2023 [BlankParticle](https://github.com/BlankParticle).
72 | This project is [MIT](https://github.com/BlankParticle/spotapi/blob/main/LICENSE.txt) licensed. 73 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1691472822, 6 | "narHash": "sha256-XVfYZ2oB3lNPVq6sHCY9WkdQ8lHoIDzzbpg8bB6oBxA=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "41c7605718399dcfa53dd7083793b6ae3bc969ff", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "NodeJS current and pnpm Project"; 3 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | outputs = { self, nixpkgs }: 5 | let 6 | system = "x86_64-linux"; 7 | pkgs = nixpkgs.legacyPackages.${system}; 8 | packages = with pkgs; [ 9 | (nodejs_18.override { enableNpm = false; }) 10 | nodePackages.pnpm 11 | nodePackages.wrangler 12 | nil 13 | nixpkgs-fmt 14 | ]; 15 | in 16 | { 17 | devShells.${system}.default = 18 | pkgs.mkShell { 19 | inherit packages; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /wrangler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spot-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "add-secret": "wrangler secret put SPOTIFY_COOKIE", 8 | "deploy": "wrangler deploy" 9 | }, 10 | "dependencies": { 11 | "hono": "^3.2.5" 12 | }, 13 | "devDependencies": { 14 | "@cloudflare/workers-types": "^4.20230518.0", 15 | "typescript": "^5.1.3", 16 | "wrangler": "3.1.0" 17 | } 18 | } -------------------------------------------------------------------------------- /wrangler/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | 3 | type Bindings = { 4 | SPOTIFY_COOKIE: string; 5 | }; 6 | 7 | const predefinedRequestHeaders = { 8 | "User-Agent": 9 | "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0", 10 | Accept: 11 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 12 | "Accept-Language": "en-US,en;q=0.5", 13 | "Alt-Used": "open.spotify.com", 14 | "Upgrade-Insecure-Requests": "1", 15 | "Sec-Fetch-Dest": "document", 16 | "Sec-Fetch-Mode": "navigate", 17 | "Sec-Fetch-Site": "cross-site", 18 | }; 19 | 20 | const predefinedResponseHeaders = { 21 | "content-type": "application/json; charset=utf-8", 22 | }; 23 | 24 | const app = new Hono<{ Bindings: Bindings }>(); 25 | 26 | let ACCESS_TOKEN: string | null = null; 27 | let ACCESS_TOKEN_EXPIRY: number = Date.now(); 28 | 29 | type AccessTokenAPIData = { 30 | clientId: string; 31 | accessToken: string; 32 | accessTokenExpirationTimestampMs: number; 33 | isAnonymous: boolean; 34 | }; 35 | 36 | app.get("/lyrics/:track_id", async (c) => { 37 | if (!c.env.SPOTIFY_COOKIE) { 38 | return new Response( 39 | JSON.stringify({ error: "API hasn't been setup correctly" }), 40 | { 41 | headers: predefinedResponseHeaders, 42 | status: 500, 43 | } 44 | ); 45 | } 46 | 47 | try { 48 | if (!ACCESS_TOKEN || ACCESS_TOKEN_EXPIRY <= Date.now()) { 49 | console.log("[DEBUG] Getting token"); 50 | const raw_data = await ( 51 | await fetch("https://open.spotify.com/get_access_token", { 52 | headers: { 53 | ...predefinedRequestHeaders, 54 | Cookie: `sp_dc=${c.env.SPOTIFY_COOKIE}`, 55 | }, 56 | }) 57 | ).text(); 58 | try { 59 | const data: AccessTokenAPIData = JSON.parse(raw_data); 60 | ACCESS_TOKEN = data.accessToken; 61 | ACCESS_TOKEN_EXPIRY = data.accessTokenExpirationTimestampMs; 62 | } catch (e) { 63 | console.log(e, raw_data); 64 | return new Response( 65 | JSON.stringify({ error: "Unknown Error Occurred!" }), 66 | { 67 | headers: predefinedResponseHeaders, 68 | status: 500, 69 | } 70 | ); 71 | } 72 | } 73 | 74 | const { track_id } = c.req.param(); 75 | const url = `https://spclient.wg.spotify.com/color-lyrics/v2/track/${track_id}?format=json&vocalRemoval=false`; 76 | const lyrics = await ( 77 | await fetch(url, { 78 | headers: { 79 | "app-platform": "WebPlayer", 80 | authorization: `Bearer ${ACCESS_TOKEN}`, 81 | }, 82 | }) 83 | ).text(); 84 | 85 | return new Response( 86 | lyrics === "" 87 | ? JSON.stringify({ error: "Lyrics are not available for this song" }) 88 | : lyrics, 89 | { 90 | headers: predefinedResponseHeaders, 91 | status: lyrics === "" ? 404 : 200, 92 | } 93 | ); 94 | } catch (e) { 95 | console.log(e); 96 | return new Response(JSON.stringify({ error: "Unknown Error Occurred!" }), { 97 | headers: predefinedResponseHeaders, 98 | status: 500, 99 | }); 100 | } 101 | }); 102 | 103 | app.get("*", (c) => 104 | c.json( 105 | { 106 | error: "Bad Request, Try Requesting to /lyrics/", 107 | }, 108 | 404 109 | ) 110 | ); 111 | 112 | export default app; 113 | -------------------------------------------------------------------------------- /wrangler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "lib": ["es2021"], 5 | "module": "es2022", 6 | "moduleResolution": "node", 7 | "types": ["@cloudflare/workers-types"], 8 | "resolveJsonModule": true, 9 | "allowJs": true, 10 | "checkJs": false, 11 | "noEmit": true, 12 | "isolatedModules": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "skipLibCheck": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /wrangler/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "spot-api" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-06-10" --------------------------------------------------------------------------------