使用 openclaw 无头浏览器(CDP)从 Beatport 下载已购曲目,支持 NextAuth 登录认证,实现无头模式下载。
Download purchased Beatport tracks through the openclaw headless browser using CDP (Chrome DevTools Protocol).
127.0.0.1:9222ws module at /opt/homebrew/lib/node_modules/openclaw/node_modules/wsBeatport uses a dual-auth system:
sessionid cookie)__Secure-next-auth.session-token cookie)https://account.beatport.com/ via CDP Page.navigateRuntime.evaluate (use native input setters to bypass React controlled inputs)javascript
// In browser context on www.beatport.com
fetch("/api/auth/csrf").then(r => r.json()).then(csrf => {
const fd = new URLSearchParams();
fd.append("csrfToken", csrf.csrfToken);
fd.append("username", "USER");
fd.append("password", "PASS");
fd.append("callbackUrl", "https://www.beatport.com/");
// Create hidden form and submit (fetch redirect fails cross-origin)
const form = document.createElement("form");
form.method = "POST";
form.action = "/api/auth/signin/beatport";
form.style.display = "none";
for (const [k, v] of Object.entries(Object.fromEntries(fd))) {
const inp = document.createElement("input");
inp.type = "hidden"; inp.name = k; inp.value = v;
form.appendChild(inp);
}
document.body.appendChild(form);
form.submit();
});
Account menu button should appear in navbar (no Create Account or Log In button)PageURLPurposeCarthttps://www.beatport.com/cartItems pending purchaseLibraryhttps://www.beatport.com/libraryPurchased tracks (may show Upgrade for free accounts)Downloadshttps://www.beatport.com/library/downloadsDownload queueCheckouthttps://www.beatport.com/checkoutPayment page
Note: /my-beatport/downloads and /my-beatport/collection return 404. The correct paths are /library and /library/downloads.
Headless Chrome cancels downloads by default. Enable via CDP on the browser-level WebSocket:
javascript
// Browser-level WS: ws://127.0.0.1:9222/devtools/browser/<id>
ws.send(JSON.stringify({
id: 1,
method: "Browser.setDownloadBehavior",
params: {
behavior: "allowAndName",
downloadPath: "/path/to/download/dir/",
eventsEnabled: true
}
}));
Get browser ID from http://127.0.0.1:9222/json/version → webSocketDebuggerUrl.
On /library, each track has a re-download icon ( svg[data-testid='icon-re-download']). Click each one to add to the download queue:
javascript
var icons = document.querySelectorAll("svg[data-testid='icon-re-download']");
icons.forEach(function(icon, i) {
setTimeout(function() { icon.closest("button, div").click(); }, i * 500);
});
Navigate to /library/downloads. All queued tracks appear with a "Download All" button.
Enable browser downloads first (see above), then click:
javascript
var btn = [...document.querySelectorAll("button")].find(b => b.innerText.includes("Download All"));
if (btn) btn.click();
The download arrives as a zip file (e.g. beatport_tracks_2026-04.zip).
bash
cd /path/to/download/dir
unzip -o beatport_tracks_*.zip -d tmp/
mv tmp/*.mp3 .
rm -rf tmp/ beatport_tracks_*.zip
text
https://zips.beatport.com/v1/download?token=<JWT_TOKEN>
The token is single-use and expires quickly. Always capture fresh from events.
text
https://zips.beatport.com/v1/download?token=<JWT_TOKEN>
The token is single-use and expires quickly. Always capture it fresh from the Page.downloadWillBegin event.
bash
curl -s -H "Cookie: <cookies>" \
"https://www.beatport.com/_next/data/<buildId>/en/library/downloads.json" \
| jq -r '.pageProps.accessToken'
bash
curl -s -H "Cookie: <cookies>" \
"https://www.beatport.com/_next/data/<buildId>/en/library.json" \
| jq '.pageProps.dehydratedState.queries[].state.data.results[] | {name, id, artists}'
bash
curl -s "https://www.beatport.com/" | grep -o '"buildId":"[^"]*"' | head -1
Current buildId (subject to change): PWoDyRo_P5V8lNYu_92bX
Cross-domain navigation fails with Page.navigate — Use location.href = "..." via Runtime.evaluate instead
React controlled inputs don't respond to .value = — Use native input value setter:
javascript
var input = document.querySelector("input[name=username]");
var nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
nativeSetter.call(input, "username");
input.dispatchEvent(new Event("input", { bubbles: true }));
Node.js string escaping in -e — Use String.raw\...` template literals, or write code to a file and run with node file.js`
Free account download limit — 20 downloads per track. "Unlimited re-downloads" requires Beatport Streaming subscription
CDP exec timeout — openclaw kills long-running node processes (~10s). Keep CDP operations short; use background: true + process poll for longer waits
curl path — Use /usr/bin/curl, not /opt/homebrew/bin/curl (may not exist)
Write scripts to files to avoid shell escaping issues:
javascript
// scripts/beatport-cdp.js
const WS = require("/opt/homebrew/lib/node_modules/openclaw/node_modules/ws");
const http = require("http");
function getPage(filter) {
return new Promise((resolve) => {
http.get("http://127.0.0.1:9222/json", (res) => {
let body = "";
res.on("data", (c) => body += c);
res.on("end", () => {
const pages = JSON.parse(body).filter(p => p.type === "page");
resolve(filter ? pages.find(filter) || pages[0] : pages[0]);
});
});
});
}
function cdpEval(ws, expression) {
return new Promise((resolve) => {
ws.send(JSON.stringify({ id: Date.now(), method: "Runtime.evaluate", params: { expression, returnByValue: true } }));
ws.on("message", (m) => {
const d = JSON.parse(m.toString());
if (d.id && d.result) { resolve(d.result); }
});
});
}
async function screenshot(ws, path) {
return new Promise((resolve) => {
ws.send(JSON.stringify({ id: Date.now(), method: "Page.captureScreenshot", params: { format: "png" } }));
ws.on("message", (m) => {
const d = JSON.parse(m.toString());
if (d.id && d.result && d.result.data) {
require("fs").writeFileSync(path, Buffer.from(d.result.data, "base64"));
resolve();
}
});
});
}
module.exports = { getPage, cdpEval, screenshot };