Personal Site
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

complete refactor and bugfix for spotify sdk loader

this fixes issues with token updating and makes the code just much more bearable

+84 -63
+84 -63
src/components/home/playing/spotify/index.ts
··· 7 7 import { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } from "astro:env/server"; 8 8 9 9 import fs from "node:fs/promises"; 10 - import { isObj, throws } from "/utils"; 10 + import { isObj, type Prettify } from "/utils"; 11 11 12 12 /** 13 13 * the refresh_token field is not checked as it might not be returned from the api 14 14 */ 15 - const isSpotifyAccessToken = (token: any): token is AccessToken => 15 + const isSpotifyAccessToken = ( 16 + token: any, 17 + // token could have an ommited refresh token field 18 + ): token is Prettify< 19 + Omit<AccessToken, "refresh_token"> & { refresh_token?: string } 20 + > => 16 21 isObj(token) && 17 22 "access_token" in token && 18 23 typeof token.access_token === "string" && 19 24 "token_type" in token && 20 25 token.token_type === "Bearer" && 21 26 "expires_in" in token && 22 - typeof token.expires_in === "number"; 27 + typeof token.expires_in === "number" && 28 + ("refresh_token" in token ? typeof token.refresh_token === "string" : true); 29 + 30 + const refresh = (() => { 31 + let token = ""; 32 + 33 + return { 34 + async get() { 35 + const refresh = 36 + token === "" 37 + ? await fs 38 + .readFile("./.refreshToken", { encoding: "utf-8" }) 39 + .catch((err) => console.error(err)) 40 + : token; 23 41 24 - const reloadAccessToken = async (refresh_token: string) => 25 - fetch("https://accounts.spotify.com/api/token", { 26 - method: "post", 27 - headers: { 28 - "content-type": "application/x-www-form-urlencoded", 29 - Authorization: 30 - "Basic " + 31 - Buffer.from(SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET).toString( 32 - "base64", 33 - ), 42 + if (!refresh) throw "Could not load refresh token"; 43 + this.set(refresh); // dispatch update whenever its loaded to make sure its up to date 44 + return refresh; 34 45 }, 35 - body: new URLSearchParams({ 36 - grant_type: "refresh_token", 37 - refresh_token: refresh_token, 38 - }).toString(), 39 - }) 40 - .then((res) => res.json()) 41 - .then((token) => 42 - isSpotifyAccessToken(token) 43 - ? { 44 - ...token, 45 - // if no refresh_token is provided then insert it 46 - ...(token.refresh_token ? {} : { refresh_token: refresh_token }), 47 - } 48 - : console.error("Response was not a valid access token:", token), 49 - ) 50 - .catch((err) => console.error(err)) 51 - .then((token) => 52 - !!token 53 - ? token 54 - : throws( 55 - new Error( 56 - "Could not generate a new access token from the refresh token", 57 - ), 58 - ), 59 - ); 46 + 47 + // by making this not async it makes sure the token update is blocking 48 + set(arg_token: string) { 49 + token = arg_token; 50 + 51 + return fs 52 + .writeFile("./.refreshToken", token, { 53 + encoding: "utf-8", 54 + }) 55 + .catch((err) => 56 + console.warn("Could not write to ./.refreshToken:", err), 57 + ); 58 + }, 59 + 60 + async reload() { 61 + const res = await fetch("https://accounts.spotify.com/api/token", { 62 + method: "post", 63 + headers: { 64 + "content-type": "application/x-www-form-urlencoded", 65 + Authorization: 66 + "Basic " + 67 + Buffer.from( 68 + SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET, 69 + ).toString("base64"), 70 + }, 71 + body: new URLSearchParams({ 72 + grant_type: "refresh_token", 73 + refresh_token: await this.get(), 74 + }).toString(), 75 + }).catch((err) => console.error(err)); 76 + 77 + if (!res) return res; 78 + 79 + const refresh_token = await res 80 + .json() 81 + .then((token) => 82 + isSpotifyAccessToken(token) 83 + ? token 84 + : console.error("Response was not a valid access token:", token), 85 + ); 60 86 61 - /** MAIN LOGIC HERE */ 62 - // try load last known refresh token from file 63 - const refreshToken = await fs 64 - .readFile("./.refreshToken", { encoding: "utf-8" }) 65 - .then((token) => (token ? token : console.log("invalid `./.refreshToken`"))) 66 - // if anything errors its undefined 67 - .catch((err) => console.error(err)); 87 + if (!refresh_token) return refresh_token; 68 88 69 - // if refreshToken is undefined then we dont have a valid one saved, and can request the user obtain one 70 - // (this could be corruption, failed save, or missing file) 71 - if (!refreshToken) 72 - throw "No access token is stored in `./.refreshToken`. Please generate one using the `/callback` endpoint in a dev server."; 89 + // update the refresh token if it exists 90 + refresh_token.refresh_token && this.set(refresh_token.refresh_token); 73 91 74 - const accessToken = await reloadAccessToken(refreshToken); 92 + return { refresh_token: token, ...refresh_token }; 93 + }, 94 + }; 95 + })(); 75 96 76 - if (!accessToken) 77 - throw "Could not generate a new access token from the refresh token"; 97 + // MAIN LOGIC HERE 98 + // try load last known refresh token from file and use it to generate new token 99 + await refresh.get(); 100 + const accessToken = await refresh.reload(); 101 + if (!accessToken) throw "Could not load access token"; 78 102 79 103 export const sdk = new SpotifyApi( 80 - new ProvidedAccessTokenStrategy(SPOTIFY_CLIENT_ID, accessToken, (_, token) => 81 - reloadAccessToken(token.refresh_token), 82 - ), 83 - { 84 - async afterRequest() { 85 - const token = await sdk.getAccessToken(); 86 - if (!token) return; 87 - fs.writeFile("./.refreshToken", token.refresh_token, { 88 - encoding: "utf-8", 89 - }); 104 + new ProvidedAccessTokenStrategy( 105 + SPOTIFY_CLIENT_ID, 106 + accessToken, 107 + async (_, prev) => { 108 + const token = await refresh.reload(); 109 + if (!token) return prev; // will cause issues but probably more robust 110 + return token; 90 111 }, 91 - }, 112 + ), 92 113 );