+133
-2
src/components/playing/spotify.ts
+133
-2
src/components/playing/spotify.ts
···
1
+
import {
2
+
SPOTIFY_CLIENT_ID,
3
+
SPOTIFY_CLIENT_SECRET,
4
+
SPOTIFY_REDIRECT_URI,
5
+
} from "astro:env/server";
6
+
import fs from "fs/promises";
7
+
8
+
const throws = (val: unknown) => {
9
+
throw val;
10
+
};
11
+
12
+
/** via: https://www.totaltypescript.com/concepts/the-prettify-helper */
13
+
type Prettify<T> = {
14
+
[K in keyof T]: T[K];
15
+
} & {};
16
+
17
+
type AuthToken = {
18
+
access_token: string;
19
+
token_type: "Bearer";
20
+
scope: string;
21
+
expires_in: number;
22
+
refresh_token: string;
23
+
};
24
+
25
+
type RefreshToken = Prettify<
26
+
Omit<AuthToken, "refresh_token"> & { refresh_token?: string }
27
+
>;
28
+
29
+
const isRefreshToken = (obj: unknown): obj is RefreshToken =>
30
+
// validate is object
31
+
typeof obj === "object" &&
32
+
obj !== null &&
33
+
// validate properties
34
+
"access_token" in obj &&
35
+
typeof obj.access_token === "string" &&
36
+
"token_type" in obj &&
37
+
obj.token_type === "Bearer" &&
38
+
"scope" in obj &&
39
+
typeof obj.scope === "string" &&
40
+
"expires_in" in obj &&
41
+
typeof obj.expires_in === "number" &&
42
+
// either refresh token exists as string or not at all
43
+
(("refresh_token" in obj && typeof obj.refresh_token === "string") ||
44
+
!("refresh_token" in obj));
45
+
46
+
// auth token is just refresh with a non optional refresh_token
47
+
const isAuthToken = (obj: unknown): obj is AuthToken =>
48
+
isRefreshToken(obj) && "refresh_token" in obj;
49
+
1
50
export async function getAccessCode(userAuthCode?: string) {
2
-
return "Not implemented!"
3
-
}
51
+
const refreshToken = await fs
52
+
.readFile("./.refreshToken", { encoding: "utf-8" })
53
+
.catch((_) => undefined)
54
+
.then((x) => (x === "" || x === "REFRESH_TOKEN" ? undefined : x));
55
+
if (!(userAuthCode || refreshToken))
56
+
throw new Error(
57
+
"No auth code or refresh token.\nGenerate an auth code at `/src/pages/_callback`\nA refresh token will be generated from this auth token.",
58
+
);
59
+
60
+
// prefer auth codes over refresh tokens
61
+
// since the auth code may have updated scopes.
62
+
63
+
const accessFrom:
64
+
| {
65
+
userAuthCode: string;
66
+
}
67
+
| {
68
+
refreshToken: string;
69
+
} = userAuthCode
70
+
? { userAuthCode }
71
+
: refreshToken
72
+
? { refreshToken }
73
+
: (undefined as never);
74
+
75
+
const req = fetch("https://accounts.spotify.com/api/token", {
76
+
method: "POST",
77
+
headers: {
78
+
"Content-Type": "application/x-www-form-urlencoded",
79
+
Authorization: `Basic ${Buffer.from(SPOTIFY_CLIENT_ID + ":" + SPOTIFY_CLIENT_SECRET).toString("base64")}`,
80
+
},
81
+
body: new URLSearchParams({
82
+
grant_type:
83
+
"userAuthCode" in accessFrom ? "authorization_code" : "refresh_token",
84
+
...("userAuthCode" in accessFrom
85
+
? {
86
+
code: accessFrom.userAuthCode,
87
+
redirect_uri: SPOTIFY_REDIRECT_URI,
88
+
}
89
+
: {
90
+
refresh_token: accessFrom.refreshToken,
91
+
}),
92
+
}).toString(),
93
+
});
94
+
95
+
return (
96
+
req
97
+
// if res isn't 200 handle it in the catch
98
+
.then((res) => (res.ok ? res : throws(res)))
99
+
// request is 200-299
100
+
// json can throw SyntaxError in this case
101
+
.then((res) => res.json())
102
+
.then((res) =>
103
+
"userAuthCode" in accessFrom
104
+
? isAuthToken(res)
105
+
? { code: res.access_token, refresh: res.refresh_token }
106
+
: throws({ err: "INVALID_RESPONSE", res })
107
+
: isRefreshToken(res)
108
+
? {
109
+
code: res.access_token,
110
+
refresh: res.refresh_token ?? accessFrom.refreshToken,
111
+
}
112
+
: throws({ err: "INVALID_RESPONSE", res }),
113
+
)
114
+
// res is now an access token and refresh token
115
+
.then((res) => {
116
+
fs.writeFile("./.refreshToken", res.refresh, { encoding: "utf-8" });
117
+
return res.code;
118
+
})
119
+
.catch((err) => {
120
+
// SyntaxError
121
+
// Response
122
+
// {err: string, res: Response}
123
+
if (err instanceof Response) console.error("Request failed:", err);
124
+
else if (err instanceof SyntaxError)
125
+
console.error("Response JSON failed", err);
126
+
else if (err.err === "INVALID_RESPONSE")
127
+
console.error("Response malformed:", err);
128
+
else {
129
+
console.error("Unhandled exception.");
130
+
throw err;
131
+
}
132
+
})
133
+
);
134
+
}