team-4/backend/spotify-api.ts
2025-08-02 13:28:10 +02:00

136 lines
3.6 KiB
TypeScript

import { assert } from "jsr:@std/assert/assert";
import { SpotifySearchResponse, SpotifyTrack } from "./types/spotify.ts";
import { SongSearch } from "./types/ai.ts";
export async function searchSpotifyTracks(
songs: SongSearch[],
): Promise<SpotifyTrack[]> {
const results: SpotifyTrack[] = [];
const trackPromises = songs.map(async (song) => {
try {
const query = `track:"${song.title}" artist:"${song.artist}"`;
const params = new URLSearchParams({
q: query,
type: "track",
market: "US",
limit: "1",
offset: "0",
});
const url = `https://api.spotify.com/v1/search?${params.toString()}`;
const response = await fetch(url, {
headers: {
"Authorization": "Bearer " + await getSpotifyAccessToken(),
},
});
if (!response.ok) {
throw Error("Unsuccesful response");
}
const data: SpotifySearchResponse = await response.json();
const track = data.tracks.items.at(0);
if (track) {
return track;
}
} catch (error) {
console.error(error);
}
return undefined;
});
const tracks = await Promise.all(trackPromises);
for (const track of tracks) {
if (track) {
results.push(track);
}
}
return results;
}
/**
* Get Spotify access token using Client Credentials Flow.
* Reads credentials from environment variables, caches token in memory with expiry.
* Throws if credentials are missing or request fails.
*/
let spotifyTokenCache: { token: string; expiresAt: number } | null = null;
export async function getSpotifyAccessToken(): Promise<string> {
const clientId = Deno.env.get("SPOTIFY_CLIENT_ID");
const clientSecret = Deno.env.get("SPOTIFY_CLIENT_SECRET");
if (!clientId || !clientSecret) {
throw new Error("Spotify credentials not found in environment variables.");
}
// If cached and not expired, return cached token
if (
spotifyTokenCache &&
spotifyTokenCache.expiresAt > Date.now()
) {
return spotifyTokenCache.token;
}
const tokenResponse = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic " +
btoa(`${clientId}:${clientSecret}`),
},
body: "grant_type=client_credentials",
});
if (!tokenResponse.ok) {
throw new Error(
`Failed to fetch Spotify access token: ${tokenResponse.status} ${await tokenResponse
.text()}`,
);
}
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
const expiresIn = tokenData.expires_in; // seconds
if (!accessToken || !expiresIn) {
throw new Error("Invalid token response from Spotify.");
}
// Cache token with expiry
spotifyTokenCache = {
token: accessToken,
expiresAt: Date.now() + expiresIn * 1000 - 10_000, // 10s early
};
return accessToken;
}
Deno.test("getSpotifyAccessToken caching", async () => {
await getSpotifyAccessToken();
const start2 = performance.now();
const token2 = await getSpotifyAccessToken();
const elapsed2 = performance.now() - start2;
assert(elapsed2 < 10, `Second call took too long: ${elapsed2}ms`);
});
Deno.test("searchSpotifyTracks basic", async () => {
const songs = [
{ title: "Blinding Lights", artist: "The Weeknd" },
{ title: "Levitating", artist: "Dua Lipa" },
];
const spotify_api_key = getSpotifyAccessToken();
if (!spotify_api_key) throw Error("Testing token not found");
const tracks = await searchSpotifyTracks(songs);
console.log(tracks);
assert(tracks.length > 0);
});