Initial commit
commit
2b64a9aa17
@ -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\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@ -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("<h1>Authorization denied.</h1><p>You can close this tab.</p>");
|
||||||
|
server.close();
|
||||||
|
reject(new Error(`Authorization denied: ${error}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html" });
|
||||||
|
res.end("<h1>Authorized!</h1><p>You can close this tab.</p>");
|
||||||
|
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);
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue