muthera 1.0.8 → 1.0.10

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.8",
3
+ "version": "1.0.10",
4
4
  "description": "A simple Lavalink wrapper for Discord music bot. Forked from Niizuki.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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
- const tokenResponse = await fetchWithRetry(
81
- "https://open.spotify.com/get_access_token?reason=transport&productType=embed"
82
- );
83
- const accessToken = tokenResponse.accessToken;
84
- if (!accessToken) throw new Error("Failed to get Spotify token.");
85
-
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
- );
96
- const artistId = trackData?.artists?.[0]?.id;
97
- if (!artistId) throw new Error("No artist found for track.");
98
-
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;
117
- }
118
-
119
- async function dzAutoPlay(trackId, title = null, artist = null, retries = 3) {
120
- async function fetchWithRetryJson(url, options = {}, retriesLeft = 3) {
121
- for (let attempt = 0; attempt < retriesLeft; attempt++) {
122
- try {
123
- const res = await fetch(url, options);
124
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
125
- return await res.json();
126
- } catch (err) {
127
- if (attempt === retriesLeft - 1) throw err;
128
- await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
129
- }
130
- }
131
- }
132
-
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
-
150
- if (!trackData || trackData.error) return null;
151
-
152
- const originalTrackId = String(trackData.id);
153
- const artistId = trackData.artist?.id ? String(trackData.artist.id) : null;
154
- const candidates = new Map();
155
-
156
- function addTracksArray(arr) {
157
- if (!Array.isArray(arr)) return;
158
- for (const t of arr) {
159
- if (!t || !t.id) continue;
160
- const tid = String(t.id);
161
- if (tid === originalTrackId) continue;
162
- if (!candidates.has(tid)) candidates.set(tid, t);
163
- }
164
- }
165
-
166
- if (artistId) {
167
- try {
168
- const topRes = await fetchWithRetryJson(`https://api.deezer.com/artist/${artistId}/top?limit=12`, {}, retries);
169
- if (topRes?.data) addTracksArray(topRes.data);
170
- } catch (_) {}
171
- }
172
-
173
- if (artistId) {
174
- try {
175
- const relRes = await fetchWithRetryJson(`https://api.deezer.com/artist/${artistId}/related`, {}, retries);
176
- const relatedArtists = (relRes && relRes.data) || [];
177
- const toFetch = relatedArtists.slice(0, 5);
178
-
179
- const topPromises = toFetch.map((ra) =>
180
- fetchWithRetryJson(`https://api.deezer.com/artist/${ra.id}/top?limit=6`, {}, retries)
181
- .then((r) => (r?.data ? r.data : []))
182
- .catch(() => [])
183
- );
184
-
185
- const topResults = await Promise.all(topPromises);
186
- topResults.forEach((arr) => addTracksArray(arr));
187
- } catch (_) {}
188
- }
189
-
190
- if (candidates.size === 0) {
191
- try {
192
- const q = `${encodeURIComponent(trackData.title || "")} ${encodeURIComponent(trackData.artist?.name || "")}`;
193
- const searchRes = await fetchWithRetryJson(`https://api.deezer.com/search?q=${q}&limit=20`, {}, retries);
194
- if (searchRes?.data) addTracksArray(searchRes.data);
195
- } catch (_) {}
196
- }
197
-
198
- const arr = Array.from(candidates.values());
199
- if (!arr.length) return null;
200
- const pick = arr[Math.floor(Math.random() * arr.length)];
201
- return `https://www.deezer.com/track/${pick.id}`;
202
- }
203
-
204
- module.exports = { scAutoPlay, spAutoPlay, amAutoPlay, dzAutoPlay };
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;
@@ -117,13 +117,16 @@ class Player extends EventEmitter {
117
117
  )
118
118
  return fail();
119
119
 
120
- let track =
121
- response.tracks[
122
- Math.floor(Math.random() * Math.floor(response.tracks.length))
123
- ];
120
+ // Filter out the previous track to avoid immediate duplicate
121
+ const tracks = response.tracks.filter(
122
+ (t) => t.info.identifier !== player.previous.info.identifier
123
+ );
124
+
125
+ if (!tracks.length) return fail();
124
126
 
125
127
  if (this.connected) {
126
- this.queue.push(track);
128
+ // Push entire Mix playlist into queue
129
+ for (const t of tracks) this.queue.push(t);
127
130
  await this.play();
128
131
  } else {
129
132
  return fail();
@@ -148,13 +151,16 @@ class Player extends EventEmitter {
148
151
  )
149
152
  return fail();
150
153
 
151
- let track =
152
- response.tracks[
153
- Math.floor(Math.random() * Math.floor(response.tracks.length))
154
- ];
154
+ // Filter out the previous track to avoid immediate duplicate
155
+ const tracks = response.tracks.filter(
156
+ (t) => t.info.identifier !== player.previous.info.identifier
157
+ );
158
+
159
+ if (!tracks.length) return fail();
155
160
 
156
161
  if (this.connected) {
157
- this.queue.push(track);
162
+ // Push entire Mix playlist into queue
163
+ for (const t of tracks) this.queue.push(t);
158
164
  await this.play();
159
165
  } else {
160
166
  return fail();
@@ -223,6 +229,41 @@ class Player extends EventEmitter {
223
229
  } else {
224
230
  return fail();
225
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
+
226
267
  return this;
227
268
  } catch (e) {
228
269
  return fail(e);
@@ -267,7 +308,39 @@ class Player extends EventEmitter {
267
308
  const title = player.previous.info.title;
268
309
  const platform = this.muthera.defaultSearchPlatform;
269
310
 
270
- if (platform.includes("dz")) {
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")) {
271
344
  // Use Deezer related-artist API for proper similar song recommendations
272
345
  const data = await dzAutoPlay(null, title, artist);
273
346
  if (!data) return fail();