aqualink
Version:
An Lavalink/Nodelink client, focused in pure performance and features
158 lines (132 loc) • 4.06 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
}
const defaultAgent = new https.Agent(AGENT_CONFIG)
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, agent, depth = 0) =>
new Promise((resolve, reject) => {
if (depth > MAX_REDIRECTS) return reject(new Error('Too many redirects'))
const req = https.get(
url,
{ agent: agent || defaultAgent, 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,
agent,
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, agent) => {
const html = await fastFetch(`${baseUrl}/recommended`, agent)
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) : []
}
const spAutoPlay = async (seed, player, requester, excludedIds = []) => {
if (!seed) return null
let trackId = seed.trackId !== 'local' ? seed.trackId : null
let isrc = seed.isrc || null
if (!trackId && !isrc && seed.title) {
const searchRes = await player.aqua.resolve({
query: `${seed.author ? `${seed.author} - ` : ''}${seed.title}`,
source: 'spsearch',
requester
})
const foundTrack = searchRes?.tracks?.[0]
if (foundTrack) {
trackId = foundTrack.identifier !== 'local' ? foundTrack.identifier : null
isrc = foundTrack.isrc || null
}
}
if (!trackId && !isrc) return null
let query
if (isrc) {
query = `sprec:mix:isrc:${isrc}`
} else {
query = `sprec:seed_tracks=${trackId}`
}
let res = await player.aqua.resolve({
query,
requester
})
if (!res?.tracks?.length && isrc && trackId) {
res = await player.aqua.resolve({
query: `sprec:seed_tracks=${trackId}`,
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)
}
return out.length ? out : null
}
module.exports = {
scAutoPlay,
spAutoPlay
}