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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muthera",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "A simple Lavalink wrapper for Discord music bot. Forked from Niizuki.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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 < retries; 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 === retries - 1) {
74
- throw new Error("No track found.");
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 recommendationsResponse = await fetchWithRetry(
87
- `https://api.spotify.com/v1/recommendations?limit=10&seed_tracks=${track_id}`,
88
- {
89
- headers: {
90
- Authorization: `Bearer ${accessToken}`,
91
- "Content-Type": "application/json",
92
- },
93
- },
94
- retries
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
- return recommendationsResponse.tracks[
98
- Math.floor(Math.random() * recommendationsResponse.tracks.length)
99
- ].id;
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
- const trackData = await fetchWithRetryJson(`https://api.deezer.com/track/${trackId}`, {}, retries);
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
- this.tracks = [new Track(response.data, requester, node)];
160
+ tracks = [new Track(response.data, requester, node)];
160
161
  } else if (response.loadType === "playlist") {
161
- this.tracks = response.data.tracks.map(
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
- this.tracks = null;
169
+ tracks = [];
169
170
  } else {
170
- this.tracks = response.data.map(
171
+ tracks = response.data?.map(
171
172
  (track) => new Track(track, requester, node)
172
- );
173
+ ) ?? [];
173
174
  }
174
175
 
175
- this.playlistInfo = response.data.info ?? null;
176
- this.loadType = response.loadType ?? null;
177
- return this;
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
- // Use YouTube Music search as fallback for Spotify autoplay since Spotify API is blocked
231
- const title = player.previous.info.title || '';
232
- const artist = player.previous.info.author || '';
233
-
234
- // Create multiple search strategies for better results
235
- const searchQueries = [
236
- `${artist} similar songs`,
237
- `${artist} music mix`,
238
- `music like ${title}`,
239
- `${artist} playlist`,
240
- `${title} ${artist} radio`
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
- // Filter out tracks that are too similar to the current one
267
- let availableTracks = response.tracks.filter(track =>
268
- track.info.title.toLowerCase() !== title.toLowerCase()
269
- );
270
-
271
- // If no different tracks found, use all tracks but skip first one
272
- if (availableTracks.length === 0) {
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
- let track = availableTracks[Math.floor(Math.random() * availableTracks.length)];
283
+ const track =
284
+ response.tracks[Math.floor(Math.random() * response.tracks.length)];
278
285
 
279
- if (this.connected) {
280
- this.queue.push(track);
281
- await this.play();
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
- return this.stop();
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
- return this;
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('Spotify autoplay error:', e);
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.stop();
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.stop();
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) {