Skip to content

Instantly share code, notes, and snippets.

@ritmo-v0
Last active June 9, 2026 18:59
Show Gist options
  • Select an option

  • Save ritmo-v0/a847d54c3e2637df25416713b85ea3ab to your computer and use it in GitHub Desktop.

Select an option

Save ritmo-v0/a847d54c3e2637df25416713b85ea3ab to your computer and use it in GitHub Desktop.
A simple TypeScript example of Spotify's `search` Web API.
import { readFile } from "fs/promises";
// Types & Interfaces
type Song = SongData & {
search_result: SearchResult[];
};
type SongData = {
name: string;
artist: string;
};
// # Modify based on your needs
type SearchResult = Omit<TrackItem, "type" | "artists" | "duration_ms" | "external_urls"> & {
artists: Array<string>;
};
// # Add more keys based on your needs
// Reference: https://developer.spotify.com/documentation/web-api/reference/search
type SpotifySearchResponse = {
tracks: {
items: Array<TrackItem>;
}
};
type TrackItem = {
id: string;
name: string;
type: "track";
artists: Array<SimplifiedArtist>;
uri: string;
href: string;
duration_ms: number;
external_urls: Record<string, string>;
};
type SimplifiedArtist = {
id: string;
name: string;
type: "artist";
uri: string;
href: string;
external_urls: Record<string, string>;
};
type SearchOptions = {
limit?: number;
};
// Constants & Variables
const JSON_FILE = "SONG_DATA.json";
// ! Plz make sure to load secrets from .env* files other than local usage
const CLIENT_ID = "YOUR_SPOTIFY_CLIENT_ID";
const CLIENT_SECRET = "YOUR_SPOTIFY_CLIENT_SECRET";
const REDIRECT_URI = "http://127.0.0.1:8080/callback";
const AUTH_URL = `https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&scope=playlist-modify-public%20playlist-modify-private`;
const CODE = "YOUR_CODE_FROM_AUTH_URL";
const BEARER_TOKEN = "YOUR_BEARER_TOKEN_FROM_getAccessToken";
// Request an Access Token
// Reference: https://developer.spotify.com/documentation/web-api/tutorials/getting-started#request-an-access-token
async function getAccessToken() {
const token = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
})
}).then(res => res.json());
return token.access_token;
}
async function getScopedAccessToken() {
const token = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Authorization": `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: CODE,
redirect_uri: REDIRECT_URI,
})
}).then(res => res.json());
return token.access_token;
}
// # Write your own data loading logic here
async function getSongData(): Promise<Array<SongData>> {
const data = await readFile(JSON_FILE, "utf8");
return JSON.parse(data);
}
// Fetch result from `Search` Web API
// ! You might consider adding a delay between (batch) searches to avoid 429s
async function fetchSongSearchResult(song: SongData, {
limit = 3,
}: SearchOptions = {}): Promise<Array<TrackItem>> {
const name = song.name;
const artist = song.artist;
const fetchURL = "https://api.spotify.com/v1/search?" + new URLSearchParams({
q: `track:${name} artist:${artist}`,
type: "track",
limit: String(limit),
}).toString();
const res = await fetch(fetchURL, {
headers: { "Authorization": `Bearer ${BEARER_TOKEN}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status} - ${res.statusText}`);
const data: SpotifySearchResponse = await res.json();
return data.tracks.items;
}
async function addSongToPlaylist(playlistId: string, uri: string | string[]) {
const uris = Array.isArray(uri) ? uri : [uri];
const res = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/tracks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${BEARER_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ uris }),
});
if (!res.ok) {
const errorBody = await res.text();
throw new Error(`HTTP ${res.status} - ${res.statusText}\n${errorBody}`);
}
console.log(`Added ${uris.length} songs to playlist ${playlistId}`);
}
async function fetchSongsFromPlaylist(playlistId: string) {
const tracks = []; // full track ID list in playlist order
const seenTrackIdToIndex = new Map(); // track.id => first index
const duplicates = {}; // id => { firstIndex, duplicateIndexes }
let offset = 0;
let hasMore = true;
while (hasMore) {
const res = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/tracks?limit=100&offset=${offset}`, {
headers: {
"Authorization": `Bearer ${BEARER_TOKEN}`,
}
});
const data = await res.json();
for (const item of data.items) {
const track = item.track;
if (!track) continue;
const id = track.id;
const currentIndex = tracks.length;
if (seenTrackIdToIndex.has(id)) {
const firstIndex = seenTrackIdToIndex.get(id);
if (!duplicates[id]) {
duplicates[id] = {
firstIndex,
duplicateIndexes: []
};
}
duplicates[id].duplicateIndexes.push(currentIndex);
} else {
seenTrackIdToIndex.set(id, currentIndex);
}
tracks.push(id);
}
hasMore = data.next !== null;
offset += 100;
}
return { tracks, duplicates };
}
// # Example Usage
// In this example, I have the order No. from "Ritmo's Secret Library" as the key of each SongData object,
// hence the return type `Record<string, SongData>` of `getSongData()`.
try {
// const token = await getAccessToken();
// console.log(`๐Ÿ”‘ Access Token: ${token}\n`);
const songs = await getSongData();
const data: any[] = [];
for (const [index, song] of songs.entries()) {
console.log(`๐Ÿ” Searching for song: ${song.name} / ${song.artist}...`);
const trackItems = await fetchSongSearchResult(song);
const search_result = trackItems.map(item => ({
id: item.id,
name: item.name,
artists: item.artists.map(artist => artist.name),
uri: item.uri,
}));
const fullSongData: Song = {
...song,
search_result,
};
data.push(fullSongData);
}
console.log(JSON.stringify(data, null, 4));
} catch (error) {
console.error("ERR:", error);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment