muthera 1.0.5 → 1.0.7
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 +55 -22
- package/src/structures/Muthera.js +10 -8
- package/src/structures/mutheraPlayer.js +103 -55
package/package.json
CHANGED
|
@@ -64,42 +64,59 @@ async function amAutoPlay(track_id, retries = 3) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function spAutoPlay(track_id, retries = 3) {
|
|
67
|
-
async function fetchWithRetry(url, options, retries) {
|
|
68
|
-
for (let attempt = 0; attempt <
|
|
67
|
+
async function fetchWithRetry(url, options = {}, retriesLeft = retries) {
|
|
68
|
+
for (let attempt = 0; attempt < retriesLeft; attempt++) {
|
|
69
69
|
try {
|
|
70
70
|
const res = await fetch(url, options);
|
|
71
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
71
72
|
return await res.json();
|
|
72
73
|
} catch (error) {
|
|
73
|
-
if (attempt ===
|
|
74
|
-
|
|
75
|
-
}
|
|
74
|
+
if (attempt === retriesLeft - 1) throw new Error("No track found.");
|
|
75
|
+
await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
|
|
79
80
|
const tokenResponse = await fetchWithRetry(
|
|
80
|
-
"https://open.spotify.com/get_access_token?reason=transport&productType=embed"
|
|
81
|
-
{},
|
|
82
|
-
retries
|
|
81
|
+
"https://open.spotify.com/get_access_token?reason=transport&productType=embed"
|
|
83
82
|
);
|
|
84
83
|
const accessToken = tokenResponse.accessToken;
|
|
84
|
+
if (!accessToken) throw new Error("Failed to get Spotify token.");
|
|
85
85
|
|
|
86
|
-
const
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
86
|
+
const headers = {
|
|
87
|
+
Authorization: `Bearer ${accessToken}`,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Get the track to find its artist
|
|
92
|
+
const trackData = await fetchWithRetry(
|
|
93
|
+
`https://api.spotify.com/v1/tracks/${track_id}`,
|
|
94
|
+
{ headers }
|
|
95
95
|
);
|
|
96
|
+
const artistId = trackData?.artists?.[0]?.id;
|
|
97
|
+
if (!artistId) throw new Error("No artist found for track.");
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
// Get related artists
|
|
100
|
+
const relatedData = await fetchWithRetry(
|
|
101
|
+
`https://api.spotify.com/v1/artists/${artistId}/related-artists`,
|
|
102
|
+
{ headers }
|
|
103
|
+
);
|
|
104
|
+
const relatedArtists = relatedData?.artists ?? [];
|
|
105
|
+
if (!relatedArtists.length) throw new Error("No related artists found.");
|
|
106
|
+
|
|
107
|
+
// Pick a random related artist and get their top tracks
|
|
108
|
+
const randomArtist = relatedArtists[Math.floor(Math.random() * relatedArtists.length)];
|
|
109
|
+
const topTracksData = await fetchWithRetry(
|
|
110
|
+
`https://api.spotify.com/v1/artists/${randomArtist.id}/top-tracks?market=US`,
|
|
111
|
+
{ headers }
|
|
112
|
+
);
|
|
113
|
+
const tracks = topTracksData?.tracks ?? [];
|
|
114
|
+
if (!tracks.length) throw new Error("No top tracks found.");
|
|
115
|
+
|
|
116
|
+
return tracks[Math.floor(Math.random() * tracks.length)].id;
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
async function dzAutoPlay(trackId, retries = 3) {
|
|
119
|
+
async function dzAutoPlay(trackId, title = null, artist = null, retries = 3) {
|
|
103
120
|
async function fetchWithRetryJson(url, options = {}, retriesLeft = 3) {
|
|
104
121
|
for (let attempt = 0; attempt < retriesLeft; attempt++) {
|
|
105
122
|
try {
|
|
@@ -113,7 +130,23 @@ async function dzAutoPlay(trackId, retries = 3) {
|
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
|
|
133
|
+
let trackData = null;
|
|
134
|
+
|
|
135
|
+
if (trackId) {
|
|
136
|
+
try {
|
|
137
|
+
trackData = await fetchWithRetryJson(`https://api.deezer.com/track/${trackId}`, {}, retries);
|
|
138
|
+
if (trackData?.error) trackData = null;
|
|
139
|
+
} catch (_) {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!trackData && title && artist) {
|
|
143
|
+
try {
|
|
144
|
+
const q = encodeURIComponent(`${title} ${artist}`);
|
|
145
|
+
const searchRes = await fetchWithRetryJson(`https://api.deezer.com/search?q=${q}&limit=1`, {}, retries);
|
|
146
|
+
if (searchRes?.data?.[0]) trackData = searchRes.data[0];
|
|
147
|
+
} catch (_) {}
|
|
148
|
+
}
|
|
149
|
+
|
|
117
150
|
if (!trackData || trackData.error) throw new Error("Deezer track not found");
|
|
118
151
|
|
|
119
152
|
const originalTrackId = String(trackData.id);
|
|
@@ -155,26 +155,28 @@ class Muthera extends EventEmitter {
|
|
|
155
155
|
`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`
|
|
156
156
|
);
|
|
157
157
|
|
|
158
|
+
let tracks = [];
|
|
158
159
|
if (response.loadType === "track") {
|
|
159
|
-
|
|
160
|
+
tracks = [new Track(response.data, requester, node)];
|
|
160
161
|
} else if (response.loadType === "playlist") {
|
|
161
|
-
|
|
162
|
+
tracks = response.data.tracks.map(
|
|
162
163
|
(track) => new Track(track, requester, node)
|
|
163
164
|
);
|
|
164
165
|
} else if (
|
|
165
166
|
response.loadType === "error" ||
|
|
166
167
|
response.loadType === "empty"
|
|
167
168
|
) {
|
|
168
|
-
|
|
169
|
+
tracks = [];
|
|
169
170
|
} else {
|
|
170
|
-
|
|
171
|
+
tracks = response.data?.map(
|
|
171
172
|
(track) => new Track(track, requester, node)
|
|
172
|
-
);
|
|
173
|
+
) ?? [];
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const playlistInfo = response.data?.info ?? null;
|
|
177
|
+
const loadType = response.loadType ?? null;
|
|
178
|
+
|
|
179
|
+
return { tracks, loadType, playlistInfo };
|
|
178
180
|
} catch (error) {
|
|
179
181
|
throw new Error(error);
|
|
180
182
|
}
|
|
@@ -121,6 +121,37 @@ class Player extends EventEmitter {
|
|
|
121
121
|
return this.stop();
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
return this;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return this.stop();
|
|
127
|
+
}
|
|
128
|
+
} else if (player.previous.info.sourceName === "youtubemusic") {
|
|
129
|
+
try {
|
|
130
|
+
let data = `https://music.youtube.com/watch?v=${player.previous.info.identifier}&list=RD${player.previous.info.identifier}`;
|
|
131
|
+
let response = await this.muthera.resolve({
|
|
132
|
+
query: data,
|
|
133
|
+
requester: player.previous.info.requester,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
!response ||
|
|
138
|
+
!response.tracks ||
|
|
139
|
+
["error", "empty"].includes(response.loadType)
|
|
140
|
+
)
|
|
141
|
+
return this.stop();
|
|
142
|
+
|
|
143
|
+
let track =
|
|
144
|
+
response.tracks[
|
|
145
|
+
Math.floor(Math.random() * Math.floor(response.tracks.length))
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
if (this.connected) {
|
|
149
|
+
this.queue.push(track);
|
|
150
|
+
await this.play();
|
|
151
|
+
} else {
|
|
152
|
+
return this.stop();
|
|
153
|
+
}
|
|
154
|
+
|
|
124
155
|
return this;
|
|
125
156
|
} catch (e) {
|
|
126
157
|
return this.stop();
|
|
@@ -227,65 +258,70 @@ class Player extends EventEmitter {
|
|
|
227
258
|
}
|
|
228
259
|
} else if (player.previous.info.sourceName === "spotify") {
|
|
229
260
|
try {
|
|
230
|
-
|
|
231
|
-
const title = player.previous.info.title
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
// Try different search queries until we find tracks
|
|
244
|
-
let response = null;
|
|
245
|
-
for (const query of searchQueries) {
|
|
246
|
-
response = await this.muthera.resolve({
|
|
247
|
-
query: query,
|
|
248
|
-
source: "ytmsearch",
|
|
261
|
+
const artist = player.previous.info.author;
|
|
262
|
+
const title = player.previous.info.title;
|
|
263
|
+
const platform = this.muthera.defaultSearchPlatform;
|
|
264
|
+
|
|
265
|
+
if (platform.includes("dz")) {
|
|
266
|
+
// Use Deezer related-artist API for proper similar song recommendations
|
|
267
|
+
const data = await dzAutoPlay(null, title, artist);
|
|
268
|
+
if (!data) return this.stop();
|
|
269
|
+
|
|
270
|
+
const response = await this.muthera.resolve({
|
|
271
|
+
query: data,
|
|
249
272
|
requester: player.previous.info.requester,
|
|
250
273
|
});
|
|
251
|
-
|
|
252
|
-
if (response && response.tracks && response.tracks.length > 0 &&
|
|
253
|
-
!["error", "empty"].includes(response.loadType)) {
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
!response ||
|
|
260
|
-
!response.tracks ||
|
|
261
|
-
["error", "empty"].includes(response.loadType) ||
|
|
262
|
-
response.tracks.length === 0
|
|
263
|
-
)
|
|
264
|
-
return this.stop();
|
|
265
274
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
availableTracks = response.tracks.slice(1);
|
|
274
|
-
if (availableTracks.length === 0) availableTracks = response.tracks;
|
|
275
|
-
}
|
|
275
|
+
if (
|
|
276
|
+
!response ||
|
|
277
|
+
!response.tracks ||
|
|
278
|
+
["error", "empty"].includes(response.loadType) ||
|
|
279
|
+
response.tracks.length === 0
|
|
280
|
+
)
|
|
281
|
+
return this.stop();
|
|
276
282
|
|
|
277
|
-
|
|
283
|
+
const track =
|
|
284
|
+
response.tracks[Math.floor(Math.random() * response.tracks.length)];
|
|
278
285
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
if (this.connected) {
|
|
287
|
+
this.queue.push(track);
|
|
288
|
+
await this.play();
|
|
289
|
+
} else {
|
|
290
|
+
return this.stop();
|
|
291
|
+
}
|
|
292
|
+
return this;
|
|
282
293
|
} else {
|
|
283
|
-
|
|
284
|
-
|
|
294
|
+
// For YouTube and other platforms, search by artist and filter current song
|
|
295
|
+
const response = await this.muthera.resolve({
|
|
296
|
+
query: artist,
|
|
297
|
+
source: platform,
|
|
298
|
+
requester: player.previous.info.requester,
|
|
299
|
+
});
|
|
285
300
|
|
|
286
|
-
|
|
301
|
+
if (
|
|
302
|
+
!response ||
|
|
303
|
+
!response.tracks ||
|
|
304
|
+
["error", "empty"].includes(response.loadType) ||
|
|
305
|
+
response.tracks.length === 0
|
|
306
|
+
)
|
|
307
|
+
return this.stop();
|
|
308
|
+
|
|
309
|
+
const filtered = response.tracks.filter(
|
|
310
|
+
(t) => t.info.title.toLowerCase() !== title.toLowerCase()
|
|
311
|
+
);
|
|
312
|
+
const pool = filtered.length > 0 ? filtered : response.tracks;
|
|
313
|
+
const track = pool[Math.floor(Math.random() * pool.length)];
|
|
314
|
+
|
|
315
|
+
if (this.connected) {
|
|
316
|
+
this.queue.push(track);
|
|
317
|
+
await this.play();
|
|
318
|
+
} else {
|
|
319
|
+
return this.stop();
|
|
320
|
+
}
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
287
323
|
} catch (e) {
|
|
288
|
-
console.log(
|
|
324
|
+
console.log(e);
|
|
289
325
|
return this.stop();
|
|
290
326
|
}
|
|
291
327
|
}
|
|
@@ -483,14 +519,26 @@ class Player extends EventEmitter {
|
|
|
483
519
|
this.muthera.emit("queueEnd", player);
|
|
484
520
|
}
|
|
485
521
|
|
|
486
|
-
trackError(player, track, payload) {
|
|
522
|
+
async trackError(player, track, payload) {
|
|
487
523
|
this.muthera.emit("trackError", player, track, payload);
|
|
488
|
-
this.
|
|
524
|
+
this.playing = false;
|
|
525
|
+
this.previous = track;
|
|
526
|
+
if (player.queue.length > 0) {
|
|
527
|
+
await player.play();
|
|
528
|
+
} else {
|
|
529
|
+
this.muthera.emit("queueEnd", player);
|
|
530
|
+
}
|
|
489
531
|
}
|
|
490
532
|
|
|
491
|
-
trackStuck(player, track, payload) {
|
|
533
|
+
async trackStuck(player, track, payload) {
|
|
492
534
|
this.muthera.emit("trackStuck", player, track, payload);
|
|
493
|
-
this.
|
|
535
|
+
this.playing = false;
|
|
536
|
+
this.previous = track;
|
|
537
|
+
if (player.queue.length > 0) {
|
|
538
|
+
await player.play();
|
|
539
|
+
} else {
|
|
540
|
+
this.muthera.emit("queueEnd", player);
|
|
541
|
+
}
|
|
494
542
|
}
|
|
495
543
|
|
|
496
544
|
socketClosed(player, payload) {
|