aqualink
Version:
An Lavalink/Nodelink client, focused in pure performance and features
157 lines (135 loc) • 3.87 kB
JavaScript
const https = require('node:https')
// Default agent config (used only if shared agent not provided)
const AGENT_CONFIG = {
keepAlive: true,
maxSockets: 64,
maxFreeSockets: 32,
timeout: 8000,
freeSocketTimeout: 4000
}
// Shared agent reference - can be set from Rest module
let sharedAgent = null
const getAgent = () => {
if (!sharedAgent) {
sharedAgent = new https.Agent(AGENT_CONFIG)
}
return sharedAgent
}
// Allow Rest module to inject its agent
const setSharedAgent = (agent) => {
if (!agent) {
sharedAgent = null
return
}
if (agent && typeof agent.addRequest === 'function') {
sharedAgent = agent
}
}
const SC_LINK_RE = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g
const MAX_REDIRECTS = 3
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 // 5 MB
const MAX_SC_LINKS = 50
const MAX_SP_RESULTS = 5
const DEFAULT_TIMEOUT_MS = 8000
const fastFetch = (url, depth = 0) =>
new Promise((resolve, reject) => {
if (depth > MAX_REDIRECTS) return reject(new Error('Too many redirects'))
const req = https.get(
url,
{ agent: getAgent(), timeout: DEFAULT_TIMEOUT_MS },
(res) => {
const { statusCode, headers } = res
if (statusCode >= 300 && statusCode < 400 && headers.location) {
res.resume()
return fastFetch(new URL(headers.location, url).href, depth + 1).then(
resolve,
reject
)
}
if (statusCode !== 200) {
res.resume()
return reject(new Error(`HTTP ${statusCode}`))
}
const chunks = []
let received = 0
res.on('data', (chunk) => {
received += chunk.length
if (received > MAX_RESPONSE_BYTES) {
req.destroy(new Error('Response too large'))
return
}
chunks.push(chunk)
})
res.on('end', () => {
try {
const buf = Buffer.concat(chunks)
resolve(buf.toString())
} catch (err) {
reject(err)
}
})
}
)
req.on('error', reject)
req.setTimeout(DEFAULT_TIMEOUT_MS, () => req.destroy(new Error('Timeout')))
})
const shuffleInPlace = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
const j = (Math.random() * (i + 1)) | 0
const tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
return arr
}
const scAutoPlay = async (baseUrl) => {
try {
const html = await fastFetch(`${baseUrl}/recommended`)
const links = []
for (const m of html.matchAll(SC_LINK_RE)) {
if (!m[1]) continue
links.push(`https://soundcloud.com${m[1]}`)
if (links.length >= MAX_SC_LINKS) break
}
return links.length ? shuffleInPlace(links) : []
} catch (err) {
console.error('scAutoPlay error:', err?.message || err)
return []
}
}
const spAutoPlay = async (seed, player, requester, excludedIds = []) => {
try {
if (!seed?.trackId) return null
const seedQuery = `seed_tracks=${seed.trackId}${seed.artistIds ? `&seed_artists=${seed.artistIds}` : ''}`
const res = await player.aqua.resolve({
query: seedQuery,
source: 'spsearch',
requester
})
const candidates = res?.tracks || []
if (!candidates.length) return null
const seen = new Set(excludedIds)
const prevId = player.current?.identifier
if (prevId) seen.add(prevId)
const out = []
for (const t of candidates) {
if (seen.has(t.identifier)) continue
seen.add(t.identifier)
t.pluginInfo = {
...(t.pluginInfo || {}),
clientData: { fromAutoplay: true }
}
out.push(t)
if (out.length === MAX_SP_RESULTS) break
}
return out.length ? out : null
} catch (err) {
console.error('spAutoPlay error:', err)
return null
}
}
module.exports = {
scAutoPlay,
spAutoPlay,
setSharedAgent
}