Created
October 24, 2023 17:57
-
-
Save kmein/94df54ac477f1b1dad07078ae078b67c to your computer and use it in GitHub Desktop.
Script to archive your Spotify playlists
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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