Back to APIs list

Spotify API examples & templates

Use these vals as a playground to view and fork Spotify API examples and templates on Val Town. Run any example below or find templates that can be used as a pre-built solution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { discoverWeeklyArchiveName } from "https://esm.town/v/stevekrouse/discoverWeeklyArchiveName";
import { spotifyAddToPlaylist } from "https://esm.town/v/stevekrouse/spotifyAddToPlaylist";
import { spotifyCreatePlaylist } from "https://esm.town/v/stevekrouse/spotifyCreatePlaylist";
import { spotifyDiscoverWeeklyTracks } from "https://esm.town/v/stevekrouse/spotifyDiscoverWeeklyTracks";
export let spotifyArchiveDiscoverWeekly = async (token) => {
let tracks = await spotifyDiscoverWeeklyTracks(token);
let date = new Date(tracks.items[0].added_at);
let name = await discoverWeeklyArchiveName(date);
let { id } = await spotifyCreatePlaylist({ token, name });
console.log({ token, date, name, id })
return await spotifyAddToPlaylist({
position: 0,
uris: tracks.items.map(t => t.track.uri),
token,
id,
});
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { generateRandomString } from "https://esm.town/v/greg0r/generateRandomString?v=1";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { home, scopesRoute } from "https://esm.town/v/stevekrouse/spotify_helpers";
import { spotifyRefreshToken } from "https://esm.town/v/stevekrouse/spotifyRefreshToken";
import { spotifyRequestToken } from "https://esm.town/v/stevekrouse/spotifyRequestToken";
import { thisWebURL } from "https://esm.town/v/stevekrouse/thisWebURL";
import { eq, sql } from "npm:drizzle-orm";
import { drizzle } from "npm:drizzle-orm/libsql";
import { integer, sqliteTable, text } from "npm:drizzle-orm/sqlite-core";
import { getCookie, setCookie } from "npm:hono/cookie";
import { Hono } from "npm:hono@3";
// await sqlite.execute("CREATE TABLE spot (id text primary key, data text)")
export const db = drizzle(sqlite as any);
export const table = sqliteTable("SPOTIFY_AUTH", {
id: text("id").primaryKey(),
data: text("data"),
});
const thisURL = thisWebURL();
const redirect_uri = thisURL + "/callback";
const app = new Hono();
app.use("*", async (c, next) => {
const state = getCookie(c, "state") ?? generateRandomString(16);
c.set("state", state);
await next();
setCookie(c, "state", state);
});
app.get("/", home);
app.get("/scopes", scopesRoute);
app.get("/login", async (c) => {
const state = c.get("state");
const scopes = c.req.queries("scopes[]").join(" ");
await db.insert(table).values([{ id: state }]);
return c.redirect(
`https://accounts.spotify.com/authorize?response_type=code&client_id=${
encodeURIComponent(Deno.env.get("SPOTIFY_CLIENT_ID"))
}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(redirect_uri)}&state=${state}`,
);
});
app.get("/callback", async (c) => {
const code = c.req.query("code");
const state = c.req.query("state") as string;
if (state !== c.get("state")) {
return c.html("State mismatch 1");
}
const rowInitialResult = await db.select().from(table).where(eq(table.id, state)).limit(1);
const rowInitial = rowInitialResult[0];
if (!rowInitial) {
return c.html("State mismatch 2");
}
if (rowInitial.data) {
return c.html("Already authenticated for that state");
}
const data = await spotifyRequestToken({
code,
client_id: Deno.env.get("SPOTIFY_CLIENT_ID"),
client_secret: Deno.env.get("SPOTIFY_CLIENT_SECRET"),
redirect_uri,
});
data.now = Date.now(); // to help us calculate when to refresh
if (data.error) return c.json(data.error);
if (!data.access_token) return c.json("No access token");
await db.update(table).set({ data: JSON.stringify(data) }).where(eq(table.id, state));
return c.redirect("/token?state=" + state);
});
app.get("/token", async (c) => {
const state = c.req.query("state") as string;
const authData = await db.select().from(table).where(eq(table.id, state)).limit(1);
if (!authData.length) return c.json("No auth data found for supplied state");
const data = JSON.parse(authData[0].data);
let expiresAt = data.now + (data.expires_in * 1000);
if (expiresAt < Date.now()) {
let refresh = await spotifyRefreshToken({
refresh_token: data.refresh_token,
client_id: Deno.env.get("SPOTIFY_CLIENT_ID"),
client_secret: Deno.env.get("SPOTIFY_CLIENT_SECRET"),
});
if (refresh.error) return c.json("Refresh error: " + refresh.error);
if (!refresh.access_token) return c.json("No refresh access token");
refresh.now = Date.now();
await db.update(table).set({ data: JSON.stringify(refresh) }).where(eq(table.id, state));
}
return c.json(data.access_token);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/** @jsxImportSource https://esm.sh/hono@3.9.2/jsx **/
import { scopes } from "https://esm.town/v/stevekrouse/spotifyScopes";
export const home = c =>
c.html(
<div>
<h1>Get your Spotify Access Token</h1>
<p>
This app uses{" "}
<a href="https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow">
Authorization Code Flow
</a>{" "}
to authenticate users:
</p>
<ol>
<li>
We generate a unique code that you will use to authenticate to us: <code>{c.get("state")}</code>
</li>
<li>
Copy and paste your code in your{" "}
<a href="https://www.val.town/settings/environment-variables?adding=true" target="_blank">
Val Town Environment Variables
</a>{" "}
as <code>SPOTIFY_VAL_TOWN_AUTH</code>
</li>
<li>You can pick which scopes to authorize.</li>
<li>We construct a URL to redirect you to, which will ask you to authorize our app on Spotify.</li>
<li>You are redirected back to our app, with a code in the URL.</li>
<li>We exchange the code for an access token and refresh token, which we save.</li>
<li>
You can request your access token at{" "}
<a href={`/token?state=${c.get("state")}`}>
<code>https://{new URL(c.req.url).hostname}/token?state={c.get("state")}</code>
</a>
</li>
<li>If your access token expires, we use the refresh token to get a new one.</li>
<li>Use your access token to make requests to the Spotify API.</li>
</ol>
<div>
<a href="/scopes">Get started</a>
</div>
<br></br>
<div>
If you get in a broken state, you can always{" "}
<a href="https://www.spotify.com/us/account/apps/" target="_blank">
remove access to this app from your Spotify account
</a>.
</div>
</div>,
);
export const scopesRoute = c =>
c.html(
<div>
<h1>Choose your Spotify Scopes</h1>
<form action="/login">
{scopes.map(scope => (
<div>
<label>
<input type="checkbox" id={scope} name="scopes[]" value={scope} checked />
{scope}
</label>
</div>
))}
<button type="submit">Login to Spotify</button>
</form>
</div>,
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const scopes = [
"ugc-image-upload",
"user-read-playback-state",
"user-modify-playback-state",
"user-read-currently-playing",
"app-remote-control",
"streaming",
"playlist-read-private",
"playlist-read-collaborative",
"playlist-modify-private",
"playlist-modify-public",
"user-follow-modify",
"user-follow-read",
"user-read-playback-position",
"user-top-read",
"user-read-recently-played",
"user-library-modify",
"user-library-read",
"user-read-email",
"user-read-private",
// "user-soa-link",
// "user-soa-unlink",
// "user-manage-entitlements",
// "user-manage-partner",
// "user-create-partner",
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { querystring } from "https://esm.town/v/stevekrouse/querystring";
export let spotifyRefreshToken = async ({ refresh_token, client_id, client_secret }) =>
fetchJSON("https://accounts.spotify.com/api/token", {
method: "POST",
body: await querystring({
refresh_token: refresh_token,
grant_type: "refresh_token",
}),
headers: {
"Authorization": "Basic " + (new Buffer(client_id + ":" + client_secret).toString("base64")),
"Content-Type": "application/x-www-form-urlencoded",
},
});
  • Console DevTools
  • devtools.fm
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { Hono } from "https://deno.land/x/hono@v3.11.1/mod.ts";
import { getItunesId, isApplePodcast } from "https://esm.town/v/vladimyr/applePodcast?v=5";
import { getLastPubDate } from "https://esm.town/v/vladimyr/podcast?v=21";
import { getFeedUrl } from "https://esm.town/v/vladimyr/podnews?v=16";
import { getSpotifyId, isSpotifyPodcast } from "https://esm.town/v/vladimyr/spotifyPodcast?v=1";
import { badgen as createBadge, BadgenOptions } from "npm:badgen@3.2.3";
import friendlyTime from "npm:friendly-time@1.1.1";
import ky, { HTTPError } from "npm:ky@1.1.3";
import { withQuery } from "npm:ufo@1.3.2";
const BADGE_LABEL = "last episode";
const app = new Hono();
app.get("/", async (c) => {
const { q: feedUrl, ...query } = c.req.query();
if (!feedUrl) return c.body("");
if (isApplePodcast(feedUrl)) {
return c.redirect(withQuery(`/i/${getItunesId(feedUrl)}`, query));
}
if (isSpotifyPodcast(feedUrl)) {
return c.redirect(withQuery(`/s/${getSpotifyId(feedUrl)}`, query));
}
const badgeOptions = await getBadgeOptions(feedUrl);
if (query.format === "json") {
return c.json({
schemaVersion: 1,
label: badgeOptions.label,
message: badgeOptions.status,
color: badgeOptions.color,
});
}
c.header("Content-Type", "image/svg+xml");
return c.body(createBadge(badgeOptions));
});
app.get("/:service{(?:i(?:tunes)?|s(?:potify)?)}/:podcastId", async (c) => {
const podcastId = c.req.param("podcastId");
const { format, debug } = c.req.query();
const feedUrl = await getFeedUrl({ podcastId });
const badgeOptions = await getBadgeOptions(feedUrl);
if (format === "json") {
const badgeInfo = {
schemaVersion: 1,
label: badgeOptions.label,
message: badgeOptions.status,
color: badgeOptions.color,
};
if (debug) {
Object.assign(badgeInfo, {
_debug: { feedUrl },
});
}
return c.json(badgeInfo);
}
c.header("Content-Type", "image/svg+xml");
return c.body(createBadge(badgeOptions));
});
export default app.fetch;
async function getBadgeOptions(rssUrl: string | URL): Promise<BadgenOptions> {
const label = BADGE_LABEL;
try {
rssUrl = new URL(rssUrl);
} catch (err) {
if (err instanceof TypeError) {
return { label, status: "400", color: "red" };
}
}
let xml;
try {
xml = await ky.get(rssUrl).text();
} catch (err) {
if (err instanceof HTTPError) {
const status = String(err.response.status);
return { label, status, color: "red" };
}
}
let pubDate;
try {
pubDate = await getLastPubDate(xml);
} catch (err) {
if (err instanceof TypeError) {
return { label, status: "invalid", color: "grey" };
}
}
if (!pubDate) {
return { label, status: "unknown", color: "grey" };
}
return { label, status: friendlyTime(pubDate), color: "blue" };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function isSpotifyPodcast(podcastURL: URL | string) {
try {
podcastURL = new URL(podcastURL);
} catch {
return false;
}
return podcastURL.hostname === "open.spotify.com"
&& podcastURL.pathname.startsWith("/show/");
}
export function getSpotifyId(podcastURL: URL | string) {
try {
podcastURL = new URL(podcastURL);
} catch {
return;
}
if (!isSpotifyPodcast(podcastURL)) return;
return podcastURL.pathname.split("/").filter(Boolean).at(-1);
}
1
2
3
4
5
6
7
8
9
import { spotifyMe } from "https://esm.town/v/stevekrouse/spotifyMe";
import { spotifyAPI } from "https://esm.town/v/stevekrouse/spotifyAPI";
export let spotifyCreatePlaylist = async ({token, ...params}) => spotifyAPI({
token,
endpoint: `users/${(await spotifyMe(token)).id}/playlists`,
method: 'POST',
body: JSON.stringify(params)
})
1
2
3
4
5
6
7
8
9
import { spotifyRequestToken } from "https://esm.town/v/stevekrouse/spotifyRequestToken";
import { spotifyTokens } from "https://esm.town/v/stevekrouse/spotifyTokens";
export let spotifyOAuthCallback = async ({code, state}) => {
return spotifyTokens[state] = {
...await spotifyRequestToken(code).catch(console.error),
now: Date.now()
}
}
1
2
3
4
5
6
import { spotifyAPI } from "https://esm.town/v/stevekrouse/spotifyAPI";
export let spotifySearch = ({...params}) => spotifyAPI({
endpoint: 'search',
...params
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { spotifyAddToPlaylist } from "https://esm.town/v/stevekrouse/spotifyAddToPlaylist";
import { spotifyCreatePlaylist } from "https://esm.town/v/stevekrouse/spotifyCreatePlaylist";
import { discoverWeeklyArchieveName } from "https://esm.town/v/stevekrouse/discoverWeeklyArchieveName";
import { spotifyDiscoverWeeklyTracks } from "https://esm.town/v/stevekrouse/spotifyDiscoverWeeklyTracks";
export let spotifyAchiveDiscoverWeekly = async (token) => {
let tracks = await spotifyDiscoverWeeklyTracks(token)
let date = new Date(tracks.items[0].added_at)
let name = await discoverWeeklyArchieveName(date)
let { id } = await spotifyCreatePlaylist({ token, name })
return await spotifyAddToPlaylist({
position: 0,
uris: tracks.items.map(t => t.track.uri),
token,
id
})
}
1
export let spotifyClientId = "594b06255b0d469a83b8755372859b22"
1
2
3
4
5
6
7
8
9
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export let spotifyAPI = ({token, endpoint, ...params}) => fetchJSON(
`https://api.spotify.com/v1/${endpoint}?${new URLSearchParams(params)}`,
{
headers: { Authorization: `Bearer ${token}` },
...params
}
)
1
2
3
4
5
6
7
import { spotifyTracks } from "https://esm.town/v/stevekrouse/spotifyTracks";
import { spotifyDiscoverWeeklyPlaylist } from "https://esm.town/v/stevekrouse/spotifyDiscoverWeeklyPlaylist";
export let spotifyDiscoverWeeklyTracks = async (token) => {
let { id } = await spotifyDiscoverWeeklyPlaylist(token)
return spotifyTracks(token, id)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import { spotifyRefreshToken } from "https://esm.town/v/stevekrouse/spotifyRefreshToken";
import { spotifyTokens } from "https://esm.town/v/stevekrouse/spotifyTokens";
export let spotifyGetToken = async state => {
let expiresAt = spotifyTokens[state]?.now + (spotifyTokens[state]?.expires_in * 1000)
if (expiresAt < Date.now()) {
let refresh = await spotifyRefreshToken(spotifyTokens[state].refresh_token).catch(console.error)
if (refresh.error) throw new Error("Refresh error: " + refresh.error)
if (!refresh.access_token) throw new Error("No refresh access token")
spotifyTokens[state].access_token = refresh.access_token
}
return spotifyTokens[state]?.access_token
}