muthera 1.0.9 → 1.0.11
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/package.json +1 -1
- package/src/functions/autoPlay.js +250 -204
- package/src/structures/Muthera.js +2 -0
- package/src/structures/mutheraPlayer.js +72 -1
package/package.json
CHANGED
|
@@ -1,204 +1,250 @@
|
|
|
1
|
-
const { JSDOM } = require("jsdom");
|
|
2
|
-
|
|
3
|
-
async function scAutoPlay(url) {
|
|
4
|
-
const res = await fetch(`${url}/recommended`);
|
|
5
|
-
|
|
6
|
-
const html = await res.text();
|
|
7
|
-
|
|
8
|
-
const dom = new JSDOM(html);
|
|
9
|
-
const document = dom.window.document;
|
|
10
|
-
|
|
11
|
-
const secondNoscript = document.querySelectorAll("noscript")[1];
|
|
12
|
-
const sectionElement = secondNoscript.querySelector("section");
|
|
13
|
-
const articleElements = sectionElement.querySelectorAll("article");
|
|
14
|
-
|
|
15
|
-
articleElements.forEach((articleElement) => {
|
|
16
|
-
const h2Element = articleElement.querySelector('h2[itemprop="name"]');
|
|
17
|
-
|
|
18
|
-
const aElement = h2Element.querySelector('a[itemprop="url"]');
|
|
19
|
-
const href = `https://soundcloud.com${aElement.getAttribute("href")}`;
|
|
20
|
-
|
|
21
|
-
return href;
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function amAutoPlay(track_id, retries = 3) {
|
|
26
|
-
async function fetchWithRetry(url, options, retries) {
|
|
27
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(url, options);
|
|
30
|
-
return await res.json();
|
|
31
|
-
} catch (error) {
|
|
32
|
-
if (attempt === retries - 1) {
|
|
33
|
-
throw new Error("No track found.");
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const trackData = await fetchWithRetry(
|
|
39
|
-
`https://itunes.apple.com/lookup?id=${track_id}`,
|
|
40
|
-
retries
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
if (!trackData.results || trackData.results.length === 0) {
|
|
44
|
-
throw new Error("No track found.");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const track = trackData.results[0];
|
|
48
|
-
const genreName = track.primaryGenreName;
|
|
49
|
-
|
|
50
|
-
const relatedData = await fetchWithRetry(
|
|
51
|
-
`https://itunes.apple.com/search?term=${encodeURIComponent(
|
|
52
|
-
genreName
|
|
53
|
-
)}&entity=song`,
|
|
54
|
-
retries
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
if (!relatedData.results || relatedData.results.length === 0) {
|
|
58
|
-
throw new Error("No track found.");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return relatedData.results.filter(
|
|
62
|
-
(item) => item.wrapperType === "track" && item.trackId !== track_id
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function spAutoPlay(track_id, retries = 3) {
|
|
67
|
-
async function fetchWithRetry(url, options = {}, retriesLeft = retries) {
|
|
68
|
-
for (let attempt = 0; attempt < retriesLeft; attempt++) {
|
|
69
|
-
try {
|
|
70
|
-
const res = await fetch(url, options);
|
|
71
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
72
|
-
return await res.json();
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (attempt === retriesLeft - 1) throw new Error("No track found.");
|
|
75
|
-
await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
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
|
-
if (!
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1
|
+
const { JSDOM } = require("jsdom");
|
|
2
|
+
|
|
3
|
+
async function scAutoPlay(url) {
|
|
4
|
+
const res = await fetch(`${url}/recommended`);
|
|
5
|
+
|
|
6
|
+
const html = await res.text();
|
|
7
|
+
|
|
8
|
+
const dom = new JSDOM(html);
|
|
9
|
+
const document = dom.window.document;
|
|
10
|
+
|
|
11
|
+
const secondNoscript = document.querySelectorAll("noscript")[1];
|
|
12
|
+
const sectionElement = secondNoscript.querySelector("section");
|
|
13
|
+
const articleElements = sectionElement.querySelectorAll("article");
|
|
14
|
+
|
|
15
|
+
articleElements.forEach((articleElement) => {
|
|
16
|
+
const h2Element = articleElement.querySelector('h2[itemprop="name"]');
|
|
17
|
+
|
|
18
|
+
const aElement = h2Element.querySelector('a[itemprop="url"]');
|
|
19
|
+
const href = `https://soundcloud.com${aElement.getAttribute("href")}`;
|
|
20
|
+
|
|
21
|
+
return href;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function amAutoPlay(track_id, retries = 3) {
|
|
26
|
+
async function fetchWithRetry(url, options, retries) {
|
|
27
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(url, options);
|
|
30
|
+
return await res.json();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (attempt === retries - 1) {
|
|
33
|
+
throw new Error("No track found.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const trackData = await fetchWithRetry(
|
|
39
|
+
`https://itunes.apple.com/lookup?id=${track_id}`,
|
|
40
|
+
retries
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!trackData.results || trackData.results.length === 0) {
|
|
44
|
+
throw new Error("No track found.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const track = trackData.results[0];
|
|
48
|
+
const genreName = track.primaryGenreName;
|
|
49
|
+
|
|
50
|
+
const relatedData = await fetchWithRetry(
|
|
51
|
+
`https://itunes.apple.com/search?term=${encodeURIComponent(
|
|
52
|
+
genreName
|
|
53
|
+
)}&entity=song`,
|
|
54
|
+
retries
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!relatedData.results || relatedData.results.length === 0) {
|
|
58
|
+
throw new Error("No track found.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return relatedData.results.filter(
|
|
62
|
+
(item) => item.wrapperType === "track" && item.trackId !== track_id
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function spAutoPlay(track_id, clientId, clientSecret, retries = 3) {
|
|
67
|
+
async function fetchWithRetry(url, options = {}, retriesLeft = retries) {
|
|
68
|
+
for (let attempt = 0; attempt < retriesLeft; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(url, options);
|
|
71
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
72
|
+
return await res.json();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (attempt === retriesLeft - 1) throw new Error("No track found.");
|
|
75
|
+
await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let accessToken;
|
|
81
|
+
|
|
82
|
+
if (clientId && clientSecret) {
|
|
83
|
+
// Client Credentials flow — no user login needed, suitable for bots
|
|
84
|
+
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
85
|
+
const tokenResponse = await fetchWithRetry(
|
|
86
|
+
"https://accounts.spotify.com/api/token",
|
|
87
|
+
{
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Basic ${credentials}`,
|
|
91
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
92
|
+
},
|
|
93
|
+
body: "grant_type=client_credentials",
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
accessToken = tokenResponse.access_token;
|
|
97
|
+
} else {
|
|
98
|
+
// Fallback: public embed token (no credentials required, but limited)
|
|
99
|
+
const tokenResponse = await fetchWithRetry(
|
|
100
|
+
"https://open.spotify.com/get_access_token?reason=transport&productType=embed"
|
|
101
|
+
);
|
|
102
|
+
accessToken = tokenResponse.accessToken;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!accessToken) throw new Error("Failed to get Spotify token.");
|
|
106
|
+
|
|
107
|
+
const headers = {
|
|
108
|
+
Authorization: `Bearer ${accessToken}`,
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Get the track to find its artist and genre info
|
|
113
|
+
const trackData = await fetchWithRetry(
|
|
114
|
+
`https://api.spotify.com/v1/tracks/${track_id}`,
|
|
115
|
+
{ headers }
|
|
116
|
+
);
|
|
117
|
+
const artistId = trackData?.artists?.[0]?.id;
|
|
118
|
+
if (!artistId) throw new Error("No artist found for track.");
|
|
119
|
+
|
|
120
|
+
const candidates = new Map();
|
|
121
|
+
|
|
122
|
+
// 1. Get artist's top tracks (always works with Client Credentials)
|
|
123
|
+
try {
|
|
124
|
+
const topTracksData = await fetchWithRetry(
|
|
125
|
+
`https://api.spotify.com/v1/artists/${artistId}/top-tracks?market=US`,
|
|
126
|
+
{ headers }
|
|
127
|
+
);
|
|
128
|
+
for (const t of topTracksData?.tracks ?? []) {
|
|
129
|
+
if (t?.id && t.id !== track_id) candidates.set(t.id, t);
|
|
130
|
+
}
|
|
131
|
+
} catch (_) {}
|
|
132
|
+
|
|
133
|
+
// 2. Get artist's albums, then sample tracks from a random album
|
|
134
|
+
try {
|
|
135
|
+
const albumsData = await fetchWithRetry(
|
|
136
|
+
`https://api.spotify.com/v1/artists/${artistId}/albums?include_groups=album,single&market=US&limit=20`,
|
|
137
|
+
{ headers }
|
|
138
|
+
);
|
|
139
|
+
const albums = albumsData?.items ?? [];
|
|
140
|
+
if (albums.length) {
|
|
141
|
+
// Pick up to 3 random albums to sample from
|
|
142
|
+
const shuffled = albums.sort(() => Math.random() - 0.5).slice(0, 3);
|
|
143
|
+
await Promise.all(
|
|
144
|
+
shuffled.map(async (album) => {
|
|
145
|
+
try {
|
|
146
|
+
const albumTracks = await fetchWithRetry(
|
|
147
|
+
`https://api.spotify.com/v1/albums/${album.id}/tracks?limit=10`,
|
|
148
|
+
{ headers }
|
|
149
|
+
);
|
|
150
|
+
for (const t of albumTracks?.items ?? []) {
|
|
151
|
+
if (t?.id && t.id !== track_id) candidates.set(t.id, t);
|
|
152
|
+
}
|
|
153
|
+
} catch (_) {}
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
} catch (_) {}
|
|
158
|
+
|
|
159
|
+
if (!candidates.size) throw new Error("No tracks found.");
|
|
160
|
+
|
|
161
|
+
const pool = Array.from(candidates.values());
|
|
162
|
+
return pool[Math.floor(Math.random() * pool.length)].id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function dzAutoPlay(trackId, title = null, artist = null, retries = 3) {
|
|
166
|
+
async function fetchWithRetryJson(url, options = {}, retriesLeft = 3) {
|
|
167
|
+
for (let attempt = 0; attempt < retriesLeft; attempt++) {
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch(url, options);
|
|
170
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
171
|
+
return await res.json();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (attempt === retriesLeft - 1) throw err;
|
|
174
|
+
await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let trackData = null;
|
|
180
|
+
|
|
181
|
+
if (trackId) {
|
|
182
|
+
try {
|
|
183
|
+
trackData = await fetchWithRetryJson(`https://api.deezer.com/track/${trackId}`, {}, retries);
|
|
184
|
+
if (trackData?.error) trackData = null;
|
|
185
|
+
} catch (_) {}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!trackData && title && artist) {
|
|
189
|
+
try {
|
|
190
|
+
const q = encodeURIComponent(`${title} ${artist}`);
|
|
191
|
+
const searchRes = await fetchWithRetryJson(`https://api.deezer.com/search?q=${q}&limit=1`, {}, retries);
|
|
192
|
+
if (searchRes?.data?.[0]) trackData = searchRes.data[0];
|
|
193
|
+
} catch (_) {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!trackData || trackData.error) return null;
|
|
197
|
+
|
|
198
|
+
const originalTrackId = String(trackData.id);
|
|
199
|
+
const artistId = trackData.artist?.id ? String(trackData.artist.id) : null;
|
|
200
|
+
const candidates = new Map();
|
|
201
|
+
|
|
202
|
+
function addTracksArray(arr) {
|
|
203
|
+
if (!Array.isArray(arr)) return;
|
|
204
|
+
for (const t of arr) {
|
|
205
|
+
if (!t || !t.id) continue;
|
|
206
|
+
const tid = String(t.id);
|
|
207
|
+
if (tid === originalTrackId) continue;
|
|
208
|
+
if (!candidates.has(tid)) candidates.set(tid, t);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (artistId) {
|
|
213
|
+
try {
|
|
214
|
+
const topRes = await fetchWithRetryJson(`https://api.deezer.com/artist/${artistId}/top?limit=12`, {}, retries);
|
|
215
|
+
if (topRes?.data) addTracksArray(topRes.data);
|
|
216
|
+
} catch (_) {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (artistId) {
|
|
220
|
+
try {
|
|
221
|
+
const relRes = await fetchWithRetryJson(`https://api.deezer.com/artist/${artistId}/related`, {}, retries);
|
|
222
|
+
const relatedArtists = (relRes && relRes.data) || [];
|
|
223
|
+
const toFetch = relatedArtists.slice(0, 5);
|
|
224
|
+
|
|
225
|
+
const topPromises = toFetch.map((ra) =>
|
|
226
|
+
fetchWithRetryJson(`https://api.deezer.com/artist/${ra.id}/top?limit=6`, {}, retries)
|
|
227
|
+
.then((r) => (r?.data ? r.data : []))
|
|
228
|
+
.catch(() => [])
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const topResults = await Promise.all(topPromises);
|
|
232
|
+
topResults.forEach((arr) => addTracksArray(arr));
|
|
233
|
+
} catch (_) {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (candidates.size === 0) {
|
|
237
|
+
try {
|
|
238
|
+
const q = `${encodeURIComponent(trackData.title || "")} ${encodeURIComponent(trackData.artist?.name || "")}`;
|
|
239
|
+
const searchRes = await fetchWithRetryJson(`https://api.deezer.com/search?q=${q}&limit=20`, {}, retries);
|
|
240
|
+
if (searchRes?.data) addTracksArray(searchRes.data);
|
|
241
|
+
} catch (_) {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const arr = Array.from(candidates.values());
|
|
245
|
+
if (!arr.length) return null;
|
|
246
|
+
const pick = arr[Math.floor(Math.random() * arr.length)];
|
|
247
|
+
return `https://www.deezer.com/track/${pick.id}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { scAutoPlay, spAutoPlay, amAutoPlay, dzAutoPlay };
|
|
@@ -23,6 +23,8 @@ class Muthera extends EventEmitter {
|
|
|
23
23
|
this.initiated = false;
|
|
24
24
|
this.send = options.send || null;
|
|
25
25
|
this.defaultSearchPlatform = options.defaultSearchPlatform || "ytsearch";
|
|
26
|
+
this.spotifyClientId = options.spotifyClientId || null;
|
|
27
|
+
this.spotifyClientSecret = options.spotifyClientSecret || null;
|
|
26
28
|
this.tracks = [];
|
|
27
29
|
this.loadType = null;
|
|
28
30
|
this.playlistInfo = null;
|
|
@@ -229,6 +229,41 @@ class Player extends EventEmitter {
|
|
|
229
229
|
} else {
|
|
230
230
|
return fail();
|
|
231
231
|
}
|
|
232
|
+
return this;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return fail(e);
|
|
235
|
+
}
|
|
236
|
+
} else if (player.previous.info.sourceName === "spsearch") {
|
|
237
|
+
try {
|
|
238
|
+
const trackId = await spAutoPlay(
|
|
239
|
+
player.previous.info.identifier,
|
|
240
|
+
this.muthera.spotifyClientId,
|
|
241
|
+
this.muthera.spotifyClientSecret
|
|
242
|
+
);
|
|
243
|
+
if (!trackId) return fail();
|
|
244
|
+
|
|
245
|
+
const response = await this.muthera.resolve({
|
|
246
|
+
query: `https://open.spotify.com/track/${trackId}`,
|
|
247
|
+
requester: player.previous.info.requester,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
!response ||
|
|
252
|
+
!response.tracks ||
|
|
253
|
+
["error", "empty"].includes(response.loadType)
|
|
254
|
+
)
|
|
255
|
+
return fail();
|
|
256
|
+
|
|
257
|
+
const track =
|
|
258
|
+
response.tracks[Math.floor(Math.random() * response.tracks.length)];
|
|
259
|
+
|
|
260
|
+
if (this.connected) {
|
|
261
|
+
this.queue.push(track);
|
|
262
|
+
await this.play();
|
|
263
|
+
} else {
|
|
264
|
+
return fail();
|
|
265
|
+
}
|
|
266
|
+
|
|
232
267
|
return this;
|
|
233
268
|
} catch (e) {
|
|
234
269
|
return fail(e);
|
|
@@ -273,7 +308,39 @@ class Player extends EventEmitter {
|
|
|
273
308
|
const title = player.previous.info.title;
|
|
274
309
|
const platform = this.muthera.defaultSearchPlatform;
|
|
275
310
|
|
|
276
|
-
if (platform.includes("
|
|
311
|
+
if (platform.includes("spsearch")) {
|
|
312
|
+
// Use Spotify related-artist API for proper similar song recommendations
|
|
313
|
+
const trackId = await spAutoPlay(
|
|
314
|
+
player.previous.info.identifier,
|
|
315
|
+
this.muthera.spotifyClientId,
|
|
316
|
+
this.muthera.spotifyClientSecret
|
|
317
|
+
);
|
|
318
|
+
if (!trackId) return fail();
|
|
319
|
+
|
|
320
|
+
const response = await this.muthera.resolve({
|
|
321
|
+
query: `https://open.spotify.com/track/${trackId}`,
|
|
322
|
+
requester: player.previous.info.requester,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (
|
|
326
|
+
!response ||
|
|
327
|
+
!response.tracks ||
|
|
328
|
+
["error", "empty"].includes(response.loadType) ||
|
|
329
|
+
response.tracks.length === 0
|
|
330
|
+
)
|
|
331
|
+
return fail();
|
|
332
|
+
|
|
333
|
+
const track =
|
|
334
|
+
response.tracks[Math.floor(Math.random() * response.tracks.length)];
|
|
335
|
+
|
|
336
|
+
if (this.connected) {
|
|
337
|
+
this.queue.push(track);
|
|
338
|
+
await this.play();
|
|
339
|
+
} else {
|
|
340
|
+
return fail();
|
|
341
|
+
}
|
|
342
|
+
return this;
|
|
343
|
+
} else if (platform.includes("dz")) {
|
|
277
344
|
// Use Deezer related-artist API for proper similar song recommendations
|
|
278
345
|
const data = await dzAutoPlay(null, title, artist);
|
|
279
346
|
if (!data) return fail();
|
|
@@ -507,6 +574,10 @@ class Player extends EventEmitter {
|
|
|
507
574
|
}
|
|
508
575
|
|
|
509
576
|
trackEnd(player, track, payload) {
|
|
577
|
+
// If the track ended due to an error (loadFailed), trackError() already
|
|
578
|
+
// handled advancing the queue / emitting queueEnd. Skip to avoid double handling.
|
|
579
|
+
if (payload.reason === "loadFailed") return;
|
|
580
|
+
|
|
510
581
|
this.previous = track;
|
|
511
582
|
if (this.loop === "track") {
|
|
512
583
|
player.queue.unshift(this.previous);
|