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 { 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 { 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); });