diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4961bc9..da2da46 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,8 @@ "Bash(node /Users/oabrivard/Projects/javascript/spotify/get_popularity.js \"Radiohead\")", "Bash(npm init:*)", "Bash(npm install:*)", - "Bash(node /Users/oabrivard/Projects/javascript/spotify/get_popularity.js --url \"https://www.rockenseine.com/programmation\")" + "Bash(node /Users/oabrivard/Projects/javascript/spotify/get_popularity.js --url \"https://www.rockenseine.com/programmation\")", + "WebFetch(domain:developer.spotify.com)" ] } } diff --git a/get_popularity.js b/get_popularity.js index aac2f67..4f97441 100644 --- a/get_popularity.js +++ b/get_popularity.js @@ -7,6 +7,29 @@ const CLIENT_ID = "44e13265ab164d209a8665e7d0ae2237"; const CLIENT_SECRET = "cbd639db2c6b4c01981e8dd0cb00569b"; const REDIRECT_URI = "http://127.0.0.1:8888/callback"; +// --- Rate Limiter --- + +function createRateLimiter(maxPerSecond) { + const minInterval = 1000 / maxPerSecond; + let lastCall = 0; + + return async function throttle() { + const now = Date.now(); + const elapsed = now - lastCall; + if (elapsed < minInterval) { + await new Promise((resolve) => setTimeout(resolve, minInterval - elapsed)); + } + lastCall = Date.now(); + }; +} + +const spotifyThrottle = createRateLimiter(2); + +async function spotifyFetch(url, options = {}) { + await spotifyThrottle(); + return fetch(url, options); +} + // --- LLM Providers --- const EXTRACTION_PROMPT = `Extract all music artist or band names from this web page text. The page is about concerts, festivals, or music venues. @@ -120,11 +143,12 @@ async function getUserAccessToken() { // --- Spotify API --- async function searchArtist(token, artistName) { - const response = await fetch( + const response = await spotifyFetch( `https://api.spotify.com/v1/search?q=${encodeURIComponent(artistName)}&type=artist&limit=1`, { headers: { Authorization: `Bearer ${token}` } } ); + //console.log(`Server response for "${artistName}" on Spotify:`, response.headers, response.status, response.statusText, await response.text()); const data = await response.json(); const artist = data.artists.items[0]; if (!artist) { @@ -134,8 +158,8 @@ async function searchArtist(token, artistName) { } async function getTracksByArtist(token, artistName) { - const response = await fetch( - `https://api.spotify.com/v1/search?q=${encodeURIComponent(`artist:${artistName}`)}&type=track&limit=10`, + const response = await spotifyFetch( + `https://api.spotify.com/v1/search?q=${encodeURIComponent(`artist:${artistName}`)}&type=track%2Cshow&limit=10`, { headers: { Authorization: `Bearer ${token}` } } ); @@ -146,19 +170,6 @@ async function getTracksByArtist(token, artistName) { return data.tracks.items; } -async function getTrack(token, trackId) { - const response = await fetch( - `https://api.spotify.com/v1/tracks/${trackId}`, - { headers: { Authorization: `Bearer ${token}` } } - ); - - const data = await response.json(); - if (!response.ok) { - throw new Error(`Spotify API error: ${JSON.stringify(data)}`); - } - return data; -} - async function getTopTracks(token, artistName, topN = 3) { const artist = await searchArtist(token, artistName); if (!artist) { @@ -166,8 +177,7 @@ async function getTopTracks(token, artistName, topN = 3) { return []; } - const genres = artist.genres?.length ? artist.genres.join(", ") : "N/A"; - console.log(`\nArtist: ${artist.name} (${genres})`); + console.log(`\nArtist: ${artist.name}`); const tracks = await getTracksByArtist(token, artistName); const topTracks = tracks @@ -179,19 +189,13 @@ async function getTopTracks(token, artistName, topN = 3) { return []; } - const topTracksWithDetails = await Promise.all( - topTracks.map((t) => getTrack(token, t.id)) - ); - - topTracksWithDetails.forEach((track, i) => { - const popularity = - track.popularity !== undefined ? ` - Popularity: ${track.popularity}` : ""; + topTracks.forEach((track, i) => { console.log( - ` ${i + 1}. "${track.name}"${popularity} (Album: ${track.album.name})` + ` ${i + 1}. "${track.name}" (Album: ${track.album.name})` ); }); - return topTracksWithDetails.map((t) => t.uri); + return topTracks.map((t) => t.uri); } // --- Playlist Creation --- @@ -204,20 +208,10 @@ function playlistNameFromUrl(url) { .replace(/^_|_$/g, ""); } -async function getCurrentUserId(token) { - const response = await fetch("https://api.spotify.com/v1/me", { - headers: { Authorization: `Bearer ${token}` }, - }); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to get user profile: ${JSON.stringify(data)}`); - } - return data.id; -} - -async function createPlaylist(token, userId, name, trackUris) { - const response = await fetch( - `https://api.spotify.com/v1/users/${userId}/playlists`, +async function createPlaylist(token, name, trackUris) { + // Use POST /me/playlists (Feb 2026: /users/{id}/playlists removed) + const response = await spotifyFetch( + `https://api.spotify.com/v1/me/playlists`, { method: "POST", headers: { @@ -237,11 +231,12 @@ async function createPlaylist(token, userId, name, trackUris) { throw new Error(`Failed to create playlist: ${JSON.stringify(playlist)}`); } + // Use POST /playlists/{id}/items (Feb 2026: /playlists/{id}/tracks removed) // Spotify allows max 100 tracks per request for (let i = 0; i < trackUris.length; i += 100) { const batch = trackUris.slice(i, i + 100); - const addResponse = await fetch( - `https://api.spotify.com/v1/playlists/${playlist.id}/tracks`, + const addResponse = await spotifyFetch( + `https://api.spotify.com/v1/playlists/${playlist.id}/items`, { method: "POST", headers: { @@ -380,9 +375,8 @@ async function main() { if (shouldCreatePlaylist && allTrackUris.length > 0) { console.log(`\nCreating playlist with ${allTrackUris.length} tracks...`); const userToken = await getUserAccessToken(); - const userId = await getCurrentUserId(userToken); const playlistName = playlistNameFromUrl(url); - const playlist = await createPlaylist(userToken, userId, playlistName, allTrackUris); + const playlist = await createPlaylist(userToken, playlistName, allTrackUris); console.log(`\nPlaylist created: "${playlist.name}"`); console.log(`URL: ${playlist.external_urls.spotify}`); }