commit 2b64a9aa17c4fe361ffce86d0dfa92159f4e7875 Author: oabrivard Date: Sat Mar 14 17:21:08 2026 +0100 Initial commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4961bc9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(node get_popularity.js \"Radiohead\")", + "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\")" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/get_popularity.js b/get_popularity.js new file mode 100644 index 0000000..aac2f67 --- /dev/null +++ b/get_popularity.js @@ -0,0 +1,396 @@ +const http = require("http"); +const { load } = require("cheerio"); +const Anthropic = require("@anthropic-ai/sdk"); +const OpenAI = require("openai"); + +const CLIENT_ID = "44e13265ab164d209a8665e7d0ae2237"; +const CLIENT_SECRET = "cbd639db2c6b4c01981e8dd0cb00569b"; +const REDIRECT_URI = "http://127.0.0.1:8888/callback"; + +// --- 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. + +Return ONLY a JSON array of artist names, nothing else. Example: ["Artist 1", "Artist 2"] + +If no artist names are found, return an empty array: [] + +Page text: +`; + +async function extractWithAnthropic(text) { + const client = new Anthropic(); + const message = await client.messages.create({ + model: "claude-haiku-4-5-20251001", + max_tokens: 1024, + messages: [{ role: "user", content: EXTRACTION_PROMPT + text }], + }); + return message.content[0].text.trim(); +} + +async function extractWithOpenAI(text) { + const client = new OpenAI(); + const completion = await client.chat.completions.create({ + model: "gpt-4o-mini", + max_tokens: 1024, + messages: [{ role: "user", content: EXTRACTION_PROMPT + text }], + }); + return completion.choices[0].message.content.trim(); +} + +const LLM_PROVIDERS = { + anthropic: extractWithAnthropic, + openai: extractWithOpenAI, +}; + +// --- Spotify Auth --- + +async function getAccessToken() { + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`), + }, + body: "grant_type=client_credentials", + }); + + const data = await response.json(); + return data.access_token; +} + +async function getUserAccessToken() { + const scopes = "playlist-modify-public playlist-modify-private"; + const authUrl = + `https://accounts.spotify.com/authorize?response_type=code` + + `&client_id=${CLIENT_ID}` + + `&scope=${encodeURIComponent(scopes)}` + + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`; + + console.log("\nOpen this URL in your browser to authorize:\n"); + console.log(authUrl + "\n"); + + const code = await new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const url = new URL(req.url, `http://127.0.0.1:8888`); + const code = url.searchParams.get("code"); + const error = url.searchParams.get("error"); + + if (error) { + res.writeHead(200, { "Content-Type": "text/html" }); + res.end("

Authorization denied.

You can close this tab.

"); + server.close(); + reject(new Error(`Authorization denied: ${error}`)); + return; + } + + if (code) { + res.writeHead(200, { "Content-Type": "text/html" }); + res.end("

Authorized!

You can close this tab.

"); + server.close(); + resolve(code); + } + }); + + server.listen(8888, () => { + console.log("Waiting for authorization..."); + }); + }); + + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`), + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: REDIRECT_URI, + }), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(`Token exchange failed: ${JSON.stringify(data)}`); + } + return data.access_token; +} + +// --- Spotify API --- + +async function searchArtist(token, artistName) { + const response = await fetch( + `https://api.spotify.com/v1/search?q=${encodeURIComponent(artistName)}&type=artist&limit=1`, + { headers: { Authorization: `Bearer ${token}` } } + ); + + const data = await response.json(); + const artist = data.artists.items[0]; + if (!artist) { + return null; + } + return artist; +} + +async function getTracksByArtist(token, artistName) { + const response = await fetch( + `https://api.spotify.com/v1/search?q=${encodeURIComponent(`artist:${artistName}`)}&type=track&limit=10`, + { headers: { Authorization: `Bearer ${token}` } } + ); + + const data = await response.json(); + if (!response.ok) { + throw new Error(`Spotify API error: ${JSON.stringify(data)}`); + } + 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) { + console.log(` Artist "${artistName}" not found on Spotify, skipping.`); + return []; + } + + const genres = artist.genres?.length ? artist.genres.join(", ") : "N/A"; + console.log(`\nArtist: ${artist.name} (${genres})`); + + const tracks = await getTracksByArtist(token, artistName); + const topTracks = tracks + .filter((t) => t.artists.some((a) => a.id === artist.id)) + .slice(0, topN); + + if (topTracks.length === 0) { + console.log(" No tracks found."); + 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}` : ""; + console.log( + ` ${i + 1}. "${track.name}"${popularity} (Album: ${track.album.name})` + ); + }); + + return topTracksWithDetails.map((t) => t.uri); +} + +// --- Playlist Creation --- + +function playlistNameFromUrl(url) { + return url + .replace(/^https?:\/\//, "") + .replace(/[<>:"\/\\|?*]+/g, "_") + .replace(/_+/g, "_") + .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`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + description: `Generated from ${name}`, + public: false, + }), + } + ); + + const playlist = await response.json(); + if (!response.ok) { + throw new Error(`Failed to create playlist: ${JSON.stringify(playlist)}`); + } + + // 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`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ uris: batch }), + } + ); + + if (!addResponse.ok) { + const err = await addResponse.json(); + throw new Error(`Failed to add tracks: ${JSON.stringify(err)}`); + } + } + + return playlist; +} + +// --- Web Scraping with LLM --- + +async function scrapeArtists(url, provider) { + console.log(`Fetching ${url}...`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status}`); + } + + const html = await response.text(); + const $ = load(html); + + $("script, style, nav, footer, header, noscript, svg").remove(); + const textContent = $("body").text().replace(/\s+/g, " ").trim(); + + const maxChars = 20000; + const truncated = + textContent.length > maxChars + ? textContent.slice(0, maxChars) + "..." + : textContent; + + console.log(`Extracting artist names with ${provider}...\n`); + + const extract = LLM_PROVIDERS[provider]; + const responseText = await extract(truncated); + + console.log(`Extracted artist names with ${provider} is ${responseText}.\n`); + + const jsonString = responseText.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, ""); + return JSON.parse(jsonString); +} + +// --- Main --- + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = { + provider: "anthropic", + url: null, + artist: null, + topN: 3, + createPlaylist: false, + }; + + const llmIndex = args.indexOf("--llm"); + if (llmIndex !== -1) { + parsed.provider = args[llmIndex + 1]; + if (!LLM_PROVIDERS[parsed.provider]) { + console.error( + `Unknown provider "${parsed.provider}". Available: ${Object.keys(LLM_PROVIDERS).join(", ")}` + ); + process.exit(1); + } + } + + const urlIndex = args.indexOf("--url"); + if (urlIndex !== -1) { + parsed.url = args[urlIndex + 1]; + } + + const topIndex = args.indexOf("--top"); + if (topIndex !== -1) { + parsed.topN = parseInt(args[topIndex + 1], 10); + } + + if (args.includes("--create_playlist")) { + if (!parsed.url) { + console.error("--create_playlist can only be used with --url"); + process.exit(1); + } + parsed.createPlaylist = true; + } + + // Artist is any arg that isn't a flag or flag value + const flagIndices = new Set(); + for (const flag of ["--llm", "--url", "--top"]) { + const idx = args.indexOf(flag); + if (idx !== -1) { + flagIndices.add(idx); + flagIndices.add(idx + 1); + } + } + const flagsWithoutValue = ["--create_playlist"]; + for (const flag of flagsWithoutValue) { + const idx = args.indexOf(flag); + if (idx !== -1) flagIndices.add(idx); + } + const positional = args.filter((_, i) => !flagIndices.has(i)); + if (positional.length > 0) { + parsed.artist = positional[0]; + } + + return parsed; +} + +async function main() { + const { provider, url, artist, topN, createPlaylist: shouldCreatePlaylist } = parseArgs(); + + if (url) { + const artistNames = await scrapeArtists(url, provider); + if (artistNames.length === 0) { + console.log("No artist names found on the page."); + return; + } + + console.log(`Found ${artistNames.length} artist(s): ${artistNames.join(", ")}\n`); + console.log("Looking up top tracks on Spotify..."); + + const token = await getAccessToken(); + const allTrackUris = []; + + for (const name of artistNames) { + const uris = await getTopTracks(token, name, topN); + allTrackUris.push(...uris); + } + + 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); + console.log(`\nPlaylist created: "${playlist.name}"`); + console.log(`URL: ${playlist.external_urls.spotify}`); + } + } else { + const artistName = artist || "Radiohead"; + const token = await getAccessToken(); + await getTopTracks(token, artistName, topN); + } +} + +main().catch(console.error); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bee6e7c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,384 @@ +{ + "name": "spotify", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spotify", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@anthropic-ai/sdk": "^0.78.0", + "cheerio": "^1.2.0", + "openai": "^6.29.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.78.0.tgz", + "integrity": "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/openai": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.29.0.tgz", + "integrity": "sha512-YxoArl2BItucdO89/sN6edksV0x47WUTgkgVfCgX7EuEMhbirENsgYe5oO4LTjBL9PtdKtk2WqND1gSLcTd2yw==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz", + "integrity": "sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..016a027 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "spotify", + "version": "1.0.0", + "description": "", + "main": "get_popularity.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@anthropic-ai/sdk": "^0.78.0", + "cheerio": "^1.2.0", + "openai": "^6.29.0" + } +}