Back to Home
Gistify Snippet
Public
Scraper spotyloader
Scraper spotyloader
#github-gist#javascript#gist:f3c35e733e9a575da954a945f1611438#gistsync:2026-09-02T12:40:42Z
1
Rawspotyloader.jsjavascript
176 lines| 1 | |
| 2 | |
| 3 | import * as __ns_794 from 'node:url' |
| 4 | const pathToFileURL = __ns_794.pathToFileURL |
| 5 | |
| 6 | const BASE = 'https://spotyloader.com' |
| 7 | |
| 8 | const headers = { |
| 9 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', |
| 10 | 'accept': '*/*', |
| 11 | 'content-type': 'application/json', |
| 12 | 'origin': BASE, |
| 13 | 'referer': BASE + '/', |
| 14 | } |
| 15 | |
| 16 | function detectType(url) { |
| 17 | if (!url || typeof url !== 'string') return null |
| 18 | const u = url.trim() |
| 19 | if (/^spotify:(track|playlist|album|artist):[A-Za-z0-9]+$/i.test(u)) { |
| 20 | const m = u.match(/^spotify:(track|playlist|album|artist):/i) |
| 21 | return m[1].toLowerCase() |
| 22 | } |
| 23 | try { |
| 24 | const parsed = new URL(u) |
| 25 | const host = parsed.hostname.toLowerCase() |
| 26 | if (host !== 'open.spotify.com' && host !== 'spotify.com' && !host.endsWith('.spotify.com')) return null |
| 27 | const m = parsed.pathname.match(/^\/(?:intl-[a-z-]+\/)?(track|playlist|album|artist)\/([A-Za-z0-9]+)/i) |
| 28 | return m ? m[1].toLowerCase() : null |
| 29 | } catch { |
| 30 | return null |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | function fmtDuration(ms) { |
| 35 | const s = Math.round(ms / 1000) |
| 36 | const m = Math.floor(s / 60) |
| 37 | return m + ':' + String(s % 60).padStart(2, '0') |
| 38 | } |
| 39 | |
| 40 | function checkError(status, data) { |
| 41 | if (data?.error) throw new Error(data.error) |
| 42 | if (data?.message && data.success === false) throw new Error(data.message) |
| 43 | if (status >= 400) throw new Error(`HTTP ${status}`) |
| 44 | } |
| 45 | |
| 46 | async function apiGet(path) { |
| 47 | const r = await fetch(BASE + path, { headers, cache: 'no-store' }) |
| 48 | const data = await r.json() |
| 49 | checkError(r.status, data) |
| 50 | return data |
| 51 | } |
| 52 | |
| 53 | async function apiPost(path, body) { |
| 54 | const r = await fetch(BASE + path, { |
| 55 | method: 'POST', |
| 56 | headers, |
| 57 | body: JSON.stringify(body), |
| 58 | cache: 'no-store', |
| 59 | }) |
| 60 | const data = await r.json() |
| 61 | checkError(r.status, data) |
| 62 | return data |
| 63 | } |
| 64 | |
| 65 | async function getInfo(url) { |
| 66 | const type = detectType(url) |
| 67 | if (!type) throw new Error('Invalid Spotify URL') |
| 68 | const encoded = encodeURIComponent(url.trim()) |
| 69 | if (type === 'track') { |
| 70 | return { type, data: (await apiGet(`/api/spotify/info?url=${encoded}`)).post || (await apiGet(`/api/spotify/info?url=${encoded}`)) } |
| 71 | } |
| 72 | if (type === 'artist') { |
| 73 | return { type, data: (await apiGet(`/api/spotify/artist?url=${encoded}`)).post } |
| 74 | } |
| 75 | if (type === 'playlist' || type === 'album') { |
| 76 | return { type, data: (await apiPost(`/api/spotify/${type}`, { url: url.trim() })).post } |
| 77 | } |
| 78 | throw new Error('Unsupported type: ' + type) |
| 79 | } |
| 80 | |
| 81 | async function convertTrack(url, format = 'mp3') { |
| 82 | const r = await apiPost('/api/spotify/track', { url: url.trim(), format }) |
| 83 | if (!r.jobId) throw new Error('Failed to start conversion') |
| 84 | return r.jobId |
| 85 | } |
| 86 | |
| 87 | async function pollStatus(jobId, maxAttempts = 30, interval = 2000) { |
| 88 | for (let i = 0; i < maxAttempts; i++) { |
| 89 | await new Promise(r => setTimeout(r, interval)) |
| 90 | const r = await apiGet(`/api/spotify/track/status/${jobId}`) |
| 91 | if (r.status === 'ready' && r.downloadLink) return r |
| 92 | if (r.status === 'error') throw new Error(r.error || 'Conversion failed') |
| 93 | if (r.status !== 'pending' && r.status !== 'processing') throw new Error('Unknown status: ' + r.status) |
| 94 | } |
| 95 | throw new Error('Timeout: conversion took too long') |
| 96 | } |
| 97 | |
| 98 | async function downloadTrack(url, format = 'mp3') { |
| 99 | const jobId = await convertTrack(url, format) |
| 100 | const result = await pollStatus(jobId) |
| 101 | return { |
| 102 | status: true, |
| 103 | name: result.post?.name || 'unknown', |
| 104 | artist: result.post?.artist || 'unknown', |
| 105 | album: result.post?.album || '', |
| 106 | format: result.post?.format || format, |
| 107 | download_url: result.downloadLink, |
| 108 | duration: result.post?.duration_ms ? fmtDuration(result.post.duration_ms) : '', |
| 109 | image: result.post?.image || '', |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | async function searchTracks(query, limit = 10) { |
| 114 | const encoded = encodeURIComponent(query) |
| 115 | const data = await apiGet(`/api/spotify/search?q=${encoded}&limit=${limit}`) |
| 116 | return (data.items || []).map(t => ({ |
| 117 | name: t.name, |
| 118 | artist: t.artist, |
| 119 | url: t.url, |
| 120 | duration: fmtDuration(t.duration_ms), |
| 121 | image: t.image || '', |
| 122 | source: t.source, |
| 123 | })) |
| 124 | } |
| 125 | |
| 126 | const __dflt_26 = { |
| 127 | detectType, |
| 128 | fmtDuration, |
| 129 | getInfo, |
| 130 | convertTrack, |
| 131 | pollStatus, |
| 132 | downloadTrack, |
| 133 | searchTracks |
| 134 | } |
| 135 | |
| 136 | if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| 137 | const __exports = { getInfo: getInfo, convertTrack: convertTrack, pollStatus: pollStatus, downloadTrack: downloadTrack, searchTracks: searchTracks, detectType: detectType, fmtDuration: fmtDuration, default: __dflt_26 } |
| 138 | const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function')) |
| 139 | const __names = Object.keys(__fns) |
| 140 | if (__names.length === 0) { |
| 141 | console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.') |
| 142 | } else { |
| 143 | const __search = __names.find((n) => /search|cari/i.test(n)) |
| 144 | const __name = __search || __names[0] |
| 145 | const __fn = __fns[__name] |
| 146 | const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn)) |
| 147 | const __defaultArgs = (() => { |
| 148 | const n = __name.toLowerCase() |
| 149 | if (/emoji/.test(n)) return [{ emoji: '๐๐' }] |
| 150 | if (/textpro|generate.*text|effect/.test(n)) return ['neon', 'Hello'] |
| 151 | if (/image|wallpaper|search|query|anime|movie|manga|news|lyrics|wiki|quote|stalk|detail/.test(n)) return ['naruto'] |
| 152 | if (/download|youtube|video|audio|music|spotify|tiktok|instagram|facebook|twitter|reels|terabox|media/.test(n)) return ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'] |
| 153 | if (/dork/.test(n)) return ['example.com', 'documents'] |
| 154 | if (/tts|speech|prompt|chat|ask|translate/.test(n)) return ['Hello world'] |
| 155 | if (/parse|format|normalize|list|features/.test(n)) return [] |
| 156 | return ['test'] |
| 157 | })() |
| 158 | let __args = __defaultArgs |
| 159 | if (process.argv.length > 2) __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } }) |
| 160 | if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) { |
| 161 | console.log('Fungsi: ' + __names.join(', ')) |
| 162 | console.log('Contoh: node ' + "spotyloader.js" + ' ' + __name + ' <arg>') |
| 163 | } else { |
| 164 | ;(async () => { |
| 165 | try { |
| 166 | const r = __isClass ? new __fn(...__args) : await __fn(...__args) |
| 167 | console.log(JSON.stringify(r, null, 2)) |
| 168 | } catch (e) { |
| 169 | console.error('Error: ' + (e && e.message ? e.message : e)) |
| 170 | process.exitCode = 1 |
| 171 | } |
| 172 | })() |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | |
Discussion & Comments0
No comments yet โ be the first to leave feedback or start a code discussion.
Sign in with your GitHub account to write comments and join the discussion.