Install
openclaw skills install beatport-dl-with-browser-toolDownload purchased tracks from Beatport using the openclaw headless browser tool (CDP). Handles login, authentication via NextAuth, enabling downloads in headless Chrome, and saving files locally. Use when the user asks to download music, tracks, or files from Beatport, or manage their Beatport purchases/library. Triggers on phrases like "download from beatport", "beatport download", "download my tracks", "get my beatport music".
openclaw skills install beatport-dl-with-browser-toolDownload 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)// 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)| Page | URL | Purpose |
|---|---|---|
| Cart | https://www.beatport.com/cart | Items pending purchase |
| Library | https://www.beatport.com/library | Purchased tracks (may show Upgrade for free accounts) |
| Downloads | https://www.beatport.com/library/downloads | Download queue |
| Checkout | https://www.beatport.com/checkout | Payment 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:
// 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:
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:
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).
cd /path/to/download/dir
unzip -o beatport_tracks_*.zip -d tmp/
mv tmp/*.mp3 .
rm -rf tmp/ beatport_tracks_*.zip
https://zips.beatport.com/v1/download?token=<JWT_TOKEN>
The token is single-use and expires quickly. Always capture fresh from events.
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.
curl -s -H "Cookie: <cookies>" \
"https://www.beatport.com/_next/data/<buildId>/en/library/downloads.json" \
| jq -r '.pageProps.accessToken'
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}'
curl -s "https://www.beatport.com/" | grep -o '"buildId":"[^"]*"' | head -1
Current buildId (subject to change): PWoDyRo_P5V8lNYu_92bX
Page.navigate — Use location.href = "..." via Runtime.evaluate instead.value = — Use native input value setter:
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 }));
-e — Use String.raw\...`template literals, or write code to a file and run withnode file.js`background: true + process poll for longer waitscurl path — Use /usr/bin/curl, not /opt/homebrew/bin/curl (may not exist)Write scripts to files to avoid shell escaping issues:
// 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 };