136 lines
3.6 KiB
TypeScript
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);
|
|
});
|