Skip to content

Instantly share code, notes, and snippets.

@onegentig
Last active August 20, 2025 20:58
Show Gist options
  • Save onegentig/8c4289da9f9708b3341010a5b0a7ff2d to your computer and use it in GitHub Desktop.
Save onegentig/8c4289da9f9708b3341010a5b0a7ff2d to your computer and use it in GitHub Desktop.
#!/usr/bin/env -S deno run --allow-env --allow-net
/**
* Spotify Album Popularity Fetcher
* @file spotify-album-pop.ts
* @author onegen (https://github.com/onegentig)
*
* A very simple script that searches for an album on Spotify and shows
* the popularity of all its tracks. I wanted something like this and most
* 3rd party tools provided it on the track level, which was annoying to me.
* I am also not a Spotify user so it takes the search term not an ID.
*
* You need a Spotify API key to use this script. The script fetches
* the stuff from env vars SPOTIFY_APP_ID and SPOTIFY_APP_SECRET.
* @see https://developer.spotify.com/dashboard
*
* @note Script searches for the album and uses the first result only.
*
* Usage: ./spotify-album-pop.ts "Album Name"
*/
import { MultiProgressBar } from "jsr:@deno-library/progress"; // Bar display
/* === Interfaces === */
interface ResultListing<T> {
href: string; // endpoint link
limit: number;
next?: string; // next page link
offset: number;
previous?: string; // previous page link
total: number;
items: Array<T>;
}
interface SimplifiedArtist {
external_urls: Record<string, string>;
href: string;
id: string;
name: string;
type: "artist";
uri: string;
}
interface Track {
album: SimplifiedAlbum;
artists: Array<SimplifiedArtist>;
available_markets: Array<string>;
disc_number: number;
duration_ms: number;
explicit: boolean;
external_ids: Record<string, string>;
external_urls: Record<string, string>;
href: string; // endpoint link
id: string; // Spotify ID
is_playable: boolean;
linked_from: unknown; // not important
restrictions: { reason: "market" | "product" | "explicit"; };
name: string;
popularity: number; // THE THING WE WANT
track_number: number;
type: "track";
uri: string; // Spotify URI
is_local: boolean;
}
interface Album {
album_type: "album" | "single" | "compilation";
total_tracks: number;
available_markets: Array<string>;
external_urls: Record<string, string>;
href: string; // Spotify ID
images: Array<unknown>; // not important
name: string;
release_date: string;
release_date_precision: "year" | "month" | "day";
restrictions: { reason: "market" | "product" | "explicit"; };
type: "album";
artists: Array<SimplifiedArtist>;
tracks: ResultListing<SimplifiedTrack>;
copyrights: Array<unknown>; // Don’t care
external_ids: Record<string, string>;
genres: Array<string>;
popularity: number;
}
interface SearchResult {
tracks?: ResultListing<SimplifiedTrack>;
artists?: ResultListing<SimplifiedArtist>;
albums?: ResultListing<SimplifiedAlbum>;
playlists?: ResultListing<unknown>; // not important
shows?: ResultListing<unknown>; // not important
episodes?: ResultListing<unknown>; // not important
audiobooks?: ResultListing<unknown>; // not important
}
type SimplifiedTrack = Omit<Track, "album" | "external_ids" | "popularity" | "linked_from">;
type SimplifiedAlbum = Omit<Album, "id" | "tracks" | "external_ids" | "genres" | "popularity"> & {
id: string;
};
type BarChartItem = { name: string; value: number; };
/* === Globals === */
let token: string | null = null;
let tokenExpires: number = 0;
/* === Helper Functions === */
async function getAccessToken(): Promise<string> {
if (token && Date.now() < tokenExpires - 5000)
return token;
const id: string | undefined = Deno.env.get("SPOTIFY_APP_ID");
const secret: string | undefined = Deno.env.get("SPOTIFY_APP_SECRET");
if (!id || !secret)
throw new Error("Spotify API credentials not found in environment variables.");
const req = new URLSearchParams({ grant_type: "client_credentials" });
const res = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Authorization": `Basic ${btoa(`${id}:${secret}`)}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: req.toString(),
});
if (!res.ok)
throw new Error(`Failed to fetch access token: ${res.status} ${res.statusText}`);
const data = await res.json();
token = data.access_token;
tokenExpires = Date.now() + (data.expires_in * 1000);
return token!;
}
async function searchAlbum(name: string): Promise<SimplifiedAlbum> {
const bearer = await getAccessToken();
const req = new URLSearchParams({
q: name,
type: "album",
limit: "1"
});
const res = await fetch(`https://api.spotify.com/v1/search?${req.toString()}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${bearer}`,
}
});
if (!res.ok)
throw new Error(`Failed to search for album: ${res.status} ${res.statusText}`);
const data: SearchResult = await res.json();
if (!data.albums || data.albums.items.length === 0)
throw new Error("No albums found for the given name.");
const album: SimplifiedAlbum = data.albums.items[0];
return album;
}
async function fetchAlbum(id: string): Promise<Album> {
const bearer = await getAccessToken();
const res = await fetch(`https://api.spotify.com/v1/albums/${id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${bearer}`,
}
});
if (!res.ok)
throw new Error(`Failed to fetch album: ${res.status} ${res.statusText}`);
const data: Album = await res.json();
return data;
}
async function fetchTracks(ids: Array<string>): Promise<Array<Track>> {
if (ids.length === 0)
return [];
if (ids.length > 50)
throw new Error("Too many tracks requested at once. Maximum is 50.");
const bearer = await getAccessToken();
const req = new URLSearchParams({
ids: ids.join(","),
});
const res = await fetch(`https://api.spotify.com/v1/tracks?${req.toString()}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${bearer}`,
}
});
if (!res.ok)
throw new Error(`Failed to fetch tracks: ${res.status} ${res.statusText}`);
const data: { tracks: Array<Track>; } = await res.json();
return data.tracks;
}
async function fetchAlbumTracks(album: Album): Promise<Array<Track>> {
if (album.tracks.total === 0)
return [];
const ids: Array<string> = [];
album.tracks.items.forEach(track => ids.push(track.id));
let next: string | null = album.tracks.next ?? null;
while (next) {
const res = await fetch(next, {
method: "GET",
headers: {
"Authorization": `Bearer ${await getAccessToken()}`
}
});
if (!res.ok)
throw new Error(`Failed to fetch more tracks: ${res.status} ${res.statusText}`);
const data: ResultListing<SimplifiedTrack> = await res.json();
data.items.forEach(track => ids.push(track.id));
next = data.next ?? null;
}
const tracks: Array<Track> = [];
do {
const batch = ids.splice(0, 50);
const fetchedTracks = await fetchTracks(batch);
tracks.push(...fetchedTracks);
} while (ids.length > 0);
return tracks;
}
async function displayPopularityBars(trachs: Array<Track>): Promise<void> {
const items: Array<BarChartItem> = trachs.map(track => ({
name: " " + track.name,
value: track.popularity
}));
const cols = (Deno.consoleSize?.().columns ?? 80) - 5;
const nameW = Math.min(32, Math.floor(cols * 0.40));
const barW = Math.max(10, cols - nameW - 6 - 3);
const pad = (s: string) => s.padEnd(nameW).slice(0, nameW);
const bars = new MultiProgressBar({
width: barW,
display: ":text | :bar | :percent",
complete: "█", incomplete: " "
});
await bars.render(items.map(({ name, value }) => ({
completed: Math.max(0, Math.min(100, Math.round(value))),
total: 100,
text: `${pad(name)}`
})));
await bars.end();
}
/* === Main FN === */
async function main(albumName: string): Promise<void> {
try {
const album = await searchAlbum(albumName);
const fullAlbum = await fetchAlbum(album.id);
const tracks = await fetchAlbumTracks(fullAlbum);
console.info(`Album: ${fullAlbum.name} by ${fullAlbum.artists.map(a => a.name).join(", ")}`);
console.info(`Release Date: ${fullAlbum.release_date}`);
console.info(`Total Tracks: ${fullAlbum.total_tracks}`);
console.info(`Popularity: ${fullAlbum.popularity}`);
/*console.info("Tracks:");
tracks.forEach(track => {
console.info(` - ${track.name} (Popularity: ${track.popularity})`);
});*/
await displayPopularityBars(tracks);
} catch (error) {
console.error(error);
}
}
if (import.meta.main) {
const args = Deno.args;
if (args.length === 0) {
console.error("Usage: ./spotify-album-pop.ts \"Album Name\"");
Deno.exit(1);
}
const albumName = args.join(" ");
await main(albumName);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment