aqualink 2.18.1 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/handlers/autoplay.js +148 -121
- package/build/handlers/fetchImage.js +152 -148
- package/build/index.d.ts +1401 -1080
- package/build/index.js +23 -12
- package/build/structures/Aqua.js +1085 -896
- package/build/structures/AqualinkEvents.js +43 -45
- package/build/structures/Connection.js +576 -483
- package/build/structures/Filters.js +351 -244
- package/build/structures/Node.js +666 -548
- package/build/structures/Player.js +1143 -904
- package/build/structures/Plugins.js +10 -11
- package/build/structures/Queue.js +129 -111
- package/build/structures/Rest.js +856 -645
- package/build/structures/Track.js +158 -129
- package/package.json +1 -1
|
@@ -1,121 +1,148 @@
|
|
|
1
|
-
const https = require('https')
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
const https = require('https')
|
|
2
|
+
|
|
3
|
+
// Default agent config (used only if shared agent not provided)
|
|
4
|
+
const AGENT_CONFIG = {
|
|
5
|
+
keepAlive: true,
|
|
6
|
+
maxSockets: 64,
|
|
7
|
+
maxFreeSockets: 32,
|
|
8
|
+
timeout: 8000,
|
|
9
|
+
freeSocketTimeout: 4000
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Shared agent reference - can be set from Rest module
|
|
13
|
+
let sharedAgent = null
|
|
14
|
+
const getAgent = () => sharedAgent || (sharedAgent = new https.Agent(AGENT_CONFIG))
|
|
15
|
+
|
|
16
|
+
// Allow Rest module to inject its agent
|
|
17
|
+
const setSharedAgent = (agent) => {
|
|
18
|
+
if (agent && typeof agent.request === 'function') {
|
|
19
|
+
sharedAgent = agent
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const SC_LINK_RE = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g
|
|
25
|
+
const MAX_REDIRECTS = 3
|
|
26
|
+
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 // 5 MB
|
|
27
|
+
const MAX_SC_LINKS = 50
|
|
28
|
+
const MAX_SP_RESULTS = 5
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 8000
|
|
30
|
+
|
|
31
|
+
const fastFetch = (url, depth = 0) =>
|
|
32
|
+
new Promise((resolve, reject) => {
|
|
33
|
+
if (depth > MAX_REDIRECTS) return reject(new Error('Too many redirects'))
|
|
34
|
+
|
|
35
|
+
const req = https.get(
|
|
36
|
+
url,
|
|
37
|
+
{ agent: getAgent(), timeout: DEFAULT_TIMEOUT_MS },
|
|
38
|
+
(res) => {
|
|
39
|
+
const { statusCode, headers } = res
|
|
40
|
+
|
|
41
|
+
if (statusCode >= 300 && statusCode < 400 && headers.location) {
|
|
42
|
+
res.resume()
|
|
43
|
+
return fastFetch(new URL(headers.location, url).href, depth + 1).then(
|
|
44
|
+
resolve,
|
|
45
|
+
reject
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (statusCode !== 200) {
|
|
50
|
+
res.resume()
|
|
51
|
+
return reject(new Error(`HTTP ${statusCode}`))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const chunks = []
|
|
55
|
+
let received = 0
|
|
56
|
+
|
|
57
|
+
res.on('data', (chunk) => {
|
|
58
|
+
received += chunk.length
|
|
59
|
+
if (received > MAX_RESPONSE_BYTES) {
|
|
60
|
+
req.destroy(new Error('Response too large'))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
chunks.push(chunk)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try {
|
|
68
|
+
const buf = Buffer.concat(chunks)
|
|
69
|
+
resolve(buf.toString())
|
|
70
|
+
} catch (err) {
|
|
71
|
+
reject(err)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
req.on('error', reject)
|
|
78
|
+
req.setTimeout(DEFAULT_TIMEOUT_MS, () => req.destroy(new Error('Timeout')))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const shuffleInPlace = (arr) => {
|
|
82
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
83
|
+
const j = (Math.random() * (i + 1)) | 0
|
|
84
|
+
const tmp = arr[i]
|
|
85
|
+
arr[i] = arr[j]
|
|
86
|
+
arr[j] = tmp
|
|
87
|
+
}
|
|
88
|
+
return arr
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const scAutoPlay = async (baseUrl) => {
|
|
92
|
+
try {
|
|
93
|
+
const html = await fastFetch(`${baseUrl}/recommended`)
|
|
94
|
+
const links = []
|
|
95
|
+
for (const m of html.matchAll(SC_LINK_RE)) {
|
|
96
|
+
if (!m[1]) continue
|
|
97
|
+
links.push(`https://soundcloud.com${m[1]}`)
|
|
98
|
+
if (links.length >= MAX_SC_LINKS) break
|
|
99
|
+
}
|
|
100
|
+
return links.length ? shuffleInPlace(links) : []
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('scAutoPlay error:', err?.message || err)
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const spAutoPlay = async (seed, player, requester, excludedIds = []) => {
|
|
108
|
+
try {
|
|
109
|
+
if (!seed?.trackId) return null
|
|
110
|
+
|
|
111
|
+
const seedQuery = `seed_tracks=${seed.trackId}${seed.artistIds ? `&seed_artists=${seed.artistIds}` : ''}`
|
|
112
|
+
const res = await player.aqua.resolve({
|
|
113
|
+
query: seedQuery,
|
|
114
|
+
source: 'spsearch',
|
|
115
|
+
requester
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const candidates = res?.tracks || []
|
|
119
|
+
if (!candidates.length) return null
|
|
120
|
+
|
|
121
|
+
const seen = new Set(excludedIds)
|
|
122
|
+
const prevId = player.current?.identifier
|
|
123
|
+
if (prevId) seen.add(prevId)
|
|
124
|
+
|
|
125
|
+
const out = []
|
|
126
|
+
for (const t of candidates) {
|
|
127
|
+
if (seen.has(t.identifier)) continue
|
|
128
|
+
seen.add(t.identifier)
|
|
129
|
+
t.pluginInfo = {
|
|
130
|
+
...(t.pluginInfo || {}),
|
|
131
|
+
clientData: { fromAutoplay: true }
|
|
132
|
+
}
|
|
133
|
+
out.push(t)
|
|
134
|
+
if (out.length === MAX_SP_RESULTS) break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out.length ? out : null
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('spAutoPlay error:', err)
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
scAutoPlay,
|
|
146
|
+
spAutoPlay,
|
|
147
|
+
setSharedAgent
|
|
148
|
+
}
|
|
@@ -1,148 +1,152 @@
|
|
|
1
|
-
const https = require('https')
|
|
2
|
-
|
|
3
|
-
const sourceHandlers = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default']
|
|
9
|
-
|
|
10
|
-
const YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}
|
|
11
|
-
const SPOTIFY_URI_REGEX =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
1
|
+
const https = require('https')
|
|
2
|
+
|
|
3
|
+
const sourceHandlers = {
|
|
4
|
+
spotify: fetchSpotifyThumbnail,
|
|
5
|
+
youtube: fetchYouTubeThumbnail
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default']
|
|
9
|
+
|
|
10
|
+
const YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/
|
|
11
|
+
const SPOTIFY_URI_REGEX =
|
|
12
|
+
/^https:\/\/open\.spotify\.com\/(track|album|playlist)\/[a-zA-Z0-9]+/
|
|
13
|
+
|
|
14
|
+
async function getImageUrl(info) {
|
|
15
|
+
if (!info?.sourceName || !info?.uri) return null
|
|
16
|
+
|
|
17
|
+
const sourceName = info.sourceName.toLowerCase()
|
|
18
|
+
const handler = sourceHandlers[sourceName]
|
|
19
|
+
|
|
20
|
+
if (!handler) return null
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const param =
|
|
24
|
+
sourceName === 'spotify'
|
|
25
|
+
? info.uri
|
|
26
|
+
: sourceName === 'youtube'
|
|
27
|
+
? extractYouTubeId(info.uri)
|
|
28
|
+
: info.uri
|
|
29
|
+
|
|
30
|
+
if (!param) return null
|
|
31
|
+
|
|
32
|
+
return await handler(param)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Error fetching ${sourceName} thumbnail:`, error.message)
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractYouTubeId(uri) {
|
|
40
|
+
if (!uri) return null
|
|
41
|
+
|
|
42
|
+
let id = null
|
|
43
|
+
|
|
44
|
+
if (uri.includes('youtube.com/watch?v=')) {
|
|
45
|
+
id = uri.split('v=')[1]?.split('&')[0]
|
|
46
|
+
} else if (uri.includes('youtu.be/')) {
|
|
47
|
+
id = uri.split('youtu.be/')[1]?.split('?')[0]
|
|
48
|
+
} else if (uri.includes('youtube.com/embed/')) {
|
|
49
|
+
id = uri.split('embed/')[1]?.split('?')[0]
|
|
50
|
+
} else if (YOUTUBE_ID_REGEX.test(uri)) {
|
|
51
|
+
id = uri
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return id && YOUTUBE_ID_REGEX.test(id) ? id : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchSpotifyThumbnail(uri) {
|
|
58
|
+
if (!SPOTIFY_URI_REGEX.test(uri)) {
|
|
59
|
+
throw new Error('Invalid Spotify URI format')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const url = `https://open.spotify.com/oembed?url=${encodeURIComponent(uri)}`
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const data = await fetchJson(url)
|
|
66
|
+
return data?.thumbnail_url || null
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`Spotify fetch failed: ${error.message}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function fetchYouTubeThumbnail(identifier) {
|
|
73
|
+
if (!identifier || !YOUTUBE_ID_REGEX.test(identifier)) {
|
|
74
|
+
throw new Error('Invalid YouTube identifier')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const quality of YOUTUBE_QUALITIES) {
|
|
78
|
+
const url = `https://img.youtube.com/vi/${identifier}/${quality}.jpg`
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const exists = await checkImageExists(url)
|
|
82
|
+
if (exists) return url
|
|
83
|
+
} catch (error) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fetchJson(url) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const request = https.get(url, (res) => {
|
|
94
|
+
if (res.statusCode !== 200) {
|
|
95
|
+
res.resume()
|
|
96
|
+
return reject(new Error(`HTTP ${res.statusCode}`))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const chunks = []
|
|
100
|
+
let totalLength = 0
|
|
101
|
+
|
|
102
|
+
res.on('data', (chunk) => {
|
|
103
|
+
chunks.push(chunk)
|
|
104
|
+
totalLength += chunk.length
|
|
105
|
+
|
|
106
|
+
if (totalLength > 1024 * 1024) {
|
|
107
|
+
res.destroy()
|
|
108
|
+
reject(new Error('Response too large'))
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
res.on('end', () => {
|
|
113
|
+
try {
|
|
114
|
+
const data = Buffer.concat(chunks, totalLength).toString('utf8')
|
|
115
|
+
const json = JSON.parse(data)
|
|
116
|
+
resolve(json)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
reject(new Error(`JSON parse error: ${error.message}`))
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
request.setTimeout(5000, () => {
|
|
124
|
+
request.destroy()
|
|
125
|
+
reject(new Error('Request timeout'))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
request.on('error', (error) => {
|
|
129
|
+
reject(new Error(`Request error: ${error.message}`))
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function checkImageExists(url) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const request = https.request(url, { method: 'HEAD' }, (res) => {
|
|
137
|
+
resolve(res.statusCode === 200)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
request.setTimeout(3000, () => {
|
|
141
|
+
request.destroy()
|
|
142
|
+
resolve(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
request.on('error', () => resolve(false))
|
|
146
|
+
request.end()
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
getImageUrl
|
|
152
|
+
}
|