muthera 1.0.9 → 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.9",
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;
@@ -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("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")) {
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();