Skip to content

Instantly share code, notes, and snippets.

@kmein
Created October 24, 2023 17:57
Show Gist options
  • Save kmein/94df54ac477f1b1dad07078ae078b67c to your computer and use it in GitHub Desktop.
Save kmein/94df54ac477f1b1dad07078ae078b67c to your computer and use it in GitHub Desktop.
Script to archive your Spotify playlists
import { Base64 } from "https://deno.land/x/bb64/mod.ts";
import { join } from "https://deno.land/std/path/mod.ts";
import { exists } from "https://deno.land/std/fs/mod.ts";
const debug = (x: T): T => {
console.error(x);
return x;
};
const assertEnv = (key: string): string => {
const value = Deno.env.get(key);
if (value) return value;
else {
console.error(
`I still need the environment variable ${key} to function correctly.`
);
Deno.exit(1);
}
};
const clientSecret = assertEnv("SPOTIFY_CLIENT_SECRET");
const clientId = assertEnv("SPOTIFY_CLIENT_ID");
const userId = assertEnv("SPOTIFY_USER_ID");
/// Authentication
const getToken = (clientId: string, clientSecret: string): Promise<string> =>
fetch("https://accounts.spotify.com/api/token", {
method: "post",
headers: {
Authorization: `Basic ${Base64.fromString(
clientId + ":" + clientSecret
).toString()}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
})
.then((x) => x.json())
.then((x) => x.access_token);
const token = await getToken(clientId, clientSecret);
/// Fetching
const fetchWithAuth = (url) =>
fetch(url, {
headers: { Authorization: `Bearer ${token}` },
}).then((x) => x.json());
async function fetchAll<T>(url: string): Promise<T[]> {
const { items, next } = await fetchWithAuth(url);
if (next) {
const more = await fetchAll(next);
if (more) return [...items, ...more];
else return items;
}
return items;
}
const getPlaylists = (user: string): Promise<object[]> =>
fetchAll(`https://api.spotify.com/v1/users/${user}/playlists?limit=50`);
const rawPlaylists = await getPlaylists(userId, token).then((playlists) =>
Promise.all(
playlists.map((playlist) =>
fetchAll(playlist.tracks.href).then((tracks) => ({
...playlist,
tracks,
}))
)
)
);
/// Cleaning
type URI = string;
interface Playlist {
uri: URI;
name: string;
description: string;
tracks: Track[];
owner: URI;
images: ImageObject[];
}
interface ImageObject {
url: URI;
}
interface Track {
uri: URI;
name: string;
artists: string[];
album: string;
added_at: Date;
added_by: URI;
}
const toTrack = (track: any): Track | null =>
track.track
? {
uri: track.track.uri,
name: track.track.name,
added_at: new Date(track.added_at),
added_by: track.added_by.uri,
album: track.track.album.name,
artists: track.track.artists.map((artist) => artist.name),
}
: null;
const toPlaylist = (playlist: any): Playlist => ({
uri: playlist.uri,
name: playlist.name,
description: playlist.description,
image: playlist.images[0].url,
owner: playlist.owner.uri,
tracks: playlist.tracks ? playlist.tracks.map(toTrack).filter(Boolean) : [],
});
const playlists: Playlist[] = rawPlaylists.map(toPlaylist);
playlists.sort((a: Playlist, b: Playlist) => {
if (a.uri < b.uri) return -1;
else if (a.uri > b.uri) return 1;
else if (a.uri === b.uri) return 0;
});
/// Output
const encoder = new TextEncoder();
for (const playlist of playlists) {
const directory = playlist.owner;
const fileName = playlist.uri + ".json";
const data = JSON.stringify(playlist, undefined, 2);
if (!(await exists(directory))) await Deno.mkdir(directory);
await Deno.writeFile(join(directory, fileName), encoder.encode(data));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment