2023-01-22 04:45:57 +00:00
|
|
|
const {Collection} = require("@projectdysnomia/dysnomia");
|
2022-12-10 21:49:25 +00:00
|
|
|
|
2023-09-13 04:51:51 +00:00
|
|
|
const {Readable} = require("node:stream");
|
2022-04-19 04:49:04 +00:00
|
|
|
const ffprobe = require("node-ffprobe");
|
|
|
|
|
|
|
|
const Command = require("../lib/command.js");
|
2023-01-24 01:22:53 +00:00
|
|
|
const {
|
|
|
|
formatTime,
|
|
|
|
parseHtmlEntities,
|
2023-10-05 19:41:47 +00:00
|
|
|
formatUsername,
|
2023-09-15 03:09:20 +00:00
|
|
|
selectionMessage,
|
2023-01-24 01:22:53 +00:00
|
|
|
} = require("../lib/utils.js");
|
2021-09-16 03:45:55 +00:00
|
|
|
|
|
|
|
const REGEX_YOUTUBE = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
|
|
|
|
const REGEX_YOUTUBE_PLAYLIST =
|
|
|
|
/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/playlist\?list=(.+)$/;
|
|
|
|
const REGEX_YOUTUBE_PLAYLIST_SHORT = /^PL[a-zA-Z0-9-_]{1,32}$/;
|
|
|
|
const REGEX_SOUNDCLOUD =
|
|
|
|
/^((https?:\/\/)?(www\.|m\.)?soundcloud\.com\/|sc:).+\/.+$/;
|
|
|
|
const REGEX_SOUNDCLOUD_PLAYLIST =
|
|
|
|
/^((https?:\/\/)?(www\.|m\.)?soundcloud\.com\/|sc:).+\/(sets\/.+|likes|tracks)$/;
|
|
|
|
const REGEX_FILE =
|
2022-07-30 18:02:39 +00:00
|
|
|
/^(https?:\/\/)?.*\..*\/.+\.(mp3|ogg|flac|wav|webm|mp4|mov|mkv|mod|s3m|it|xm)$/;
|
2021-09-16 03:45:55 +00:00
|
|
|
|
|
|
|
let SOUNDCLOUD_CLIENTID;
|
|
|
|
|
2022-10-09 18:03:18 +00:00
|
|
|
hf.voiceStorage = hf.voiceStorage || new Collection();
|
2022-04-19 04:49:04 +00:00
|
|
|
const voiceStorage = hf.voiceStorage;
|
2021-09-16 03:45:55 +00:00
|
|
|
|
|
|
|
// https://stackoverflow.com/a/12646864 § "Updating to ES6 / ECMAScript 2015"
|
|
|
|
function shuffleArray(array) {
|
|
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getSoundcloudClientID() {
|
|
|
|
if (SOUNDCLOUD_CLIENTID != null) {
|
|
|
|
return SOUNDCLOUD_CLIENTID;
|
|
|
|
}
|
|
|
|
const page = await fetch("https://soundcloud.com").then((res) => res.text());
|
|
|
|
const scripts = page
|
|
|
|
.match(/<script crossorigin src="(.+?)"><\/script>/g)
|
|
|
|
.reverse();
|
|
|
|
for (const script of scripts) {
|
|
|
|
const url = script.match(/src="(.+?)"/)[1];
|
|
|
|
const contents = await fetch(url).then((res) => res.text());
|
|
|
|
if (/,client_id:"(.+?)",/.test(contents)) {
|
|
|
|
const client_id = contents.match(/,client_id:"(.+?)",/)[1];
|
|
|
|
SOUNDCLOUD_CLIENTID = client_id;
|
|
|
|
return SOUNDCLOUD_CLIENTID;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
|
2022-04-24 18:04:40 +00:00
|
|
|
async function processPlaylist(
|
|
|
|
url,
|
|
|
|
type,
|
|
|
|
shuffle = false,
|
|
|
|
limit = -1,
|
|
|
|
offset = 0
|
|
|
|
) {
|
2022-04-19 04:49:04 +00:00
|
|
|
let playlist;
|
|
|
|
|
|
|
|
if (type === "yt") {
|
|
|
|
const playlistId =
|
|
|
|
url.match(REGEX_YOUTUBE_PLAYLIST)?.[4] ??
|
|
|
|
url.match(REGEX_YOUTUBE_PLAYLIST_SHORT)?.[0];
|
|
|
|
if (!playlistId) return null;
|
|
|
|
|
2023-09-13 04:24:17 +00:00
|
|
|
const baseUrl = "/playlists/" + playlistId;
|
2022-04-19 04:49:04 +00:00
|
|
|
|
2023-09-13 04:24:17 +00:00
|
|
|
const data = await fetch(hf.config.piped_api + baseUrl).then((res) =>
|
|
|
|
res.json()
|
|
|
|
);
|
2022-04-19 04:49:04 +00:00
|
|
|
|
2023-09-13 04:24:17 +00:00
|
|
|
playlist = data.relatedStreams;
|
2022-04-19 04:49:04 +00:00
|
|
|
|
2023-09-13 04:24:17 +00:00
|
|
|
let pageToken = data.nextpage;
|
|
|
|
while (pageToken?.startsWith("{")) {
|
|
|
|
const pageData = await fetch(
|
|
|
|
hf.config.piped_api +
|
|
|
|
"/nextpage" +
|
|
|
|
baseUrl +
|
|
|
|
"&nextpage=" +
|
|
|
|
encodeURIComponent(pageToken)
|
|
|
|
).then((res) => res.json());
|
|
|
|
if (pageData.nextpage) pageToken = pageData.nextpage;
|
2022-04-19 04:49:04 +00:00
|
|
|
|
2023-09-13 04:24:17 +00:00
|
|
|
playlist = [...playlist, ...pageData.relatedStreams];
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
} else if (type === "sc") {
|
|
|
|
const clientId = await getSoundcloudClientID();
|
|
|
|
|
|
|
|
if (url.indexOf("/likes") > -1) {
|
2022-04-24 18:09:39 +00:00
|
|
|
let userInfo = await fetch(
|
2022-04-19 04:49:04 +00:00
|
|
|
`https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${clientId}&limit=500`
|
|
|
|
).then((res) => res.json());
|
2022-04-24 18:09:39 +00:00
|
|
|
|
|
|
|
while (!userInfo.uri) {
|
|
|
|
userInfo = await fetch(
|
|
|
|
`https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${clientId}&limit=500`
|
|
|
|
).then((res) => res.json());
|
|
|
|
}
|
|
|
|
|
2022-04-19 04:49:04 +00:00
|
|
|
const likesUrl =
|
|
|
|
userInfo.uri.replace("api.", "api-v2.") +
|
|
|
|
"/likes?limit=500&client_id=" +
|
|
|
|
clientId;
|
|
|
|
|
|
|
|
let currentLikes = await fetch(likesUrl).then((res) => res.json());
|
|
|
|
playlist = currentLikes.collection;
|
|
|
|
|
|
|
|
while (currentLikes.next_href != null) {
|
|
|
|
currentLikes = await fetch(
|
|
|
|
currentLikes.next_href + "&client_id=" + clientId
|
|
|
|
).then((res) => res.json());
|
|
|
|
playlist = [...playlist, ...currentLikes.collection];
|
|
|
|
}
|
2024-04-12 17:30:04 +00:00
|
|
|
} else if (url.indexOf("/tracks")) {
|
|
|
|
let userInfo = await fetch(
|
|
|
|
`https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${clientId}&limit=500`
|
|
|
|
).then((res) => res.json());
|
|
|
|
|
|
|
|
while (!userInfo.uri) {
|
|
|
|
userInfo = await fetch(
|
|
|
|
`https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${clientId}&limit=500`
|
|
|
|
).then((res) => res.json());
|
|
|
|
}
|
|
|
|
|
|
|
|
const tracksUrl =
|
|
|
|
userInfo.uri.replace("api.", "api-v2.") +
|
|
|
|
"/tracks?limit=500&client_id=" +
|
|
|
|
clientId;
|
|
|
|
|
|
|
|
let currentTracks = await fetch(tracksUrl).then((res) => res.json());
|
|
|
|
playlist = currentTracks.collection;
|
|
|
|
|
|
|
|
while (currentTracks.next_href != null) {
|
|
|
|
currentTracks = await fetch(
|
|
|
|
currentTracks.next_href + "&client_id=" + clientId
|
|
|
|
).then((res) => res.json());
|
|
|
|
playlist = [...playlist, ...currentTracks.collection];
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
} else {
|
|
|
|
playlist = await fetch(
|
|
|
|
`https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${clientId}&limit=500`
|
|
|
|
)
|
|
|
|
.then((res) => res.json())
|
|
|
|
.then((obj) => obj.tracks);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shuffle === true && playlist != null) {
|
|
|
|
shuffleArray(playlist);
|
|
|
|
}
|
|
|
|
|
2022-04-24 18:04:40 +00:00
|
|
|
if (offset > 0) {
|
|
|
|
playlist = playlist.slice(offset);
|
|
|
|
}
|
|
|
|
|
2022-04-24 17:16:00 +00:00
|
|
|
if (limit > 0) {
|
|
|
|
playlist = playlist.slice(0, limit);
|
|
|
|
}
|
|
|
|
|
2022-04-19 04:49:04 +00:00
|
|
|
return playlist;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function createVoiceConnection(guild_id, voice_id, text_id) {
|
2022-12-10 22:08:58 +00:00
|
|
|
const state = {
|
|
|
|
voice_id,
|
|
|
|
text_id,
|
|
|
|
};
|
2022-12-10 21:49:25 +00:00
|
|
|
|
2023-01-22 05:07:27 +00:00
|
|
|
state.connection = await hf.bot.joinVoiceChannel(voice_id, {
|
2022-12-10 21:49:25 +00:00
|
|
|
selfDeaf: true,
|
|
|
|
selfMute: false,
|
|
|
|
});
|
|
|
|
state.queue = [];
|
|
|
|
|
|
|
|
state.onEnd = async function () {
|
|
|
|
if (state.queue.length > 0) {
|
|
|
|
const next = state.queue.splice(0, 1)[0];
|
2022-08-03 04:00:10 +00:00
|
|
|
await enqueue({
|
2022-04-19 04:49:04 +00:00
|
|
|
guild_id,
|
|
|
|
voice_id,
|
|
|
|
text_id,
|
2022-08-03 04:00:10 +00:00
|
|
|
url: next.url,
|
|
|
|
type: next.type,
|
|
|
|
addedBy: next.addedBy,
|
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
} else {
|
2023-01-22 04:45:57 +00:00
|
|
|
await state.connection.disconnect();
|
2022-12-10 21:49:25 +00:00
|
|
|
if (!state.__leave) {
|
2022-12-10 22:17:07 +00:00
|
|
|
await hf.bot.guilds.get(guild_id).channels.get(text_id).createMessage({
|
|
|
|
content: ":musical_note: Queue is empty, leaving voice channel.",
|
|
|
|
});
|
2022-12-10 22:08:58 +00:00
|
|
|
await hf.bot.leaveVoiceChannel(voice_id);
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
2023-01-22 04:45:57 +00:00
|
|
|
state.connection.off("end", state.onEnd);
|
2022-04-19 04:49:04 +00:00
|
|
|
voiceStorage.delete(guild_id);
|
|
|
|
}
|
|
|
|
};
|
2023-01-22 04:45:57 +00:00
|
|
|
state.connection.on("end", state.onEnd);
|
2022-12-10 21:49:25 +00:00
|
|
|
voiceStorage.set(guild_id, state);
|
2022-04-19 04:49:04 +00:00
|
|
|
|
2022-12-10 21:49:25 +00:00
|
|
|
return state;
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
|
2023-09-29 03:59:23 +00:00
|
|
|
const REGEX_HLS_AUDIO_TRACK = /#EXT-X-MEDIA:URI="(.+?)",TYPE=AUDIO,/;
|
2022-08-03 04:00:10 +00:00
|
|
|
async function enqueue({
|
2022-04-19 04:49:04 +00:00
|
|
|
guild_id,
|
|
|
|
voice_id,
|
|
|
|
text_id,
|
|
|
|
url,
|
|
|
|
type,
|
|
|
|
addedBy,
|
2022-05-04 21:46:29 +00:00
|
|
|
suppress = false,
|
2022-08-03 04:00:10 +00:00
|
|
|
queueNext = false,
|
|
|
|
}) {
|
2022-04-24 18:09:39 +00:00
|
|
|
if (!url) return;
|
|
|
|
|
2022-04-19 04:49:04 +00:00
|
|
|
const connection =
|
|
|
|
voiceStorage.get(guild_id) ??
|
|
|
|
(await createVoiceConnection(guild_id, voice_id, text_id));
|
|
|
|
const textChannel = hf.bot.guilds.get(guild_id).channels.get(text_id);
|
|
|
|
|
|
|
|
let title,
|
|
|
|
length,
|
|
|
|
thumbnail,
|
|
|
|
media,
|
|
|
|
stream = false;
|
|
|
|
|
|
|
|
if (type == "yt") {
|
|
|
|
let info;
|
2023-05-12 22:06:14 +00:00
|
|
|
let id = url;
|
2022-04-19 04:49:04 +00:00
|
|
|
try {
|
2023-04-26 23:16:22 +00:00
|
|
|
if (/^https?:\/\//.test(url)) {
|
|
|
|
const uri = new URL(url);
|
|
|
|
if (uri.hostname == "youtu.be") {
|
|
|
|
id = uri.pathname.substring(1);
|
|
|
|
} else if (uri.hostname.indexOf("youtube.com") > -1) {
|
|
|
|
id = uri.searchParams.get("v");
|
|
|
|
}
|
|
|
|
}
|
2023-09-13 04:30:32 +00:00
|
|
|
info = await fetch(`${hf.config.piped_api}/streams/${id}`).then((res) =>
|
|
|
|
res.json()
|
|
|
|
);
|
2022-04-19 04:49:04 +00:00
|
|
|
} catch (err) {
|
2022-12-10 21:55:39 +00:00
|
|
|
await textChannel.createMessage({
|
|
|
|
content: `:warning: Failed to get metadata: \`\`\`\n${err}\n\`\`\``,
|
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
|
2023-09-13 04:33:34 +00:00
|
|
|
title = info?.title;
|
2023-09-13 04:24:17 +00:00
|
|
|
length = info?.duration * 1000;
|
|
|
|
thumbnail = info?.thumbnailUrl;
|
2023-01-24 01:15:54 +00:00
|
|
|
|
2023-09-29 03:39:23 +00:00
|
|
|
const hlsUrl = new URL(info.hls);
|
2023-09-29 03:59:23 +00:00
|
|
|
const hlsBase = await fetch(info.hls)
|
|
|
|
.then((res) => res.text())
|
|
|
|
.then((data) =>
|
|
|
|
data.replaceAll(
|
|
|
|
"/api/manifest/",
|
|
|
|
`https://${hlsUrl.hostname}/api/manifest/`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2023-09-13 04:51:51 +00:00
|
|
|
media = Readable.from(
|
2023-09-29 03:59:23 +00:00
|
|
|
await fetch(hlsBase.match(REGEX_HLS_AUDIO_TRACK)[1])
|
2023-09-29 03:39:23 +00:00
|
|
|
.then((res) => res.text())
|
|
|
|
.then((data) =>
|
2023-09-29 03:44:56 +00:00
|
|
|
data.replaceAll(
|
2023-09-29 03:59:23 +00:00
|
|
|
"/videoplayback/",
|
|
|
|
`https://${hlsUrl.hostname}/videoplayback/`
|
2023-09-29 03:44:56 +00:00
|
|
|
)
|
2023-09-29 03:39:23 +00:00
|
|
|
)
|
2023-09-13 04:51:51 +00:00
|
|
|
);
|
2022-04-19 04:49:04 +00:00
|
|
|
} else if (type == "sc") {
|
2022-05-11 18:14:48 +00:00
|
|
|
if (url?.startsWith("sc:"))
|
2022-04-24 18:09:39 +00:00
|
|
|
url = url.replace(/^sc:/, "https://soundcloud.com/");
|
2022-04-19 04:49:04 +00:00
|
|
|
const client_id = await getSoundcloudClientID();
|
|
|
|
|
|
|
|
const info = await fetch(
|
|
|
|
`https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${client_id}`
|
|
|
|
).then((res) => res.json());
|
|
|
|
|
|
|
|
const formatUrl = info.media.transcodings.filter(
|
|
|
|
(obj) => !obj.snipped && obj.format.protocol == "progressive"
|
|
|
|
)[0].url;
|
|
|
|
const streamUrl = await fetch(`${formatUrl}?client_id=${client_id}`)
|
|
|
|
.then((res) => res.json())
|
|
|
|
.then((obj) => obj.url);
|
|
|
|
|
|
|
|
title = info.title;
|
|
|
|
length = info.duration;
|
|
|
|
thumbnail = info.artwork_url;
|
2023-01-24 01:15:54 +00:00
|
|
|
media = streamUrl;
|
2022-04-19 04:49:04 +00:00
|
|
|
} else if (type == "file") {
|
|
|
|
title = url;
|
|
|
|
let info;
|
|
|
|
|
|
|
|
try {
|
|
|
|
info = await ffprobe(url).then((obj) => obj.format);
|
|
|
|
} catch (err) {
|
2022-12-10 21:55:39 +00:00
|
|
|
textChannel.createMessage({
|
|
|
|
content: `:warning: Failed to get metadata: \`\`\`\n${err}\n\`\`\``,
|
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
stream = !info.duration;
|
|
|
|
|
|
|
|
if (info.tags) {
|
|
|
|
title = `${
|
2022-07-30 18:09:41 +00:00
|
|
|
info.tags.artist ??
|
|
|
|
info.tags.ARTIST ??
|
|
|
|
info.tags.album_artist ??
|
|
|
|
info.tags.ALBUM_ARTIST ??
|
|
|
|
"<unknown artist>"
|
|
|
|
} - ${info.tags.title ?? info.tags.TITLE ?? "<no title>"}`;
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
length = info.duration ? Math.floor(info.duration) * 1000 : 0;
|
2023-01-24 01:15:54 +00:00
|
|
|
media = url;
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
|
2023-01-22 04:45:57 +00:00
|
|
|
if (connection.connection.playing) {
|
2022-05-04 21:46:29 +00:00
|
|
|
const queueItem = {
|
2022-04-19 04:49:04 +00:00
|
|
|
url,
|
|
|
|
type,
|
|
|
|
title,
|
|
|
|
length,
|
|
|
|
addedBy,
|
|
|
|
stream,
|
2022-04-24 19:09:00 +00:00
|
|
|
id: Math.random().toString(16).substring(2),
|
2022-05-04 21:46:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if (queueNext === true) {
|
2022-12-10 21:49:25 +00:00
|
|
|
connection.queue.splice(0, 0, queueItem);
|
2022-05-04 21:46:29 +00:00
|
|
|
} else {
|
2022-12-10 21:49:25 +00:00
|
|
|
connection.queue.push(queueItem);
|
2022-05-04 21:46:29 +00:00
|
|
|
}
|
|
|
|
|
2022-04-19 04:49:04 +00:00
|
|
|
if (suppress === false) {
|
|
|
|
textChannel.createMessage({
|
|
|
|
embeds: [
|
|
|
|
{
|
2022-05-04 21:46:29 +00:00
|
|
|
title: `<:ms_tick:503341995348066313> Added to queue ${
|
|
|
|
queueNext === true ? "(next up)" : ""
|
|
|
|
}`,
|
2022-04-20 20:51:56 +00:00
|
|
|
color: 0x00cc00,
|
2022-04-19 04:49:04 +00:00
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: "Title",
|
2023-09-13 04:33:34 +00:00
|
|
|
value: (title !== url ? `[${title}](${url})` : url).substring(
|
|
|
|
0,
|
|
|
|
1024
|
|
|
|
),
|
2022-04-19 04:49:04 +00:00
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Length",
|
|
|
|
value: stream
|
|
|
|
? "<continuous>"
|
|
|
|
: length
|
|
|
|
? formatTime(length)
|
|
|
|
: "<unknown>",
|
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Added by",
|
|
|
|
value: `<@${addedBy}>`,
|
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
thumbnail: {
|
|
|
|
url: thumbnail,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!media) {
|
2022-12-10 21:55:39 +00:00
|
|
|
textChannel.createMessage({
|
|
|
|
content: `:warning: No usable media was found for \`${url}\`. May possibly be due to region restrictions or paywalls.`,
|
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-01-24 01:15:54 +00:00
|
|
|
await connection.connection.play(media, {
|
2023-01-22 04:45:57 +00:00
|
|
|
inlineVolume: true,
|
|
|
|
voiceDataTimeout: -1,
|
2023-09-29 03:46:19 +00:00
|
|
|
inputArgs: [
|
|
|
|
"-protocol_whitelist",
|
|
|
|
"file,http,https,tcp,tls,pipe,data,crypto",
|
|
|
|
],
|
2023-01-22 04:45:57 +00:00
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
|
|
|
|
textChannel.createMessage({
|
|
|
|
embeds: [
|
|
|
|
{
|
2022-04-19 04:52:48 +00:00
|
|
|
title: `:musical_note: Now Playing`,
|
2022-04-20 20:51:56 +00:00
|
|
|
color: 0x0088cc,
|
2022-04-19 04:49:04 +00:00
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: "Title",
|
2023-11-03 18:00:48 +00:00
|
|
|
value: (title && title != url
|
|
|
|
? `[${title}](${url})`
|
|
|
|
: url
|
|
|
|
).substring(0, 1024),
|
2022-04-19 04:49:04 +00:00
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Length",
|
|
|
|
value: stream
|
|
|
|
? "<continuous>"
|
|
|
|
: length
|
|
|
|
? formatTime(length)
|
|
|
|
: "<unknown>",
|
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Added by",
|
|
|
|
value: `<@${addedBy}>`,
|
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
thumbnail: {
|
|
|
|
url: thumbnail,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
2022-12-10 22:08:58 +00:00
|
|
|
connection.nowplaying = {
|
2022-04-19 04:49:04 +00:00
|
|
|
title,
|
|
|
|
addedBy,
|
|
|
|
thumbnail,
|
|
|
|
length,
|
|
|
|
start: Date.now(),
|
|
|
|
stream,
|
2022-04-20 21:01:34 +00:00
|
|
|
url,
|
2022-04-19 04:49:04 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function youtubeSearch(msg, str) {
|
2022-04-19 05:16:09 +00:00
|
|
|
const {items} = await fetch(
|
2023-09-13 04:24:17 +00:00
|
|
|
`${hf.config.piped_api}/search?q=${encodeURIComponent(str)}&filter=videos`
|
|
|
|
).then((x) => x.json());
|
2022-04-19 04:49:04 +00:00
|
|
|
|
|
|
|
const selection = items.map((item) => ({
|
2023-09-13 04:34:46 +00:00
|
|
|
value: "https://youtube.com" + item.url,
|
2023-09-13 04:24:17 +00:00
|
|
|
key: item.url.replace("/watch?v=", ""),
|
|
|
|
display: `${parseHtmlEntities(item.title).substring(0, 99)}${
|
|
|
|
parseHtmlEntities(item.title).length > 99 ? "…" : ""
|
2022-04-24 18:16:25 +00:00
|
|
|
}`,
|
2023-09-13 04:24:17 +00:00
|
|
|
description: `from ${parseHtmlEntities(item.uploaderName).substring(
|
2023-04-08 20:12:52 +00:00
|
|
|
0,
|
|
|
|
95
|
2023-09-13 04:24:17 +00:00
|
|
|
)}${parseHtmlEntities(item.uploaderName).length > 95 ? "…" : ""}`,
|
2022-04-19 04:49:04 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
try {
|
|
|
|
return await selectionMessage(msg, "Search results:", selection);
|
|
|
|
} catch (out) {
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-20 20:51:56 +00:00
|
|
|
const NOWPLAYING_BAR_LENGTH = 30;
|
|
|
|
|
2022-04-19 04:49:04 +00:00
|
|
|
const command = new Command("music");
|
|
|
|
command.addAlias("m");
|
|
|
|
command.category = "misc";
|
|
|
|
command.helpText = "Music";
|
2022-04-24 17:16:00 +00:00
|
|
|
command.usage = "help";
|
2022-12-10 21:49:25 +00:00
|
|
|
command.callback = async function (
|
|
|
|
msg,
|
|
|
|
line,
|
2022-12-10 22:20:19 +00:00
|
|
|
args,
|
2022-12-10 21:49:25 +00:00
|
|
|
{shuffle = false, limit = -1, offset = 0, next = false}
|
|
|
|
) {
|
2022-04-19 04:49:04 +00:00
|
|
|
if (!msg.guildID) return "This command can only be used in guilds.";
|
|
|
|
|
2022-12-10 22:20:19 +00:00
|
|
|
const subcommand = args.shift();
|
2022-04-19 04:49:04 +00:00
|
|
|
let argStr = args.join(" ");
|
|
|
|
|
|
|
|
switch (subcommand) {
|
|
|
|
case "play":
|
|
|
|
case "p":
|
|
|
|
if (msg.member?.voiceState?.channelID) {
|
|
|
|
if (voiceStorage.has(msg.guildID)) {
|
2022-04-24 18:56:53 +00:00
|
|
|
const connection = voiceStorage.get(msg.guildID);
|
2022-12-10 22:08:58 +00:00
|
|
|
if (connection.voice_id != msg.member.voiceState.channelID) {
|
2022-04-19 04:49:04 +00:00
|
|
|
return "You are in a different voice channel than the bot.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let type;
|
|
|
|
let playlist = false;
|
|
|
|
|
|
|
|
if (
|
|
|
|
REGEX_YOUTUBE_PLAYLIST.test(argStr) ||
|
|
|
|
REGEX_YOUTUBE_PLAYLIST_SHORT.test(argStr)
|
|
|
|
) {
|
|
|
|
type = "yt";
|
|
|
|
playlist = true;
|
|
|
|
} else if (REGEX_SOUNDCLOUD_PLAYLIST.test(argStr)) {
|
|
|
|
type = "sc";
|
|
|
|
playlist = true;
|
|
|
|
} else if (REGEX_YOUTUBE.test(argStr)) {
|
|
|
|
type = "yt";
|
|
|
|
} else if (REGEX_SOUNDCLOUD.test(argStr)) {
|
|
|
|
type = "sc";
|
|
|
|
} else if (REGEX_FILE.test(argStr)) {
|
|
|
|
type = "file";
|
2023-04-24 17:10:24 +00:00
|
|
|
} else if (msg.attachments.size > 0) {
|
|
|
|
const entries = [...msg.attachments.values()].filter((attachment) =>
|
2023-11-03 18:00:48 +00:00
|
|
|
attachment.contentType.startsWith("audio/")
|
2022-04-19 04:56:30 +00:00
|
|
|
);
|
|
|
|
if (entries.length > 0) {
|
|
|
|
type = "file";
|
|
|
|
argStr = entries[0].url;
|
|
|
|
}
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
|
|
|
|
if (type != null) {
|
|
|
|
if (playlist) {
|
2022-04-22 02:05:36 +00:00
|
|
|
const statusMessage = await msg.channel.createMessage({
|
2022-04-22 01:59:22 +00:00
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title:
|
|
|
|
"<a:loading:493087964918972426> Processing playlist...",
|
|
|
|
description: `Fetching tracks...`,
|
|
|
|
color: 0xcc0088,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
2022-04-24 17:16:00 +00:00
|
|
|
const playlist = await processPlaylist(
|
|
|
|
argStr,
|
|
|
|
type,
|
|
|
|
shuffle,
|
2022-04-24 18:04:40 +00:00
|
|
|
limit,
|
|
|
|
offset
|
2022-04-24 17:16:00 +00:00
|
|
|
);
|
2022-04-22 01:59:22 +00:00
|
|
|
await statusMessage.edit({
|
2022-04-19 04:49:04 +00:00
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title:
|
|
|
|
"<a:loading:493087964918972426> Processing playlist...",
|
|
|
|
description: `${playlist.length} tracks`,
|
2022-04-20 20:51:56 +00:00
|
|
|
color: 0xcc0088,
|
2022-04-19 04:49:04 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
for (const track of playlist) {
|
|
|
|
let url;
|
|
|
|
if (type == "yt") {
|
2023-09-13 04:39:11 +00:00
|
|
|
url = "https://youtube.com" + track.url;
|
2022-04-19 04:49:04 +00:00
|
|
|
} else if (type == "sc") {
|
|
|
|
url = track.track
|
|
|
|
? track.track.permalink_url
|
|
|
|
: track.permalink_url;
|
|
|
|
}
|
|
|
|
|
2022-08-03 04:00:10 +00:00
|
|
|
await enqueue({
|
|
|
|
guild_id: msg.guildID,
|
|
|
|
voice_id: msg.member.voiceState.channelID,
|
|
|
|
text_id: msg.channel.id,
|
2022-04-19 04:49:04 +00:00
|
|
|
url,
|
|
|
|
type,
|
2022-08-03 04:00:10 +00:00
|
|
|
addedBy: msg.author.id,
|
2022-12-10 23:03:07 +00:00
|
|
|
suppress: true,
|
2022-08-03 04:00:10 +00:00
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
await statusMessage.edit({
|
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title: "<:ms_tick:503341995348066313> Done processing",
|
|
|
|
description: `${playlist.length} tracks`,
|
2022-04-20 20:51:56 +00:00
|
|
|
color: 0xcc0088,
|
2022-04-19 04:49:04 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} else {
|
2022-08-03 04:00:10 +00:00
|
|
|
await enqueue({
|
|
|
|
guild_id: msg.guildID,
|
|
|
|
voice_id: msg.member.voiceState.channelID,
|
|
|
|
text_id: msg.channel.id,
|
|
|
|
url: argStr,
|
2022-04-19 04:49:04 +00:00
|
|
|
type,
|
2022-08-03 04:00:10 +00:00
|
|
|
addedBy: msg.author.id,
|
2022-12-10 21:49:25 +00:00
|
|
|
queueNext: next,
|
2022-08-03 04:00:10 +00:00
|
|
|
});
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-12-10 22:20:19 +00:00
|
|
|
if (argStr.match(/^https?:\/\//)) {
|
2023-12-26 01:03:07 +00:00
|
|
|
let contentType = await fetch(argStr, {method: "HEAD"}).then(
|
2023-11-03 18:00:48 +00:00
|
|
|
(res) => res.headers.get("Content-Type")
|
2022-04-19 04:49:04 +00:00
|
|
|
);
|
2023-12-26 01:03:07 +00:00
|
|
|
|
|
|
|
if (!contentType) {
|
|
|
|
contentType = await fetch(argStr, {method: "GET"}).then((res) =>
|
|
|
|
res.headers.get("Content-Type")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-05-11 18:14:48 +00:00
|
|
|
if (
|
|
|
|
contentType.startsWith("audio/") ||
|
|
|
|
contentType.startsWith("video/")
|
|
|
|
) {
|
2022-08-03 04:00:10 +00:00
|
|
|
await enqueue({
|
|
|
|
guild_id: msg.guildID,
|
|
|
|
voice_id: msg.member.voiceState.channelID,
|
|
|
|
text_id: msg.channel.id,
|
|
|
|
url: argStr,
|
|
|
|
type: "file",
|
|
|
|
addedBy: msg.author.id,
|
2022-12-10 21:49:25 +00:00
|
|
|
queueNext: next,
|
2022-08-03 04:00:10 +00:00
|
|
|
});
|
2022-05-11 18:14:48 +00:00
|
|
|
} else {
|
|
|
|
return "Unsupported content type.";
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
} else {
|
2022-05-11 18:14:48 +00:00
|
|
|
const url = await youtubeSearch(msg, argStr);
|
2023-09-13 04:39:11 +00:00
|
|
|
if (url?.startsWith("https://youtube.com/")) {
|
2022-08-03 04:00:10 +00:00
|
|
|
await enqueue({
|
|
|
|
guild_id: msg.guildID,
|
|
|
|
voice_id: msg.member.voiceState.channelID,
|
|
|
|
text_id: msg.channel.id,
|
2022-05-11 18:14:48 +00:00
|
|
|
url,
|
2022-08-03 04:00:10 +00:00
|
|
|
type: "yt",
|
|
|
|
addedBy: msg.author.id,
|
2022-12-10 21:49:25 +00:00
|
|
|
queueNext: next,
|
2022-08-03 04:00:10 +00:00
|
|
|
});
|
2022-05-11 18:14:48 +00:00
|
|
|
} else {
|
|
|
|
return url;
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return "You are not in a voice channel.";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "skip":
|
|
|
|
case "s":
|
|
|
|
if (msg.member?.voiceState?.channelID) {
|
|
|
|
const connection = voiceStorage.get(msg.guildID);
|
|
|
|
if (voiceStorage.has(msg.guildID)) {
|
2022-12-10 22:08:58 +00:00
|
|
|
if (connection.voice_id != msg.member.voiceState.channelID) {
|
2022-04-19 04:49:04 +00:00
|
|
|
return "You are in a different voice channel than the bot.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: skip lock checks
|
2023-01-24 01:07:17 +00:00
|
|
|
await connection.connection.stopPlaying();
|
2022-04-19 04:49:04 +00:00
|
|
|
return {reaction: "\u23ed"};
|
|
|
|
} else {
|
|
|
|
return "You are not in a voice channel.";
|
|
|
|
}
|
|
|
|
case "leave":
|
|
|
|
case "l":
|
|
|
|
case "stop":
|
|
|
|
if (msg.member?.voiceState?.channelID) {
|
|
|
|
const connection = voiceStorage.get(msg.guildID);
|
|
|
|
if (voiceStorage.has(msg.guildID)) {
|
2022-12-10 22:08:58 +00:00
|
|
|
if (connection.voice_id != msg.member.voiceState.channelID) {
|
2022-04-19 04:49:04 +00:00
|
|
|
return "You are in a different voice channel than the bot.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: skip lock checks
|
2022-12-10 21:53:53 +00:00
|
|
|
connection.queue = [];
|
|
|
|
connection.__leave = true;
|
2023-01-24 01:07:17 +00:00
|
|
|
await connection.connection.stopPlaying();
|
2022-12-10 22:59:34 +00:00
|
|
|
connection.onEnd();
|
2022-04-19 05:10:13 +00:00
|
|
|
await hf.bot.leaveVoiceChannel(msg.member.voiceState.channelID);
|
2022-04-19 04:49:04 +00:00
|
|
|
return {reaction: "\uD83D\uDC4B"};
|
|
|
|
} else {
|
|
|
|
return "You are not in a voice channel.";
|
|
|
|
}
|
2022-04-20 20:51:56 +00:00
|
|
|
case "np": {
|
|
|
|
if (!voiceStorage.has(msg.guildID))
|
|
|
|
return "The bot is not in a voice channel.";
|
|
|
|
|
|
|
|
const connection = voiceStorage.get(msg.guildID);
|
2022-12-10 22:08:58 +00:00
|
|
|
const nowPlaying = connection.nowplaying;
|
2023-01-22 04:45:57 +00:00
|
|
|
if (!nowPlaying || !connection.connection.playing)
|
2022-04-20 20:51:56 +00:00
|
|
|
return "Nothing is currently playing.";
|
|
|
|
|
|
|
|
const position = Date.now() - nowPlaying.start;
|
|
|
|
|
2023-01-22 04:45:57 +00:00
|
|
|
const timeEnd =
|
|
|
|
nowPlaying.length == 0 ? "\u221e" : formatTime(nowPlaying.length);
|
2022-04-20 20:51:56 +00:00
|
|
|
const timePos = formatTime(position);
|
|
|
|
|
2023-01-22 04:45:57 +00:00
|
|
|
const progress =
|
|
|
|
nowPlaying.length == 0 ? 1 : position / nowPlaying.length;
|
2022-04-20 20:51:56 +00:00
|
|
|
const barLength = Math.round(progress * NOWPLAYING_BAR_LENGTH);
|
|
|
|
|
|
|
|
const bar = `\`[${"=".repeat(barLength)}${" ".repeat(
|
|
|
|
NOWPLAYING_BAR_LENGTH - barLength
|
|
|
|
)}]\``;
|
|
|
|
const time = `\`${timePos}${" ".repeat(
|
|
|
|
NOWPLAYING_BAR_LENGTH + 2 - timePos.length - timeEnd.length
|
|
|
|
)}${timeEnd}\``;
|
|
|
|
|
|
|
|
return {
|
|
|
|
embed: {
|
|
|
|
title: ":musical_note: Now Playing",
|
|
|
|
color: 0x0088cc,
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: "Title",
|
2023-11-03 18:00:48 +00:00
|
|
|
value: (nowPlaying.title && nowPlaying.title != nowPlaying.url
|
2022-04-20 20:51:56 +00:00
|
|
|
? `[${nowPlaying.title}](${nowPlaying.url})`
|
2023-11-03 18:00:48 +00:00
|
|
|
: nowPlaying.url
|
|
|
|
).substring(0, 1024),
|
2022-04-20 20:51:56 +00:00
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Added by",
|
|
|
|
value: `<@${nowPlaying.addedBy}>`,
|
|
|
|
inline: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: bar,
|
|
|
|
value: time,
|
|
|
|
inline: false,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
case "queue":
|
2022-04-24 17:34:54 +00:00
|
|
|
case "q": {
|
|
|
|
if (!voiceStorage.has(msg.guildID))
|
|
|
|
return "The bot is not in a voice channel";
|
|
|
|
|
|
|
|
const connection = voiceStorage.get(msg.guildID);
|
2022-12-10 22:08:58 +00:00
|
|
|
const queue = connection.queue;
|
2022-04-24 17:34:54 +00:00
|
|
|
if (queue.length === 0) return "Nothing else is currently queued";
|
|
|
|
|
2022-12-10 22:08:58 +00:00
|
|
|
const nowPlaying = connection.nowplaying;
|
2022-04-24 17:34:54 +00:00
|
|
|
|
|
|
|
const now = Date.now();
|
2022-04-24 17:41:19 +00:00
|
|
|
let nextTrack = now + (nowPlaying.length - (now - nowPlaying.start));
|
2022-04-24 17:34:54 +00:00
|
|
|
const fields = [];
|
2022-04-24 17:43:21 +00:00
|
|
|
for (const index in queue.slice(0, 9)) {
|
2022-04-24 17:34:54 +00:00
|
|
|
const item = queue[index];
|
|
|
|
fields.push({
|
2022-04-24 17:40:15 +00:00
|
|
|
name: item.title ?? item.url,
|
|
|
|
value: `${item.title ? `[Link](${item.url}) - ` : ""}${formatTime(
|
|
|
|
item.length
|
2022-04-24 17:41:19 +00:00
|
|
|
)}\nAdded by: <@${item.addedBy}>\n<t:${Math.floor(
|
|
|
|
nextTrack / 1000
|
|
|
|
)}:R>`,
|
2022-04-24 17:34:54 +00:00
|
|
|
inline: true,
|
|
|
|
});
|
2022-04-24 17:41:19 +00:00
|
|
|
nextTrack += item.length;
|
2022-04-24 17:34:54 +00:00
|
|
|
}
|
|
|
|
|
2022-05-04 21:37:59 +00:00
|
|
|
let totalLength = 0;
|
|
|
|
for (const item of queue) {
|
|
|
|
totalLength += item.length;
|
|
|
|
}
|
|
|
|
|
2022-04-24 17:34:54 +00:00
|
|
|
return {
|
|
|
|
embed: {
|
2022-04-24 18:56:53 +00:00
|
|
|
title: ":inbox_tray: Currently Queued",
|
2022-04-24 17:34:54 +00:00
|
|
|
color: 0x0088cc,
|
|
|
|
fields,
|
|
|
|
footer: {
|
2022-05-04 21:37:59 +00:00
|
|
|
text:
|
|
|
|
(queue.length > 9 ? `Showing 9/${queue.length} items | ` : "") +
|
|
|
|
`Total length: ${formatTime(totalLength)}`,
|
2022-04-24 17:34:54 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
case "remove":
|
|
|
|
case "qr":
|
2022-04-24 18:56:53 +00:00
|
|
|
if (msg.member?.voiceState?.channelID) {
|
|
|
|
const connection = voiceStorage.get(msg.guildID);
|
|
|
|
if (voiceStorage.has(msg.guildID)) {
|
2022-12-10 22:08:58 +00:00
|
|
|
if (connection.voice_id != msg.member.voiceState.channelID) {
|
2022-04-24 18:56:53 +00:00
|
|
|
return "You are in a different voice channel than the bot.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-10 22:08:58 +00:00
|
|
|
let queue = connection.queue;
|
2022-04-24 18:56:53 +00:00
|
|
|
if (queue.length === 0) return "Nothing else is currently queued";
|
|
|
|
|
|
|
|
const hasManageMessages = msg.member.permissions.has("manageMessages");
|
|
|
|
if (!hasManageMessages)
|
|
|
|
queue = queue.filter((item) => item.addedBy == msg.member.id);
|
|
|
|
|
|
|
|
if (queue.length === 0) return "You currently have nothing queued";
|
|
|
|
|
|
|
|
const toRemove = await selectionMessage(
|
|
|
|
msg,
|
|
|
|
"Choose items to remove",
|
|
|
|
queue.slice(0, 25).map((item) => {
|
|
|
|
const user = hf.bot.users.get(item.addedBy);
|
|
|
|
return {
|
2022-04-24 19:09:00 +00:00
|
|
|
key: item.id,
|
2022-04-24 18:56:53 +00:00
|
|
|
display: (item.title ?? item.url).substr(0, 100),
|
2023-01-22 05:09:36 +00:00
|
|
|
description: hasManageMessages
|
2023-10-05 19:41:47 +00:00
|
|
|
? `Added by: ${formatUsername(user)}`
|
2023-01-22 05:09:36 +00:00
|
|
|
: "",
|
2022-04-24 18:56:53 +00:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
30000,
|
2022-04-24 19:00:14 +00:00
|
|
|
Math.min(queue.length, 25)
|
2022-04-24 18:56:53 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (Array.isArray(toRemove)) {
|
2022-12-10 22:08:58 +00:00
|
|
|
connection.queue = connection.queue.filter(
|
2022-04-24 19:16:01 +00:00
|
|
|
(item) => !toRemove.includes(item.id)
|
|
|
|
);
|
2022-04-24 18:56:53 +00:00
|
|
|
return `Removed ${toRemove.length} item(s).`;
|
|
|
|
} else {
|
|
|
|
return toRemove;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return "You are not in a voice channel";
|
|
|
|
}
|
2022-04-19 04:49:04 +00:00
|
|
|
case "lock":
|
|
|
|
case "unlock":
|
|
|
|
return "TODO";
|
|
|
|
case "help":
|
|
|
|
case "h":
|
|
|
|
return `**__Music Subcommands__**
|
|
|
|
\u2022 \`play/p [url|search string]\` - Play or add to queue. Supports: YouTube, SoundCloud, mp3/flac/ogg/wav/webm/mp4/mov/mkv/mod/s3m/it/xm, streams
|
|
|
|
\u2022 \`queue/q (page)\` - Lists the current queue.
|
|
|
|
\u2022 \`leave/l/stop\` - Leaves the voice channel.
|
|
|
|
\u2022 \`np\` - Shows whats currently playing.
|
|
|
|
\u2022 \`skip/s\` - Skips whats currently playing.
|
|
|
|
\u2022 \`remove/qr\` - Remove an item from queue.
|
|
|
|
\u2022 \`lock\` - Lock skipping and queue. (manage messages)
|
|
|
|
\u2022 \`unlock\` - Unlock skipping and queue. (manage messages)
|
|
|
|
\u2022 \`help/h\` - This text.`;
|
|
|
|
default:
|
|
|
|
return `Invalid/missing subcommand. See \`${hf.config.prefix}music help\` for all subcommands.`;
|
|
|
|
}
|
|
|
|
return null; // shutting up eslint
|
|
|
|
};
|
|
|
|
hf.registerCommand(command);
|