muthera 1.0.2

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.
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ # Enable version updates for npm
4
+ - package-ecosystem: "npm"
5
+ # Look for `package.json` and `lock` files in the `root` directory
6
+ directory: "/"
7
+ # Check the npm registry for updates every day (weekdays)
8
+ schedule:
9
+ interval: "daily"
10
+ # Enable version updates for Docker
11
+ - package-ecosystem: "docker"
12
+ # Look for a `Dockerfile` in the `root` directory
13
+ directory: "/"
14
+ # Check for updates once a week
15
+ schedule:
16
+ interval: "weekly"
@@ -0,0 +1,22 @@
1
+ name: Publish Package to npmjs
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+ push:
7
+ branches: "master"
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v2
14
+ # Setup .npmrc file to publish to npm
15
+ - uses: actions/setup-node@v2
16
+ with:
17
+ node-version: '16.x'
18
+ registry-url: 'https://registry.npmjs.org'
19
+ - run: npm ci
20
+ - run: npm publish
21
+ env:
22
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025 urfavteddybear
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ <div align="center">
2
+ <h1>Muthera</h1>
3
+ <p>A simple discord music bot wrapper.</p>
4
+ <p>
5
+ <a href="https://www.npmjs.com/package/muthera"><img src="https://img.shields.io/npm/v/muthera?maxAge=3600" alt="NPM version" /></a>
6
+ <p>
7
+ <p>
8
+ <a href="https://www.npmjs.com/package/muthera"><img src="https://nodei.co/npm/muthera.png?downloads=true&stars=true" alt="NPM Banner"></a>
9
+ </p>
10
+ </div>
11
+
12
+ ## Attribution
13
+
14
+ This project is forked from the original [Niizuki](https://www.npmjs.com/package/niizuki) package by [LewdHuTao](https://github.com/LewdHuTao). Since the original project is no longer maintained, this fork continues development and maintenance under the name "Muthera".
15
+
16
+ - **Original Author**: LewdHuTao
17
+ - **Original Repository**: [shittybot/niizuki](https://github.com/shittybot/niizuki)
18
+ - **Current Maintainer**: [urfavteddybear](https://github.com/urfavteddybear)
19
+
20
+ ## Install
21
+ ```sh
22
+ npm install muthera
23
+ # or
24
+ yarn add muthera
25
+ ```
26
+
27
+ ## Example
28
+ ```js
29
+ const { Muthera } = require("muthera"); // Import muthera
30
+
31
+ client.on("ready", async (client) => {
32
+ // defined nodes
33
+ const nodes = [
34
+ {
35
+ name: "localhost",
36
+ host: "",
37
+ password: "",
38
+ port: 2333,
39
+ secure: false,
40
+ },
41
+ ];
42
+
43
+ // initialize manager
44
+ client.manager = new Muthera(nodes, {
45
+ send: (payload) => {
46
+ const guild = this.guilds.cache.get(payload.d.guild_id);
47
+ if (guild) guild.shard.send(payload);
48
+ },
49
+ defaultSearchPlatform: "ytmsearch",
50
+ reconnectTimeout: 600000,
51
+ reconnectTries: 1000,
52
+ });
53
+ });
54
+
55
+ client.on("interactionCreate", async (interaction) => {
56
+ if (slashCmd === "play") {
57
+ const player = client.manager.createConnection({
58
+ guildId: interaction.guild.id,
59
+ voiceChannel: interaction.member.voice.channel.id,
60
+ textChannel: interaction.channel.id,
61
+ deaf: true,
62
+ });
63
+
64
+ const resolve = await client.manager.resolve({
65
+ query: query,
66
+ requester: interaction.user.id,
67
+ });
68
+ const { loadType, tracks, playlistInfo } = resolve;
69
+
70
+ if (loadType === "track" || loadType === "search") {
71
+ const track = tracks.shift();
72
+ player.queue.add(track);
73
+ interaction.reply(
74
+ `Add [${track.info.title}](${track.info.uri}) to the queue.`
75
+ );
76
+ if (!player.playing && !player.paused) return player.play();
77
+ }
78
+
79
+ if (loadType === "playlist") {
80
+ for (const track of resolve.tracks) {
81
+ track.info.requester = interaction.user.id;
82
+ player.queue.add(track);
83
+ }
84
+
85
+ interaction.reply(`Add \`${playlistInfo.name}\` to the queue`);
86
+ if (!player.playing && !player.paused) return player.play();
87
+ } else {
88
+ return interaction.reply("There are no results found.");
89
+ }
90
+ }
91
+ });
92
+
93
+ client.login("token");
94
+
95
+ client.manager.on("nodeConnect", async (node) => {
96
+ console.log(`${node.name} is connected.`);
97
+ });
98
+
99
+ client.manager.on("nodeDisconnect", async (node) => {
100
+ console.log(`${node.name} is disconnected.`);
101
+ });
102
+
103
+ client.manager.on("trackStart", async (player, track) => {
104
+ const channel = client.channels.cache.get(player.textChannel);
105
+
106
+ channel.send(
107
+ `Now Playing: [${track.info.title}](${track.info.uri}) [${track.info.requester}]`
108
+ );
109
+ });
110
+
111
+ client.manager.on("queueEnd", async (player, track) => {
112
+ const channel = client.channels.cache.get(player.textChannel);
113
+
114
+ let autoPlay = false;
115
+
116
+ if (autoPlay) {
117
+ player.autoplay(player);
118
+ } else {
119
+ player.destroy();
120
+ channel.send(`The queue has ended`);
121
+ }
122
+ });
123
+
124
+ ```
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "muthera",
3
+ "version": "1.0.2",
4
+ "description": "A simple Lavalink wrapper for Discord music bot. Forked from Niizuki.",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "urfavteddybear",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@discordjs/collection": "^2.1.1",
13
+ "jsdom": "^26.1.0",
14
+ "ws": "^8.18.3"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/urfavteddybear/muthera.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/urfavteddybear/muthera/issues"
22
+ },
23
+ "homepage": "https://github.com/urfavteddybear/muthera#readme",
24
+ "keywords": [
25
+ "wrapper",
26
+ "discord",
27
+ "lavalink-v4",
28
+ "lavalink",
29
+ "music-bot"
30
+ ]
31
+ }
@@ -0,0 +1,171 @@
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, retries) {
68
+ for (let attempt = 0; attempt < retries; attempt++) {
69
+ try {
70
+ const res = await fetch(url, options);
71
+ return await res.json();
72
+ } catch (error) {
73
+ if (attempt === retries - 1) {
74
+ throw new Error("No track found.");
75
+ }
76
+ }
77
+ }
78
+ }
79
+ const tokenResponse = await fetchWithRetry(
80
+ "https://open.spotify.com/get_access_token?reason=transport&productType=embed",
81
+ {},
82
+ retries
83
+ );
84
+ const accessToken = tokenResponse.accessToken;
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
95
+ );
96
+
97
+ return recommendationsResponse.tracks[
98
+ Math.floor(Math.random() * recommendationsResponse.tracks.length)
99
+ ].id;
100
+ }
101
+
102
+ async function dzAutoPlay(trackId, retries = 3) {
103
+ async function fetchWithRetryJson(url, options = {}, retriesLeft = 3) {
104
+ for (let attempt = 0; attempt < retriesLeft; attempt++) {
105
+ try {
106
+ const res = await fetch(url, options);
107
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
108
+ return await res.json();
109
+ } catch (err) {
110
+ if (attempt === retriesLeft - 1) throw err;
111
+ await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
112
+ }
113
+ }
114
+ }
115
+
116
+ const trackData = await fetchWithRetryJson(`https://api.deezer.com/track/${trackId}`, {}, retries);
117
+ if (!trackData || trackData.error) throw new Error("Deezer track not found");
118
+
119
+ const originalTrackId = String(trackData.id);
120
+ const artistId = trackData.artist?.id ? String(trackData.artist.id) : null;
121
+ const candidates = new Map();
122
+
123
+ function addTracksArray(arr) {
124
+ if (!Array.isArray(arr)) return;
125
+ for (const t of arr) {
126
+ if (!t || !t.id) continue;
127
+ const tid = String(t.id);
128
+ if (tid === originalTrackId) continue;
129
+ if (!candidates.has(tid)) candidates.set(tid, t);
130
+ }
131
+ }
132
+
133
+ if (artistId) {
134
+ try {
135
+ const topRes = await fetchWithRetryJson(`https://api.deezer.com/artist/${artistId}/top?limit=12`, {}, retries);
136
+ if (topRes?.data) addTracksArray(topRes.data);
137
+ } catch (_) {}
138
+ }
139
+
140
+ if (artistId) {
141
+ try {
142
+ const relRes = await fetchWithRetryJson(`https://api.deezer.com/artist/${artistId}/related`, {}, retries);
143
+ const relatedArtists = (relRes && relRes.data) || [];
144
+ const toFetch = relatedArtists.slice(0, 5);
145
+
146
+ const topPromises = toFetch.map((ra) =>
147
+ fetchWithRetryJson(`https://api.deezer.com/artist/${ra.id}/top?limit=6`, {}, retries)
148
+ .then((r) => (r?.data ? r.data : []))
149
+ .catch(() => [])
150
+ );
151
+
152
+ const topResults = await Promise.all(topPromises);
153
+ topResults.forEach((arr) => addTracksArray(arr));
154
+ } catch (_) {}
155
+ }
156
+
157
+ if (candidates.size === 0) {
158
+ try {
159
+ const q = `${encodeURIComponent(trackData.title || "")} ${encodeURIComponent(trackData.artist?.name || "")}`;
160
+ const searchRes = await fetchWithRetryJson(`https://api.deezer.com/search?q=${q}&limit=20`, {}, retries);
161
+ if (searchRes?.data) addTracksArray(searchRes.data);
162
+ } catch (_) {}
163
+ }
164
+
165
+ const arr = Array.from(candidates.values());
166
+ if (!arr.length) throw new Error("No Deezer recommendations found");
167
+ const pick = arr[Math.floor(Math.random() * arr.length)];
168
+ return `https://www.deezer.com/track/${pick.id}`;
169
+ }
170
+
171
+ module.exports = { scAutoPlay, spAutoPlay, amAutoPlay, dzAutoPlay };
@@ -0,0 +1,57 @@
1
+ async function getImageUrl(info) {
2
+ if (info.sourceName === "spotify") {
3
+ try {
4
+ const match = info.uri.match(/track\/([a-zA-Z0-9]+)/);
5
+ if (match) {
6
+ const res = await fetch(
7
+ `https://open.spotify.com/oembed?url=${info.uri}`
8
+ );
9
+ const json = await res.json();
10
+
11
+ return json.thumbnail_url;
12
+ }
13
+ } catch (error) {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ if (info.sourceName === "applemusic") {
19
+ try {
20
+ const res = await fetch(
21
+ `https://itunes.apple.com/lookup?id=${info.identifier}`
22
+ );
23
+
24
+ const response = await res.json();
25
+
26
+ return response.results[0].artworkUrl100;
27
+ } catch (error) {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ if (info.sourceName === "soundcloud") {
33
+ try {
34
+ const res = await fetch(
35
+ `https://soundcloud.com/oembed?format=json&url=${info.uri}`
36
+ );
37
+ const json = await res.json();
38
+ const thumbnailUrl = json.thumbnail_url;
39
+
40
+ return thumbnailUrl;
41
+ } catch (error) {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ if (info.sourceName === "youtube") {
47
+ try {
48
+ const thumbnailUrl = `https://img.youtube.com/vi/${info.identifier}/maxresdefault.jpg`;
49
+
50
+ return thumbnailUrl;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = { getImageUrl };