LOADING
Spotify search track, album, artist, playlist, download track, with metadata, info lanjut cek readme
/**
* SpotubeDL — Node ESM
* Spotify metadata (spotubedl) + multi-source audio resolve (no yt-dlp / no ffmpeg / no key)
*
* node spotubedl.mjs search [track|album|playlist|artist] <query>
* node spotubedl.mjs <spotify-url|id>
* node spotubedl.mjs download <url|id> [out.mp3]
* node spotubedl.mjs album|playlist <url|id>
*/
import crypto from 'node:crypto';
import { writeFile } from 'node:fs/promises';
const BASE = 'https://spotubedl.com';
const te = new TextEncoder(), td = new TextDecoder();
const rand = (n) => crypto.randomBytes(n);
const b64 = (b) => Buffer.from(b).toString('base64');
const unb64 = (s) => Buffer.from(String(s).trim(), 'base64');
const hex = (b) => Buffer.from(b).toString('hex');
const cat = (...a) => Buffer.concat(a.map((x) => (Buffer.isBuffer(x) ? x : Buffer.from(x))));
const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n >>> 0); return b; };
const rpath = () => `/${hex(rand(6))}/${hex(rand(8))}/${hex(rand(6))}`;
const rhdr = () => 'x-' + hex(rand(8));
function info(kind, path, rid, ver, exp) {
const p = { env: 'spdl-env:1:', req: 'spdl-req:1:', aad: 'spdl-aad:', raqd: 'spdl-req-aad:' }[kind];
if (kind === 'env' || kind === 'req') return cat(te.encode(p), te.encode(path), Buffer.from([0]), rid);
return cat(te.encode(p), Buffer.from([ver, 0, 0, 0, 0]), u32(exp), te.encode(path), Buffer.from([0]), rid);
}
function keyPair() { return crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); }
function exportPub(pub) {
const j = pub.export({ format: 'jwk' });
return cat(Buffer.from([4]), Buffer.from(j.x, 'base64url'), Buffer.from(j.y, 'base64url'));
}
function importPub(raw) {
if (raw.length !== 65 || raw[0] !== 4) throw new Error('bad pubkey');
return crypto.createPublicKey({
key: { kty: 'EC', crv: 'P-256', x: raw.subarray(1, 33).toString('base64url'), y: raw.subarray(33).toString('base64url') },
format: 'jwk',
});
}
function shared(priv, peer) { return crypto.diffieHellman({ privateKey: priv, publicKey: importPub(peer) }); }
function hkdf(sec, salt, i) { return crypto.hkdfSync('sha256', sec, salt, i, 32); }
function gcmEnc(key, iv, pt, aad) {
const c = crypto.createCipheriv('aes-256-gcm', key, iv); c.setAAD(aad);
return cat(c.update(pt), c.final(), c.getAuthTag());
}
function gcmDec(key, iv, ct, aad) {
const d = crypto.createDecipheriv('aes-256-gcm', key, iv);
d.setAAD(aad); d.setAuthTag(ct.subarray(-16));
return cat(d.update(ct.subarray(0, -16)), d.final());
}
function pack(e) {
return cat(Buffer.from([e.v, e.f]), u32(e.exp), e.rid, e.salt, e.pub, e.iv, u32(e.ct.length), e.ct);
}
function unpack(buf) {
if (buf.length < 119) throw new Error('envelope short');
let o = 0;
const v = buf[o++], f = buf[o++], exp = buf.readUInt32BE(o); o += 4;
const rid = buf.subarray(o, (o += 16));
const salt = buf.subarray(o, (o += 16));
const pub = buf.subarray(o, (o += 65));
const iv = buf.subarray(o, (o += 12));
const len = buf.readUInt32BE(o); o += 4;
const ct = buf.subarray(o);
if (ct.length !== len) throw new Error('ct mismatch');
return { v, f, exp, rid, salt, pub, iv, ct };
}
/* endpoints dipecah + di-xor biar gak kebaca string plain di source */
function _x(arr, k = 0x5a) {
return String.fromCharCode(...arr.map((c) => c ^ k));
}
// "https://loader.to" dll
const _EP = [
_x([0x32,0x2e,0x2e,0x2a,0x29,0x60,0x75,0x75,0x36,0x35,0x3b,0x3e,0x3f,0x28,0x74,0x2e,0x35]),
_x([0x32,0x2e,0x2e,0x2a,0x29,0x60,0x75,0x75,0x2d,0x2d,0x2d,0x74,0x36,0x35,0x3b,0x3e,0x3f,0x28,0x74,0x2e,0x35]),
_x([0x32,0x2e,0x2e,0x2a,0x29,0x60,0x75,0x75,0x2a,0x74,0x29,0x3b,0x2c,0x3f,0x34,0x35,0x2d,0x74,0x2e,0x35]),
_x([0x32,0x2e,0x2e,0x2a,0x29,0x60,0x75,0x75,0x3f,0x34,0x74,0x36,0x35,0x3b,0x3e,0x3f,0x28,0x74,0x2e,0x35]),
];
const _PATH = _x([0x75,0x3b,0x30,0x3b,0x22,0x75,0x3e,0x35,0x2d,0x34,0x36,0x35,0x3b,0x3e,0x74,0x2a,0x32,0x2a]); // /ajax/download.php
const UA =
'Mozilla/5.0 (Linux; Android 14; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36';
async function resolveFromMirror(youtubeUrl, format = 'mp3') {
let last;
for (const host of _EP) {
try {
const url = `${host}${_PATH}?format=${format}&url=${encodeURIComponent(youtubeUrl)}`;
const init = await fetch(url, {
headers: {
'User-Agent': UA,
Accept: 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
Referer: host + '/',
Origin: host,
},
signal: AbortSignal.timeout(25000),
});
if (!init.ok) throw new Error(`http ${init.status}`);
const j = await init.json();
if (!j.success || !j.progress_url) throw new Error('bad init');
for (let i = 0; i < 40; i++) {
await sleep(1100);
const pr = await fetch(j.progress_url, {
headers: { 'User-Agent': UA, Accept: 'application/json' },
signal: AbortSignal.timeout(12000),
});
const p = await pr.json();
const dl = p.download_url || p.url;
if ((p.success == 1 || Number(p.progress) >= 1000) && dl) {
return { downloadUrl: dl, title: p.title || j.title || null };
}
}
throw new Error('timeout');
} catch (e) {
last = e;
}
}
throw last || new Error('mirror fail');
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
export class SpotubeDL {
constructor(base = BASE) {
this.base = base.replace(/\/$/, '');
this.serverKey = null;
this.hdrs = {
'User-Agent': UA,
Origin: this.base,
Referer: this.base + '/',
};
}
async serverPub() {
if (this.serverKey) return this.serverKey;
const res = await fetch(this.base + rpath(), {
headers: { ...this.hdrs, Accept: 'text/plain', 'Cache-Control': 'no-store' },
cache: 'no-store',
});
if (!res.ok) throw new Error(`key ${res.status}`);
const k = unb64(await res.text());
if (k.length !== 65) throw new Error('bad key');
return (this.serverKey = k);
}
async securePost(apiPath, body = null) {
const postPath = rpath(), postUrl = this.base + postPath;
const payload = JSON.stringify({
path: apiPath.startsWith('/') ? apiPath : '/' + apiPath,
method: 'POST',
body,
});
const serverPub = await this.serverPub();
const { privateKey, publicKey } = keyPair();
const clientPub = exportPub(publicKey);
const sh = shared(privateKey, serverPub);
const rid = rand(16), salt = rand(16), iv = rand(12);
const exp = Math.floor(Date.now() / 1000) + 60;
const key = hkdf(sh, salt, info('req', postPath, rid));
const aad = info('raqd', postPath, rid, 1, exp);
const ct = gcmEnc(key, iv, Buffer.from(payload), aad);
const envelope = pack({ v: 1, f: 2, exp, rid, salt, pub: clientPub, iv, ct });
const res = await fetch(postUrl, {
method: 'POST',
headers: {
...this.hdrs,
[rhdr()]: b64(clientPub),
'Content-Type': 'application/octet-stream',
Accept: 'application/octet-stream, application/json',
},
body: envelope,
redirect: 'follow',
});
const ctype = (res.headers.get('content-type') || '').toLowerCase();
if (!ctype.includes('octet-stream')) {
let data; try { data = await res.json(); } catch { data = await res.text(); }
return { ok: res.ok, status: res.status, data };
}
const enc = Buffer.from(await res.arrayBuffer());
const respPath = new URL(res.url || postUrl).pathname || postPath;
const env = unpack(enc);
if (env.v !== 1) throw new Error('bad env');
const key2 = hkdf(shared(privateKey, env.pub), env.salt, info('env', respPath, env.rid));
const plain = gcmDec(key2, env.iv, env.ct, info('aad', respPath, env.rid, env.v, env.exp));
return { ok: res.ok, status: res.status, data: JSON.parse(td.decode(plain)) };
}
getTrack(id) { return this.securePost('/api/info/track', { id: sid(id, 'track') }); }
getMetadata(id) { return this.securePost('/api/metadata', { id: sid(id, 'track') }); }
getAlbum(id) { return this.securePost('/api/info/album', { id: sid(id, 'album') }); }
getPlaylist(id) { return this.securePost('/api/info/playlist', { id: sid(id, 'playlist') }); }
getArtist(id) { return this.securePost('/api/info/artist', { id: sid(id, 'artist') }); }
search(q) { return this.securePost('/api/info/search', { query: q }); }
/** deteksi jenis dari URL/id lalu ambil metadata */
async getInfo(input) {
const kind = detectKind(input);
if (kind === 'album') {
const r = await this.getAlbum(input);
if (!r.ok) throw new Error(typeof r.data === 'string' ? r.data : JSON.stringify(r.data));
return { kind: 'album', data: r.data };
}
if (kind === 'playlist') {
const r = await this.getPlaylist(input);
if (!r.ok) throw new Error(typeof r.data === 'string' ? r.data : JSON.stringify(r.data));
return { kind: 'playlist', data: r.data };
}
if (kind === 'artist') {
const r = await this.getArtist(input);
if (!r.ok) throw new Error(typeof r.data === 'string' ? r.data : JSON.stringify(r.data));
return { kind: 'artist', data: r.data };
}
// default track
const r = await this.getTrack(input);
if (!r.ok) throw new Error(typeof r.data === 'string' ? r.data : JSON.stringify(r.data));
return { kind: 'track', data: r.data };
}
getDownload(videoId, { engine = 'v5', format = 'mp3', quality = '320' } = {}) {
return this.securePost('/api/download', { id: videoId, engine, format, quality });
}
/** metadata + resolve direct audio url (multi-source) */
async downloadTrack(input, opts = {}) {
const id = sid(input, 'track');
const track = await this.getTrack(id);
if (!track.ok) throw new Error(JSON.stringify(track.data));
const full = await this.getMetadata(id);
const vids = ytIds(track.data, full.data);
if (!vids.length) throw new Error('youtube id tidak ditemukan');
let downloadUrl = null;
let videoId = vids[0];
let source = null;
// 1) coba lewat spotubedl engine (CDN signed URL)
const engines = opts.engine ? [opts.engine] : ['v5', 'v4', 'v3', 'v2', 'v1'];
for (const vid of vids) {
for (const engine of engines) {
try {
const dl = await this.getDownload(vid, { engine, format: 'mp3', quality: opts.quality || '320' });
const u = pickUrl(dl.data);
if (u) {
// cek apakah URL bisa diakses (sample)
const ok = await probeAudio(u);
if (ok) {
downloadUrl = u;
videoId = vid;
source = 'cdn';
break;
}
}
} catch {}
}
if (downloadUrl) break;
}
// 2) fallback mirror resolve
if (!downloadUrl) {
let last;
for (const vid of vids) {
try {
const r = await resolveFromMirror(`https://www.youtube.com/watch?v=${vid}`, 'mp3');
downloadUrl = r.downloadUrl;
videoId = vid;
source = 'mirror';
break;
} catch (e) {
last = e;
}
}
if (!downloadUrl) throw new Error('gagal resolve audio: ' + (last?.message || 'all sources failed'));
}
return {
spotifyId: id,
videoId,
downloadUrl,
filename: sanitize(`${track.data?.name || id} - ${track.data?.artist || 'unknown'}.mp3`),
track: track.data,
metadata: full.data,
source,
youtubeUrl: `https://www.youtube.com/watch?v=${videoId}`,
};
}
async saveTrack(input, outPath, opts = {}) {
const info = await this.downloadTrack(input, opts);
const name = outPath || info.filename;
const buf = await this.fetchUrl(info.downloadUrl);
if (!isAudio(buf)) throw new Error('file bukan audio valid');
await writeFile(name, buf);
return { ...info, savedAs: name, bytes: buf.length };
}
async fetchUrl(url) {
const res = await fetch(url, {
headers: { 'User-Agent': UA, Accept: '*/*' },
redirect: 'follow',
signal: AbortSignal.timeout(180000),
});
if (!(res.ok || res.status === 206)) throw new Error(`HTTP ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
}
async function probeAudio(url) {
try {
const res = await fetch(url, {
headers: { 'User-Agent': UA, Accept: '*/*', Range: 'bytes=0-2047' },
redirect: 'follow',
signal: AbortSignal.timeout(12000),
});
if (!(res.ok || res.status === 206)) return false;
const buf = Buffer.from(await res.arrayBuffer());
return isAudio(buf);
} catch {
return false;
}
}
function detectKind(input) {
const raw = String(input).trim();
try {
const path = new URL(raw).pathname.toLowerCase();
if (path.includes('/playlist/')) return 'playlist';
if (path.includes('/album/')) return 'album';
if (path.includes('/artist/')) return 'artist';
if (path.includes('/track/')) return 'track';
} catch {}
// bare id: default track (spotify ids tidak unik per type di client ini)
return 'track';
}
function sid(input, type = 'track') {
const raw = String(input).trim();
if (/^[a-zA-Z0-9]{22}$/.test(raw)) return raw;
try {
const p = new URL(raw).pathname.split('/').filter(Boolean);
const i = p.indexOf(type);
if (i >= 0 && p[i + 1]) return p[i + 1].split('?')[0];
const last = p.at(-1)?.split('?')[0];
if (last && /^[a-zA-Z0-9]{22}$/.test(last)) return last;
} catch {}
const m = raw.match(/[a-zA-Z0-9]{22}/);
if (m) return m[0];
throw new Error('spotify id tidak ditemukan: ' + input);
}
function ytId(s) {
if (typeof s !== 'string') return null;
s = s.trim();
if (/^[a-zA-Z0-9_-]{11}$/.test(s)) return s;
const m = s.match(/(?:youtu\.be\/|v=|\/embed\/)([a-zA-Z0-9_-]{11})/);
return m ? m[1] : null;
}
function walk(o, fn) {
if (o == null) return;
if (typeof o === 'string') return fn(o);
if (Array.isArray(o)) return o.forEach((x) => walk(x, fn));
if (typeof o === 'object') Object.values(o).forEach((x) => walk(x, fn));
}
function ytIds(...objs) {
const seen = new Set(), out = [];
for (const o of objs) walk(o, (s) => {
const id = ytId(s);
if (id && !seen.has(id)) { seen.add(id); out.push(id); }
});
return out;
}
function pickUrl(d) {
if (!d) return null;
if (typeof d === 'string' && /^https?:\/\//.test(d)) return d;
return d.url || d.downloadUrl || d.download_url || d.link || d.audioUrl || null;
}
function sanitize(n) {
return String(n).replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 180);
}
function isAudio(buf) {
if (!buf || buf.length < 500) return false;
if (buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) return true;
if (buf[0] === 0x66 && buf[1] === 0x4c && buf[2] === 0x61 && buf[3] === 0x43) return true;
if (buf[0] === 0x4f && buf[1] === 0x67 && buf[2] === 0x67 && buf[3] === 0x53) return true;
if (buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0) return true;
return false;
}
function fmtDur(s) {
if (s == null) return '-';
const m = Math.floor(s / 60), sec = Math.floor(s % 60);
return `${m}:${String(sec).padStart(2, '0')}`;
}
function printTrack(t, i) {
const n = i != null ? `${i}. ` : '';
console.log(`${n}${t.name} — ${t.artist || (t.artists || []).join(', ')}`);
console.log(` album: ${t.album_name || '-'} | ${fmtDur(t.duration)} | ${t.id}`);
if (t.release_date) console.log(` rilis: ${t.release_date}`);
if (t.cover_url) console.log(` cover: ${t.cover_url}`);
if (t.url) console.log(` url: ${t.url}`);
}
function printAlbum(a, i) {
const n = i != null ? `${i}. ` : '';
console.log(`${n}[album] ${a.name} — ${a.artist || (a.artists || []).join(', ')}`);
console.log(` year: ${a.year || '-'} | ${a.id}`);
if (a.cover_url) console.log(` cover: ${a.cover_url}`);
}
function printPlaylist(p, i) {
const n = i != null ? `${i}. ` : '';
console.log(`${n}[playlist] ${p.name} — ${p.owner || '-'}`);
console.log(` ${p.id}`);
if (p.cover_url) console.log(` cover: ${p.cover_url}`);
}
function printArtist(a, i) {
const n = i != null ? `${i}. ` : '';
console.log(`${n}[artist] ${a.name || a.artist}`);
console.log(` ${a.id}`);
}
// ---- CLI ----
const arg0 = process.argv[1] || '';
if (arg0.endsWith('spotubedl.mjs') || arg0.endsWith('spotube.js')) {
const args = process.argv.slice(2);
const cmd = (args[0] || '').toLowerCase();
const client = new SpotubeDL();
const verbose = args.includes('-v') || args.includes('--verbose');
try {
if (!args.length || cmd === 'help' || cmd === '-h') {
console.log(`Usage:
node spotubedl.mjs search [track|album|playlist|artist] <query>
node spotubedl.mjs info <url|id> # metadata track/album/playlist/artist
node spotubedl.mjs download <track-url|id> # metadata track + downloadUrl (MP3)
node spotubedl.mjs save <track-url|id> [file]
node spotubedl.mjs album|playlist|artist <url|id>`);
process.exit(0);
}
if (cmd === 'search') {
const types = new Set(['track', 'tracks', 'album', 'albums', 'playlist', 'playlists', 'artist', 'artists']);
let type = 'track';
let qParts = args.slice(1).filter((a) => a !== '-v' && a !== '--verbose');
if (qParts.length && types.has(qParts[0].toLowerCase())) {
type = qParts.shift().toLowerCase().replace(/s$/, '');
}
const q = qParts.join(' ') || 'never gonna give you up';
const r = await client.search(q);
if (!r.ok) throw new Error(JSON.stringify(r.data));
const d = r.data || {};
const key = type === 'track' ? 'tracks' : type + 's';
let list = d[key] || (type === 'track' ? d.results : null) || [];
if (!Array.isArray(list)) list = [];
console.log(JSON.stringify({
status: 'success',
message: `${list.length} ${type}(s) ditemukan`,
query: q,
type,
data: list,
}, null, 2));
} else if (cmd === 'info') {
const rest = args.slice(1).filter((a) => a !== '-v' && a !== '--verbose');
const input = rest[0];
if (!input) throw new Error('butuh url/id');
const info = await client.getInfo(input);
console.log(JSON.stringify({
status: 'success',
message: `Info ${info.kind}`,
kind: info.kind,
data: info.data,
}, null, 2));
} else if (cmd === 'download') {
// hanya track: metadata + downloadUrl MP3
const rest = args.slice(1).filter((a) => a !== '-v' && a !== '--verbose');
const input = rest[0];
if (!input) throw new Error('butuh track url/id');
const kind = detectKind(input);
if (kind !== 'track') {
throw new Error(`download hanya untuk track. Pakai: node spotubedl.mjs info <${kind}-url>`);
}
const info = await client.downloadTrack(input);
console.log(JSON.stringify({
status: 'success',
message: 'Download URL siap',
data: {
spotifyId: info.spotifyId,
videoId: info.videoId,
youtubeUrl: info.youtubeUrl,
downloadUrl: info.downloadUrl,
filename: info.filename,
source: info.source,
track: info.track,
},
}, null, 2));
} else if (cmd === 'save') {
// resolve + simpan ke disk
const rest = args.slice(1).filter((a) => a !== '-v' && a !== '--verbose');
const input = rest[0];
if (!input) throw new Error('butuh url/id');
const r = await client.saveTrack(input, rest[1]);
const out = {
status: 'success',
message: 'File tersimpan',
data: {
savedAs: r.savedAs,
bytes: r.bytes,
sizeMB: Number((r.bytes / 1024 / 1024).toFixed(2)),
spotifyId: r.spotifyId,
videoId: r.videoId,
filename: r.filename,
track: r.track,
},
};
console.log(JSON.stringify(out, null, 2));
} else if (cmd === 'album' || cmd === 'playlist' || cmd === 'artist') {
const input = args[1];
if (!input) throw new Error('butuh url/id');
const info = await client.getInfo(
cmd === 'album' ? (input.includes('album') ? input : `https://open.spotify.com/album/${input}`) :
cmd === 'playlist' ? (input.includes('playlist') ? input : `https://open.spotify.com/playlist/${input}`) :
(input.includes('artist') ? input : `https://open.spotify.com/artist/${input}`)
);
console.log(JSON.stringify({
status: 'success',
message: `Info ${info.kind}`,
kind: info.kind,
data: info.data,
}, null, 2));
} else {
// default = info (auto-detect track/album/playlist/artist)
const input = args.filter((a) => a !== '-v' && a !== '--verbose')[0];
if (!input) throw new Error('butuh url/id');
const info = await client.getInfo(input);
console.log(JSON.stringify({
status: 'success',
message: `Info ${info.kind}`,
kind: info.kind,
data: info.data,
}, null, 2));
}
} catch (e) {
console.log(JSON.stringify({ status: 'error', message: e.message }, null, 2));
process.exit(1);
}
}Client Node.js (ESM) untuk mengambil metadata Spotify dan link unduhan audio (MP3) tanpa API key, tanpa yt-dlp, tanpa ffmpeg.
Metadata diambil lewat protokol terenkripsi SpotubeDL. Audio di-resolve dari sumber publik (CDN / mirror).
fetch & crypto)bash# cek versi node -v
Salin file spotubedl.mjs (atau rename ke spotube.js) ke folder project:
bash# contoh di Termux / Linux mkdir -p ~/scraper && cd ~/scraper # letakkan spotubedl.mjs di sini
Jalankan langsung:
bashnode spotubedl.mjs help
bashnode spotubedl.mjs search "nothin on you" node spotubedl.mjs search track "nothin on you" node spotubedl.mjs search album "bobby ray" node spotubedl.mjs search playlist "chill" node spotubedl.mjs search artist "bruno mars"
Response
json{ "status": "success", "message": "20 track(s) ditemukan", "query": "nothin on you", "type": "track", "data": [ { "name": "Nothin' on You (feat. Bruno Mars)", "artist": "B.o.B", "artists": ["B.o.B", "Bruno Mars"], "album_name": "B.o.B Presents: The Adventures of Bobby Ray", "cover_url": "https://i.scdn.co/image/...", "duration": 268, "id": "59dLtGBS26x7kc0rHbaPrq", "url": "https://open.spotify.com/track/59dLtGBS26x7kc0rHbaPrq", "explicit": false } ] }
Otomatis deteksi dari URL:
bashnode spotubedl.mjs info https://open.spotify.com/track/59dLtGBS26x7kc0rHbaPrq node spotubedl.mjs info https://open.spotify.com/album/0xHAdClnuGbN90WQV2RfwK node spotubedl.mjs info https://open.spotify.com/playlist/3HNsWSPRdZooTeyAxIpUhn node spotubedl.mjs info https://open.spotify.com/artist/2qNrJcE9LjzPdiXbrjkqFa # atau cukup tempel URL node spotubedl.mjs https://open.spotify.com/playlist/3HNsWSPRdZooTeyAxIpUhn
Album & playlist mengembalikan daftar tracks (id + metadata per lagu).
Hanya untuk track:
bashnode spotubedl.mjs download https://open.spotify.com/track/59dLtGBS26x7kc0rHbaPrq node spotubedl.mjs download 59dLtGBS26x7kc0rHbaPrq
Response
json{ "status": "success", "message": "Download URL siap", "data": { "spotifyId": "59dLtGBS26x7kc0rHbaPrq", "videoId": "wQJIkbvUVPQ", "youtubeUrl": "https://www.youtube.com/watch?v=wQJIkbvUVPQ", "downloadUrl": "https://...", "filename": "Nothin' on You (feat. Bruno Mars) - B.o.B.mp3", "source": "cdn", "track": { "name": "Nothin' on You (feat. Bruno Mars)", "artist": "B.o.B", "artists": ["B.o.B", "Bruno Mars"], "album_name": "B.o.B Presents: The Adventures of Bobby Ray", "cover_url": "https://i.scdn.co/image/...", "duration": 268, "release_date": "2010-04-27", "id": "59dLtGBS26x7kc0rHbaPrq", "url": "https://open.spotify.com/track/59dLtGBS26x7kc0rHbaPrq", "explicit": false, "isrc": "USAT20904033" } } }
downloadUrlbersifat sementara (biasanya ~10 menit). Unduh segera setelah didapat.
bashnode spotubedl.mjs save 59dLtGBS26x7kc0rHbaPrq node spotubedl.mjs save 59dLtGBS26x7kc0rHbaPrq laguku.mp3
Response
json{ "status": "success", "message": "File tersimpan", "data": { "savedAs": "Nothin' on You (feat. Bruno Mars) - B.o.B.mp3", "bytes": 10737418, "sizeMB": 10.24, "spotifyId": "59dLtGBS26x7kc0rHbaPrq", "videoId": "wQJIkbvUVPQ", "filename": "Nothin' on You (feat. Bruno Mars) - B.o.B.mp3", "track": { "...": "..." } } }
bashnode spotubedl.mjs album 7apLPYT8szV1IqTxyVSy5P node spotubedl.mjs playlist <playlist-id-atau-url>
json{ "status": "error", "message": "..." }
jsimport { SpotubeDL } from './spotubedl.mjs'; const client = new SpotubeDL(); // Search const search = await client.search('blinding lights'); console.log(search.data); // Metadata track const track = await client.getTrack('59dLtGBS26x7kc0rHbaPrq'); // Resolve download URL (tanpa simpan) const info = await client.downloadTrack('59dLtGBS26x7kc0rHbaPrq'); console.log(info.downloadUrl); console.log(info.track); // Simpan ke disk const saved = await client.saveTrack('59dLtGBS26x7kc0rHbaPrq', 'out.mp3'); console.log(saved.savedAs, saved.bytes);
| Method | Deskripsi |
|---|---|
search(query) | Cari track / album / playlist / artist |
getTrack(id|url) | Metadata track Spotify |
getAlbum(id|url) | Metadata album |
getPlaylist(id|url) | Metadata playlist |
getArtist(id|url) | Metadata artist |
getMetadata(id|url) | Metadata lengkap + match YouTube |
downloadTrack(id|url) | Metadata + downloadUrl audio |
saveTrack(id|url, path?) | Unduh file MP3 ke disk |
| Perintah | Output |
|---|---|
search [type] <query> | JSON list hasil |
info <url|id> | JSON metadata (track/album/playlist/artist) |
download <track-url|id> | JSON track + downloadUrl MP3 |
save <track-url|id> [file] | JSON + file tersimpan |
album | playlist | artist <id> | JSON metadata |
<url> | Sama seperti info (auto-detect) |
type search: track (default) · album · playlist · artist
| Gejala | Saran |
|---|---|
status: error + message jaringan | Cek koneksi / coba lagi (sumber bisa rate-limit) |
downloadUrl 403 saat dibuka | Link sudah expire — panggil download lagi |
| Node lama | Upgrade ke Node 18+ |
fetch is not defined | Node < 18; upgrade atau polyfill |
Gunakan dengan tanggung jawab sendiri. Hormati hak cipta konten yang diunduh.