Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions adapters/spotify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
export const ID = "spotify";

const URL_RE = /^https?:\/\/open\.spotify\.com\/track\/([A-Za-z0-9]{22})/;

const CHROME_UA =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";

export function detect({ url, jsonld, meta }) {
const m = url.match(URL_RE);
if (!m) return null;
const id = m[1];
return {
adapter: ID,
kind: "track",
id,
url,
meta: meta || {},
jsonld: jsonld || [],
};
}

export async function extract(ctx) {
const tools = [];
const product = { type: "track", id: ctx.id, url: ctx.url };

const ogTitle = ctx.meta["og:title"] || "";
const ogDescription = ctx.meta["og:description"] || "";
const ogImage = ctx.meta["og:image"] || null;
const ogAudio = ctx.meta["og:audio"] || null;
const musicDuration = ctx.meta["music:duration"] || null;
const musicMusician = ctx.meta["music:musician"] || null;

let trackName = ogTitle;
let artists = [];
if (ogTitle.includes(" - ")) {
const parts = ogTitle.split(" - ");
trackName = parts[0].trim();
artists = parts.slice(1).map((a) => a.trim());
} else if (ogTitle.includes(" · ")) {
const parts = ogTitle.split(" · ");
trackName = parts[0].trim();
artists = parts.slice(1).map((a) => a.trim());
}

if (musicMusician) {
const musicianNames = Array.isArray(musicMusician) ? musicMusician : [musicMusician];
for (const m of musicianNames) {
const name = typeof m === "string" ? m : m?.name;
if (name && !artists.includes(name)) artists.push(name);
}
}

const trackInfo = {
name: trackName || null,
artists: artists.length ? artists : null,
album: ogDescription ? ogDescription.replace(/^from the album /i, "").replace(/\.$/, "") : null,
duration_seconds: musicDuration ? Number(musicDuration) : null,
preview_url: ogAudio || null,
image: ogImage,
spotify_url: ctx.url,
};
product.name = trackInfo.name;
product.artists = trackInfo.artists;

tools.push({
name: "get_track",
description: `Get track details for "${trackInfo.name || ctx.id}"${trackInfo.artists ? ` by ${trackInfo.artists.join(", ")}` : ""}.`,
inputSchema: { type: "object", properties: {}, required: [] },
result: trackInfo,
});

if (ogAudio) {
tools.push({
name: "get_preview",
description: `Get the 30-second preview URL for "${trackInfo.name || ctx.id}".`,
inputSchema: { type: "object", properties: {}, required: [] },
result: { preview_url: ogAudio, track: trackInfo.name },
});
}

tools.push({
name: "view_track",
description: `Return a direct link to "${trackInfo.name || ctx.id}" on Spotify.`,
inputSchema: { type: "object", properties: {}, required: [] },
result: { url: ctx.url, name: trackInfo.name },
});

return { product, variants: [], tools };
}

export const actions = {};
26 changes: 26 additions & 0 deletions worker/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as defillama from "../../adapters/defillama.js";
import * as dexscreener from "../../adapters/dexscreener.js";
import * as pyth from "../../adapters/pyth.js";
import * as chainlink from "../../adapters/chainlink.js";
import * as spotify from "../../adapters/spotify.js";
import { fetchAndParse } from "./html";
import { resolveTokenForUrl } from "./token_resolver";
import { loadProviderToken } from "./token_vault";
Expand Down Expand Up @@ -137,6 +138,18 @@ export async function resolveTools(
}
}

const spotifyCtx = spotify.detect({ url, jsonld: [], meta: {} });
if (spotifyCtx) {
try {
const data = await spotify.extract(spotifyCtx);
const payload: ToolsPayload = { adapter: "spotify", tools: data.tools, product: data.product };
ctx.waitUntil(writeCache(env, url, payload, 3600));
return { ok: true, payload, from: "live" };
} catch {
// fall through
}
}

const openapiCtx = openapi.detect({ url });
if (openapiCtx) {
try {
Expand Down Expand Up @@ -279,6 +292,19 @@ export async function executeTool(
}
}

const spCtx = spotify.detect({ url, jsonld: [], meta: {} });
if (spCtx) {
try {
const data = await spotify.extract(spCtx);
const tool = data.tools.find((t: any) => t.name === toolName);
if (!tool) return { ok: false, status: 404, body: { error: `tool ${toolName} not found` } };
if (tool.result !== undefined) return { ok: true, value: tool.result };
return { ok: false, status: 400, body: { error: "spotify adapter is read-only" } };
} catch (err: any) {
return { ok: false, status: 500, body: { ok: false, error: String(err?.message || err) } };
}
}

for (const { name, mod } of CRYPTO_ADAPTERS) {
const cctx = (mod as any).detect({ url });
if (!cctx) continue;
Expand Down