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.
@@ -1,121 +1,148 @@
1
- const https = require('https')
2
-
3
- const AGENT_CONFIG = {
4
- keepAlive: true,
5
- maxSockets: 5,
6
- maxFreeSockets: 2,
7
- timeout: 8000,
8
- freeSocketTimeout: 4000
9
- }
10
-
11
- const agent = new https.Agent(AGENT_CONFIG)
12
-
13
- const SC_LINK_RE = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g
14
- const MAX_REDIRECTS = 3
15
- const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 // 5 MB
16
- const MAX_SC_LINKS = 50
17
- const MAX_SP_RESULTS = 5
18
- const DEFAULT_TIMEOUT_MS = 8000
19
-
20
- const fastFetch = (url, depth = 0) => new Promise((resolve, reject) => {
21
- if (depth > MAX_REDIRECTS) return reject(new Error('Too many redirects'))
22
-
23
- const req = https.get(url, { agent, timeout: DEFAULT_TIMEOUT_MS }, res => {
24
- const { statusCode, headers } = res
25
-
26
- if (statusCode >= 300 && statusCode < 400 && headers.location) {
27
- res.resume()
28
- return fastFetch(new URL(headers.location, url).href, depth + 1).then(resolve, reject)
29
- }
30
-
31
- if (statusCode !== 200) {
32
- res.resume()
33
- return reject(new Error(`HTTP ${statusCode}`))
34
- }
35
-
36
- const chunks = []
37
- let received = 0
38
-
39
- res.on('data', chunk => {
40
- received += chunk.length
41
- if (received > MAX_RESPONSE_BYTES) {
42
- req.destroy(new Error('Response too large'))
43
- return
44
- }
45
- chunks.push(chunk)
46
- })
47
-
48
- res.on('end', () => {
49
- try {
50
- const buf = Buffer.concat(chunks)
51
- resolve(buf.toString())
52
- } catch (err) {
53
- reject(err)
54
- }
55
- })
56
- })
57
-
58
- req.on('error', reject)
59
- req.setTimeout(DEFAULT_TIMEOUT_MS, () => req.destroy(new Error('Timeout')))
60
- })
61
-
62
- const shuffleInPlace = arr => {
63
- for (let i = arr.length - 1; i > 0; i--) {
64
- const j = Math.random() * (i + 1) | 0
65
- const tmp = arr[i]
66
- arr[i] = arr[j]
67
- arr[j] = tmp
68
- }
69
- return arr
70
- }
71
-
72
- const scAutoPlay = async baseUrl => {
73
- try {
74
- const html = await fastFetch(`${baseUrl}/recommended`)
75
- const links = []
76
- for (const m of html.matchAll(SC_LINK_RE)) {
77
- if (!m[1]) continue
78
- links.push(`https://soundcloud.com${m[1]}`)
79
- if (links.length >= MAX_SC_LINKS) break
80
- }
81
- return links.length ? shuffleInPlace(links) : []
82
- } catch (err) {
83
- console.error('scAutoPlay error:', err?.message || err)
84
- return []
85
- }
86
- }
87
-
88
- const spAutoPlay = async (seed, player, requester, excludedIds = []) => {
89
- try {
90
- if (!seed?.trackId) return null
91
-
92
- const seedQuery = `seed_tracks=${seed.trackId}${seed.artistIds ? `&seed_artists=${seed.artistIds}` : ''}`
93
- const res = await player.aqua.resolve({ query: seedQuery, source: 'spsearch', requester })
94
-
95
- const candidates = res?.tracks || []
96
- if (!candidates.length) return null
97
-
98
- const seen = new Set(excludedIds)
99
- const prevId = player.current?.identifier
100
- if (prevId) seen.add(prevId)
101
-
102
- const out = []
103
- for (const t of candidates) {
104
- if (seen.has(t.identifier)) continue
105
- seen.add(t.identifier)
106
- t.pluginInfo = { ...(t.pluginInfo || {}), clientData: { fromAutoplay: true } }
107
- out.push(t)
108
- if (out.length === MAX_SP_RESULTS) break
109
- }
110
-
111
- return out.length ? out : null
112
- } catch (err) {
113
- console.error('spAutoPlay error:', err)
114
- return null
115
- }
116
- }
117
-
118
- module.exports = {
119
- scAutoPlay,
120
- spAutoPlay
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
- 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 = /^https:\/\/open\.spotify\.com\/(track|album|playlist)\/[a-zA-Z0-9]+/;
12
-
13
- async function getImageUrl(info) {
14
- if (!info?.sourceName || !info?.uri) return null;
15
-
16
- const sourceName = info.sourceName.toLowerCase();
17
- const handler = sourceHandlers[sourceName];
18
-
19
- if (!handler) return null;
20
-
21
- try {
22
- const param = sourceName === 'spotify' ? info.uri :
23
- sourceName === 'youtube' ? extractYouTubeId(info.uri) : info.uri;
24
-
25
- if (!param) return null;
26
-
27
- return await handler(param);
28
- } catch (error) {
29
- console.error(`Error fetching ${sourceName} thumbnail:`, error.message);
30
- return null;
31
- }
32
- }
33
-
34
- function extractYouTubeId(uri) {
35
- if (!uri) return null;
36
-
37
- let id = null;
38
-
39
- if (uri.includes('youtube.com/watch?v=')) {
40
- id = uri.split('v=')[1]?.split('&')[0];
41
- } else if (uri.includes('youtu.be/')) {
42
- id = uri.split('youtu.be/')[1]?.split('?')[0];
43
- } else if (uri.includes('youtube.com/embed/')) {
44
- id = uri.split('embed/')[1]?.split('?')[0];
45
- } else if (YOUTUBE_ID_REGEX.test(uri)) {
46
- id = uri;
47
- }
48
-
49
- return id && YOUTUBE_ID_REGEX.test(id) ? id : null;
50
- }
51
-
52
- async function fetchSpotifyThumbnail(uri) {
53
- if (!SPOTIFY_URI_REGEX.test(uri)) {
54
- throw new Error('Invalid Spotify URI format');
55
- }
56
-
57
- const url = `https://open.spotify.com/oembed?url=${encodeURIComponent(uri)}`;
58
-
59
- try {
60
- const data = await fetchJson(url);
61
- return data?.thumbnail_url || null;
62
- } catch (error) {
63
- throw new Error(`Spotify fetch failed: ${error.message}`);
64
- }
65
- }
66
-
67
- async function fetchYouTubeThumbnail(identifier) {
68
- if (!identifier || !YOUTUBE_ID_REGEX.test(identifier)) {
69
- throw new Error('Invalid YouTube identifier');
70
- }
71
-
72
- for (const quality of YOUTUBE_QUALITIES) {
73
- const url = `https://img.youtube.com/vi/${identifier}/${quality}.jpg`;
74
-
75
- try {
76
- const exists = await checkImageExists(url);
77
- if (exists) return url;
78
- } catch (error) {
79
- continue;
80
- }
81
- }
82
-
83
- return null;
84
- }
85
-
86
- function fetchJson(url) {
87
- return new Promise((resolve, reject) => {
88
- const request = https.get(url, (res) => {
89
- if (res.statusCode !== 200) {
90
- res.resume();
91
- return reject(new Error(`HTTP ${res.statusCode}`));
92
- }
93
-
94
- const chunks = [];
95
- let totalLength = 0;
96
-
97
- res.on('data', chunk => {
98
- chunks.push(chunk);
99
- totalLength += chunk.length;
100
-
101
- if (totalLength > 1024 * 1024) {
102
- res.destroy();
103
- reject(new Error('Response too large'));
104
- }
105
- });
106
-
107
- res.on('end', () => {
108
- try {
109
- const data = Buffer.concat(chunks, totalLength).toString('utf8');
110
- const json = JSON.parse(data);
111
- resolve(json);
112
- } catch (error) {
113
- reject(new Error(`JSON parse error: ${error.message}`));
114
- }
115
- });
116
- });
117
-
118
- request.setTimeout(5000, () => {
119
- request.destroy();
120
- reject(new Error('Request timeout'));
121
- });
122
-
123
- request.on('error', (error) => {
124
- reject(new Error(`Request error: ${error.message}`));
125
- });
126
- });
127
- }
128
-
129
- function checkImageExists(url) {
130
- return new Promise((resolve) => {
131
- const request = https.request(url, { method: 'HEAD' }, (res) => {
132
- resolve(res.statusCode === 200);
133
- });
134
-
135
- request.setTimeout(3000, () => {
136
- request.destroy();
137
- resolve(false);
138
- });
139
-
140
- request.on('error', () => resolve(false));
141
- request.end();
142
- });
143
- }
144
-
145
-
146
- module.exports = {
147
- getImageUrl,
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
+ }