6 Commits
57 changed files with 1317 additions and 1068 deletions

No files matched your search

+1
View File
@@ -14,6 +14,7 @@
"plugins": ["@typescript-eslint", "import", "simple-import-sort"], "plugins": ["@typescript-eslint", "import", "simple-import-sort"],
"ignorePatterns": ["**/node_modules/**", "/dist/**", "/tests/**"], "ignorePatterns": ["**/node_modules/**", "/dist/**", "/tests/**"],
"rules": { "rules": {
"curly": ["error", "all"],
"simple-import-sort/imports": "error", "simple-import-sort/imports": "error",
"simple-import-sort/exports": "error" "simple-import-sort/exports": "error"
} }
+3
View File
@@ -16,6 +16,9 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Binaries
bin/
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release build/Release
+1 -4
View File
@@ -12,10 +12,7 @@ RUN mkdir bin
RUN curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/src/app/bin/youtube-dl RUN curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/src/app/bin/youtube-dl
RUN chmod a+rx /usr/src/app/bin/youtube-dl RUN chmod a+rx /usr/src/app/bin/youtube-dl
COPY package*.json ./ COPY . .
RUN npm ci RUN npm ci
COPY . .
RUN npm run build
CMD [ "npm", "start" ] CMD [ "npm", "start" ]
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "quetza-bot", "name": "quetza-bot",
"version": "2302.1.0", "version": "2302.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "quetza-bot", "name": "quetza-bot",
"version": "2302.1.0", "version": "2302.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.8.0", "@discordjs/opus": "^0.8.0",
+10 -10
View File
@@ -5,7 +5,7 @@
"npm": "*" "npm": "*"
}, },
"type": "module", "type": "module",
"version": "2302.1", "version": "2303.1",
"description": "Discord multifunctional bot", "description": "Discord multifunctional bot",
"author": { "author": {
"name": "unknowableshade", "name": "unknowableshade",
@@ -26,25 +26,25 @@
"start": "node dist/index.js", "start": "node dist/index.js",
"build": "npx tsc --build", "build": "npx tsc --build",
"clean": "npx tsc --build --clean", "clean": "npx tsc --build --clean",
"lint": "npx prettier --write . && npx eslint --fix .", "lint": "npx eslint --fix . && npx prettier --write .",
"prestart": "npm run build" "postinstall": "npm run build"
}, },
"dependencies": { "dependencies": {
"discord.js": "^14.3.0",
"@discordjs/opus": "^0.8.0", "@discordjs/opus": "^0.8.0",
"@discordjs/voice": "^0.11.0", "@discordjs/voice": "^0.11.0",
"ytdl-core": "^4.11.1",
"discord-ytdl-core": "^5.0.4", "discord-ytdl-core": "^5.0.4",
"tweetnacl": "^1.0.3" "discord.js": "^14.3.0",
"tweetnacl": "^1.0.3",
"ytdl-core": "^4.11.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.8.2",
"@types/node": "^18.7.13", "@types/node": "^18.7.13",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.6.0",
"@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1", "@typescript-eslint/parser": "^5.35.1",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-simple-import-sort": "^10.0.0" "eslint-plugin-simple-import-sort": "^10.0.0",
"typescript": "^4.8.2"
} }
} }
-22
View File
@@ -1,22 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
const amount = player.Queue.Tracks.length;
player.clear();
await ctx.reply({ embeds: [I18n.en.cleared(amount)] });
}
const data = new SlashCommandBuilder().setName("clear").setDescription("Clear the player's queue.");
export { data };
-38
View File
@@ -1,38 +0,0 @@
import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel || !ctx.member) return;
await ctx.deferReply();
const player =
client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel) ||
client.modules.music.set(ctx.guild, ctx.channel as TextChannel);
if (!player.Connection) {
const channel = (ctx.member as GuildMember).voice.channel;
if (channel) {
player.connect(channel);
await ctx.editReply({ embeds: [I18n.en.okConnected(channel.name)] });
} else {
client.modules.music.del(ctx.guild.id);
await ctx.editReply({ embeds: [I18n.en.notConnected()] });
return undefined;
}
} else {
await ctx.editReply({ embeds: [I18n.en.wasConnected()] });
}
return player;
}
const data = new SlashCommandBuilder()
.setName("connect")
.setDescription("Connect me to a voice channel.");
export { data };
-20
View File
@@ -1,20 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel || !ctx.member) return;
await ctx.deferReply();
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
await ctx.editReply({ embeds: [I18n.en.playerInfo(player)] });
}
const data = new SlashCommandBuilder()
.setName("info")
.setDescription("Information about the playback.");
export { data };
-33
View File
@@ -1,33 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
const pos = ctx.options.get("position")?.value as number;
const query = ctx.options.get("query")?.value as string;
if (!ctx.guild || !query || !pos || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
await ctx.deferReply();
const track = await player.add(query, ctx.user, pos - 1);
await ctx.editReply({ embeds: [I18n.en.appended(track)] });
}
const data = new SlashCommandBuilder()
.setName("insert")
.setDescription("Insert a new track to specific position in the queue.")
.addStringOption((option) =>
option.setName("query").setDescription("Title or URL of the song.").setRequired(true)
)
.addIntegerOption((option) =>
option.setName("position").setDescription("Position to insert.").setRequired(true)
);
export { data };
-20
View File
@@ -1,20 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
player.skip();
await ctx.reply({ embeds: [I18n.en.skipped()] });
}
const data = new SlashCommandBuilder().setName("next").setDescription("Skip current song.");
export { data };
-20
View File
@@ -1,20 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
const state = player.pause();
await ctx.reply({ embeds: [I18n.en.paused(state)] });
}
const data = new SlashCommandBuilder().setName("pause").setDescription("Toggle pause on player.");
export { data };
-32
View File
@@ -1,32 +0,0 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
import { run as connect } from "./connect.js";
export async function run(client: Client, ctx: CommandInteraction) {
const query = ctx.options.get("query")?.value as string;
const player = await connect(client, ctx);
if (!player) return;
if (query) {
const track = await player.add(query, ctx.user);
await ctx.editReply({ embeds: [I18n.en.appended(track)] });
}
if (!player.Resource) {
player.play();
}
}
const data = new SlashCommandBuilder()
.setName("play")
.setDescription("Launch player and add songs to the queue.")
.addStringOption((option) =>
option.setName("query").setDescription("Title or URL of the song.")
);
export { data };
-20
View File
@@ -1,20 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
player.shuffle();
await ctx.reply({ embeds: [I18n.en.reshuffle()] });
}
const data = new SlashCommandBuilder().setName("reshuffle").setDescription("Shuffle the queue.");
export { data };
-33
View File
@@ -1,33 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
const hours = (ctx.options.get("hrs")?.value as number) || 0;
const minutes = (ctx.options.get("mins")?.value as number) || 0;
const seconds = (ctx.options.get("secs")?.value as number) || 0;
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
const time = hours * 3600 + minutes * 60 + seconds;
player.seek(time * 1000);
await ctx.reply({
embeds: [I18n.en.fastForwarded(time)]
});
}
const data = new SlashCommandBuilder()
.setName("seek")
.setDescription("Fast-forward by the time.")
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."));
export { data };
-20
View File
@@ -1,20 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
player.destroy();
await ctx.reply({ embeds: [I18n.en.destroyed()] });
}
const data = new SlashCommandBuilder().setName("stop").setDescription("Destroy the player.");
export { data };
-27
View File
@@ -1,27 +0,0 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) {
const volume = (ctx.options.get("volume")?.value as number) || 100;
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel);
if (!player) return;
player.Volume = volume;
await ctx.reply({ embeds: [I18n.en.volumeSet(player.Volume)] });
}
const data = new SlashCommandBuilder()
.setName("volume")
.setDescription("Change player's volume amount.")
.addIntegerOption((option) =>
option.setName("volume").setDescription("Volume in percents.").setRequired(true)
);
export { data };
+4 -7
View File
@@ -1,15 +1,12 @@
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.dirname(fileURLToPath(import.meta.url));
const config = { const config = {
token: discordToken: process.env.DISCORD_TOKEN,
process.env.DISCORD_TOKEN ?? modulesDir: path.join(ROOT, "/modules/"),
"NzgzNDQwMjU3NjE3MzYyOTQ1.GQ0lLK.VftrGanpOtqfoKUQRkoXDCcbQqwMqBsybmwjGE", binariesDir: path.join(ROOT, "/../bin/")
commands: path.join(__dirname, "/commands/"),
events: path.join(__dirname, "/events/"),
binaries: path.join(__dirname, "/../bin/")
}; };
export default config; export default config;
-15
View File
@@ -1,15 +0,0 @@
import { Interaction } from "discord.js";
import Client from "../lib/client.js";
export async function run(client: Client, args: Interaction[]): Promise<void> {
const [interaction] = args;
if (interaction.isCommand()) {
const command = client.Commands.get(interaction.commandName);
if (command) command.run(client, interaction);
}
}
export const name = "interactionCreate";
-23
View File
@@ -1,23 +0,0 @@
import { VoiceState } from "discord.js";
import Client from "../lib/client.js";
import I18n from "../lib/i18n.js";
import { asleep } from "../lib/misc.js";
export async function run(client: Client, args: VoiceState[]): Promise<void> {
const [oldState] = args;
const player = client.modules.music.get(oldState.guild.id);
if (!player || !oldState.channel || !client.user) return;
if (oldState.channel.members.filter((value) => value.user !== client.user).size === 0) {
await asleep(30000);
if (oldState.channel.members.filter((value) => value.user !== client.user).size === 0) {
await player.Channel.send({ embeds: [I18n.en.fullyAlone()] });
player.destroy();
}
}
}
export const name = "voiceStateUpdate";
+6 -2
View File
@@ -1,6 +1,10 @@
import { GatewayIntentBits } from "discord.js";
import config from "./config.js"; import config from "./config.js";
import Client from "./lib/client.js"; import Client from "./lib/client.js";
const client = new Client(); const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates]
});
client.login(config.token); client.login(config.discordToken);
+67 -30
View File
@@ -1,48 +1,85 @@
import { Client as DiscordClient, GatewayIntentBits } from "discord.js"; import { Client as DiscordClient, ClientOptions, Collection } from "discord.js";
import { existsSync, readdirSync } from "fs"; import { existsSync, lstatSync, readdirSync } from "fs";
import path from "path"; import path from "path";
import { pathToFileURL } from "url"; import { pathToFileURL } from "url";
import config from "../config.js"; import config from "../config.js";
import Music from "./music.js"; import { Command, Event, Module, ModuleMetadata } from "./types.js";
import { command, event } from "./types.js";
const MODULE_ENTRY = "module.js";
const MODULE_COMMANDS = "commands";
const MODULE_EVENTS = "events";
export default class Client extends DiscordClient { export default class Client extends DiscordClient {
private commands = new Map<string, command>(); public readonly commands = new Collection<string, Command>();
get Commands(): Map<string, command> { public readonly modules = new Collection<string, ModuleMetadata>();
return this.commands;
public constructor(options: ClientOptions) {
super(options);
for (const module of readdirSync(config.modulesDir)) {
const modulePath = path.join(config.modulesDir, module);
if (!lstatSync(modulePath).isDirectory()) {
continue;
} }
public readonly modules = { import(pathToFileURL(path.join(modulePath, MODULE_ENTRY)).toString()).then(
music: new Music() (data: Module) => this.loadModule(data)
}; );
}
}
public constructor() { private loadModule(data: Module): void {
super({ const commands: string[] = [];
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] const events: string[] = [];
function importModule(dir: string, importThen: (data: Command & Event) => void) {
if (existsSync(dir)) {
for (const source of readdirSync(dir)) {
const file = path.join(dir, source);
if (!lstatSync(file).isFile() || !file.endsWith(".js")) {
continue;
}
import(pathToFileURL(file).toString()).then(importThen);
}
}
}
importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => {
this.commands.set(command.data.name, command)
commands.push(command.data.name);
}); });
if (existsSync(config.commands)) { importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
const files = readdirSync(config.commands).filter((file) => file.endsWith(".js")); this.on(event.name, (...eventee: unknown[]) => event.execute(this, eventee));
events.push(event.name);
});
for (const file of files) { this.modules.set(data.name, {
import(pathToFileURL(path.join(config.commands, file)).toString()).then( ...data,
(cmd: command) => this.commands.set(cmd.data.name, cmd) commands,
); events
} })
} }
if (existsSync(config.events)) { public generateApplicationStatus(): string {
const files = readdirSync(config.events).filter((file) => file.endsWith(".js")); const modulesStatus = this.modules.map((module) => {
const tag = `✓ ${module.name}@${module.tag} module is loaded.`
const commands = module.commands.map((name) => " - " + name).join("\n");
const events = module.events.map((name) => " - " + name).join("\n");
for (const file of files) { return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n");
import(pathToFileURL(path.join(config.events, file)).toString()).then( }).join("\n" + "-".repeat(50) + "\n");
(evnt: event) => {
this.on(evnt.name, (...args: unknown[]) => evnt.run(this, args)); const clientStatus =
} this.user && this.application
); ? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
} : "✖ Application is not availiable.";
}
return ["-".repeat(50), modulesStatus, "-".repeat(50), clientStatus].join("\n");
} }
} }
-69
View File
@@ -1,69 +0,0 @@
export default class Filter {
private filters: Record<string, [boolean, string]> = {
bassboost_low: [false, "bass=g=15:f=110:w=0.3"],
bassboost: [false, "bass=g=20:f=110:w=0.3"],
bassboost_high: [false, "bass=g=30:f=110:w=0.3"],
"8D": [false, "apulsator=hz=0.09"],
vaporwave: [false, "aresample=48000,asetrate=48000*0.75"],
nightcore: [false, "aresample=48000,asetrate=48000*1.25"],
phaser: [false, "aphaser=in_gain=0.4"],
tremolo: [false, "tremolo"],
vibrato: [false, "vibrato=f=6.5"],
reverse: [false, "areverse"],
treble: [false, "treble=g=5"],
normalizer: [false, "dynaudnorm=g=101"],
normalizer2: [false, "acompressor"],
surrounding: [false, "surround"],
pulsator: [false, "apulsator=hz=1"],
subboost: [false, "asubboost"],
karaoke: [false, "stereotools=mlev=0.03"],
flanger: [false, "flanger"],
gate: [false, "agate"],
haas: [false, "haas"],
mcompand: [false, "mcompand"],
mono: [false, "pan=mono|c0=.5*c0+.5*c1"],
mstlr: [false, "stereotools=mode=ms>lr"],
mstrr: [false, "stereotools=mode=ms>rr"],
compressor: [false, "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6"],
expander: [
false,
"compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3"
],
softlimiter: [false, "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8"],
chorus: [false, "chorus=0.7:0.9:55:0.4:0.25:2"],
chorus2d: [false, "chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3"],
chorus3d: [false, "chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3"],
fadein: [false, "afade=t=in:ss=0:d=10"],
dim: [
false,
"afftfilt=\"'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'\""
],
earrape: [false, "channelsplit,sidechaingate=level_in=64"]
};
public get Filters(): [string, string][] {
return Object.entries(this.filters)
.filter((value) => value[1][0])
.map((value) => [value[0], value[1][1]]);
}
public toggle(filter?: string): void {
if (!filter) {
Object.values(this.filters).forEach((value) => {
value[0] = false;
});
return;
}
if (Object.prototype.hasOwnProperty.call(this.filters, filter))
this.filters[filter][0] = !this.filters[filter][0];
}
public toString(): string {
return this.Filters.map((value) => value[1]).join(",");
}
public empty(): boolean {
return this.Filters.length === 0;
}
}
-112
View File
@@ -1,112 +0,0 @@
import { loopOption, track } from "./types.js";
export default class Queue {
private tracks: track[] = [];
public get Tracks(): track[] {
return this.tracks;
}
private position = 0;
public get Position(): number {
return this.position;
}
private loop: loopOption = "NONE";
public get Loop(): loopOption {
return this.loop;
}
public set Loop(x: loopOption) {
this.loop = x;
}
private duration = 0;
public get Duration(): number {
return this.duration;
}
public next(): track | undefined {
if (this.loop === "SONG") return this.current();
if (this.loop === "AUTO") {
this.position = Math.floor(Math.random() * this.tracks.length);
return this.current();
}
if (this.position + 1 < this.tracks.length) {
this.position++;
return this.current();
} else if (this.loop === "LOOP") {
this.position = 0;
return this.current();
} else if (this.loop === "NONE") {
this.position = 0;
return undefined;
}
}
public shuffle(): void {
let currentIndex = this.tracks.length;
while (currentIndex != 0) {
const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[this.tracks[currentIndex], this.tracks[randomIndex]] = [
this.tracks[randomIndex],
this.tracks[currentIndex]
];
}
this.position = 0;
}
public push(tracks: track[], index?: number): track {
if (!index || index >= this.tracks.length) {
this.tracks = this.tracks.concat(tracks);
} else {
this.tracks.splice(Math.max(0, index), 0, ...tracks);
if (this.position >= index) this.position += tracks.length;
}
for (const track of tracks) {
this.duration += track.duration;
}
return this.tracks[index || this.tracks.length - 1];
}
public pop(query: number): track | undefined {
if (query >= 0 && query < this.tracks.length) {
const track = this.tracks[query];
this.tracks.splice(query, 1);
this.position = Math.max(0, this.position - 1);
this.duration -= track.duration;
return track;
}
return undefined;
}
public jump(query: number): track | undefined {
if (query >= 0 && query < this.tracks.length) {
const track = this.tracks[query];
this.position = query;
return track;
}
return undefined;
}
public current(): track {
return this.tracks[this.position];
}
public empty(): boolean {
return this.tracks.length === 0;
}
public clear(): void {
this.tracks = [];
this.position = 0;
}
}
+20 -266
View File
@@ -1,273 +1,27 @@
import { ApplicationCommandData, CommandInteraction, User } from "discord.js"; import { ApplicationCommandData, CommandInteraction } from "discord.js";
import Client from "./client.js"; import Client from "./client.js";
export type track = { export interface Module {
url: string;
title: string;
thumbnail: string;
duration: number;
requester: User;
};
export type command = {
data: ApplicationCommandData;
run: (arg0: Client, arg1: CommandInteraction) => Promise<void>;
};
export type event = {
name: string; name: string;
run: (arg0: Client, arg1: unknown[]) => Promise<void>; description?: string;
}; author: string;
tag: string;
controller?: unknown;
rootDir: string;
}
export type loopOption = "LOOP" | "SONG" | "NONE" | "AUTO"; export interface ModuleMetadata extends Module {
commands: string[];
events: string[];
}
export type ytdlArgs = { export interface Command {
help?: boolean; data: ApplicationCommandData;
version?: boolean; execute: (client: Client, interaction: CommandInteraction) => Promise<void>;
update?: boolean; }
ignoreErrors?: boolean;
abortOnError?: boolean;
dumpUserAgent?: boolean;
listExtractors?: boolean;
extractorDescriptions?: boolean;
forceGenericExtractor?: boolean;
defaultSearch?: string;
igonreConfig?: boolean;
configLocation?: string;
flatPlaylist?: boolean;
markWatched?: boolean;
noColor?: boolean;
proxy?: string;
socketTimeout?: number;
sourceAddress?: string;
forceIpv4?: boolean;
forceIpv6?: boolean;
geoVerificationProxy?: string;
geoBypass?: boolean;
geoBypassCountry?: string;
geoBypassIpBlock?: string;
playlistStart?: number;
playlistEnd?: number | "last";
playlistItems?: string;
matchTitle?: string;
rejectTitle?: string;
maxDownloads?: number;
minFilesize?: string;
maxFilesize?: string;
date?: string;
datebefore?: string;
dateafter?: string;
minViews?: number;
maxViews?: number;
matchFilter?: string;
noPlaylist?: boolean;
yesPlaylist?: boolean;
ageLimit?: number;
downloadArchive?: string;
includeAds?: boolean;
limitRate?: string;
retries?: number | "infinite";
skipUnavailableFragments?: boolean;
abortOnUnavailableFragment?: boolean;
keepFragments?: boolean;
bufferSize?: string;
noResizeBuffer?: boolean;
httpChunkSize?: string;
playlistReverse?: boolean;
playlistRandom?: boolean;
xattrSetFilesize?: boolean;
hlsPreferNative?: boolean;
hlsPreferFfmpeg?: boolean;
hlsUseMpegts?: boolean;
externalDownloader?: string;
externalDownloaderArgs?: string;
batchFile?: string;
id?: boolean;
output?: string;
outputNaPlaceholder?: string;
autonumberStart?: number;
restrictFilenames?: boolean;
noOverwrites?: boolean;
continue?: boolean;
noPart?: boolean;
noMtime?: boolean;
writeDescription?: boolean;
writeInfoJson?: boolean;
writeAnnotations?: boolean;
loadInfoJson?: string;
cookies?: string;
cacheDir?: string;
noCacheDir?: boolean;
rmCacheDir?: boolean;
writeThumbnail?: boolean;
writeAllThumbnails?: boolean;
listThumbnails?: boolean;
quiet?: boolean;
noWarnings?: boolean;
simulate?: boolean;
skipDownload?: boolean;
getUrl?: boolean;
getTitle?: boolean;
getId?: boolean;
getThumbnail?: boolean;
getDuration?: boolean;
getFilename?: boolean;
getFormat?: boolean;
dumpJson?: boolean;
dumpSingleJson?: boolean;
printJson?: boolean;
newline?: boolean;
noProgress?: boolean;
consoleTitle?: boolean;
verbose?: boolean;
dumpPages?: boolean;
writePages?: boolean;
printTraffic?: boolean;
callHome?: boolean;
encoding?: string;
noCheckCertificate?: boolean;
preferInsecure?: boolean;
userAgent?: string;
referer?: string;
addHeader?: string;
bidiWorkaround?: boolean;
sleepInterval?: number;
maxSleepInterval?: number;
format?: string;
allFormats?: boolean;
preferFreeFormats?: boolean;
listFormats?: boolean;
youtubeSkipDashManifest?: boolean;
mergeOutputFormat?: string;
writeSub?: boolean;
writeAutoSub?: boolean;
allSubs?: boolean;
listSubs?: boolean;
subFormat?: string;
subLang?: string;
username?: string;
password?: string;
twofactor?: string;
netrc?: boolean;
videoPassword?: string;
apMso?: string;
apUsername?: string;
apPassword?: string;
apListMso?: boolean;
extractAudio?: boolean;
audioFormat?: string;
audioQuality?: number;
recodeVideo?: string;
postprocessorArgs?: string;
keepVideo?: boolean;
noPostOverwrites?: boolean;
embedSubs?: boolean;
embedThumbnail?: boolean;
addMetadata?: boolean;
metadataFromFile?: string;
xattrs?: boolean;
fixup?: string;
preferAvconv?: boolean;
preferFfmpeg?: boolean;
ffmpegLocation?: string;
exec?: string;
convertSubs?: string;
};
type ytdlFormat = { export interface Event {
asr: number; name: string;
filesize: number; execute: (client: Client, eventee: unknown[]) => Promise<void>;
format_id: string; }
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: unknown;
container: string;
format: string;
protocol: string;
http_headers: unknown;
};
type ytdlThumbnail = {
height: number;
url: string;
width: number;
resolution: string;
id: string;
};
type ytdlEntry = {
uploader: string;
duration: number;
id: string;
title: string;
url: string;
};
export type ytdlResponse = {
id: string;
title: string;
formats: ytdlFormat[];
thumbnails: ytdlThumbnail[];
description: string;
upload_date: string;
uploader: string;
uploader_id: string;
uploader_url: string;
channel_id: string;
channel_url: string;
duration: number;
view_count: number;
average_rating: number;
age_limit: number;
webpage_url: string;
categories: string[];
tags: string[];
is_live: boolean;
like_count: number;
dislike_count: number;
channel: string;
track: string;
artist: string;
creator: string;
alt_title: string;
extractor: string;
webpage_url_basename: string;
extractor_key: string;
playlist: string;
playlist_index: number;
thumbnail: string;
display_id: string;
requested_subtitles: unknown;
asr: number;
filesize: number;
format_id: string;
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: { http_chunk_size: number };
container: string;
format: string;
protocol: string;
http_headers: unknown;
fulltitle: string;
_filename: string;
entries: ytdlEntry[];
};
+14
View File
@@ -0,0 +1,14 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
async function execute(client: Client, interaction: CommandInteraction): Promise<void> {
await interaction.reply(I18n.replies.modules(client.modules));
}
const data = new SlashCommandBuilder()
.setName("modules")
.setDescription("List of availiable modules.");
export { data, execute };
@@ -1,21 +1,21 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js"; import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js"; import I18n from "../lib/i18n.js";
export async function run(client: Client, ctx: CommandInteraction) { async function execute(client: Client, interaction: CommandInteraction): Promise<void> {
const start = Date.now(); const start = Date.now();
if (!client.application) { if (!client.application) {
await ctx.reply({ embeds: [I18n.en.ping(undefined)] }); await interaction.reply(I18n.replies.ping());
return; return;
} }
await client.application.fetch(); await client.application.fetch();
await ctx.reply({ embeds: [I18n.en.ping(Date.now() - start)] }); await interaction.reply(I18n.replies.ping(Date.now() - start));
} }
const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me."); const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me.");
export { data }; export { data, execute };
@@ -0,0 +1,19 @@
import { Interaction } from "discord.js";
import Client from "../../../lib/client.js";
async function execute(client: Client, eventee: Interaction[]): Promise<void> {
const [interaction] = eventee;
if (interaction.isCommand()) {
const command = client.commands.get(interaction.commandName);
if (command) {
command.execute(client, interaction);
}
}
}
const name = "interactionCreate";
export { execute, name };
@@ -1,21 +1,23 @@
import { generateDependencyReport } from "@discordjs/voice"; import { generateDependencyReport } from "@discordjs/voice";
import { ActivityType } from "discord.js"; import { ActivityType } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
export async function run(client: Client): Promise<void> { async function execute(client: Client): Promise<void> {
if (!client.user || !client.application) { if (!client.user || !client.application) {
console.log("No user or application is provided!"); throw new Error("No application or user for it is provided.");
process.exit(1);
} }
console.log(`${client.user.username} logged in as ${client.user.tag}!`);
console.log(generateDependencyReport()); console.log(generateDependencyReport());
console.log(client.generateApplicationStatus());
client.user.setActivity("over you", { type: ActivityType.Watching }); client.user.setActivity("over you", { type: ActivityType.Watching });
const restCommands = Array.from(client.Commands.values()).map((value) => value.data); const restCommands = Array.from(client.commands.values()).map((value) => value.data);
client.application.commands.set(restCommands); client.application.commands.set(restCommands);
} }
export const name = "ready"; const name = "ready";
export { execute, name };
+41
View File
@@ -0,0 +1,41 @@
import { bold, Collection, EmbedBuilder, InteractionReplyOptions, italic } from "discord.js";
import { ModuleMetadata } from "../../../lib/types.js";
export default class I18n {
public static readonly replies = {
modules: (modules: Collection<string, ModuleMetadata>): InteractionReplyOptions => {
const embed = new EmbedBuilder()
.setColor("Random")
.setTitle("Modules")
.addFields(
modules.map((value) => {
return {
name: value.name + "@" + value.tag,
value: (value.description ?? "") + " | " + value.author,
inline: true
};
})
);
return { embeds: [embed] };
},
ping: (ping?: number): InteractionReplyOptions => {
const title = ping ? `I have pinged ${ping} ms late.` : "There is no application.";
const comment = !ping
? "How am I speaking then?"
: ping > 250
? "I'm kinda slowpoke ngl."
: ping > 200
? "I can do better than this."
: "Dream's speed!";
const embed = new EmbedBuilder()
.setColor("Random")
.setTitle(bold(title))
.setDescription(italic(bold(comment)));
return { embeds: [embed] };
}
};
}
+11
View File
@@ -0,0 +1,11 @@
import path from "path";
import { fileURLToPath } from "url";
const name = "core";
const description = "Quetza core functionality";
const author = "unknowableshade";
const tag = "2302.1";
const rootDir = path.dirname(fileURLToPath(import.meta.url));
export { author, description, name, rootDir, tag };
+27
View File
@@ -0,0 +1,27 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction): Promise<void> {
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
const amount = player.queue.tracks.length;
player.clear();
await interaction.reply({ embeds: [I18n.embeds.cleared(amount)] });
}
const data = new SlashCommandBuilder().setName("clear").setDescription("Clear the player's queue.");
export { data, execute };
+41
View File
@@ -0,0 +1,41 @@
import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
if (!interaction.guild || !interaction.channel || !interaction.member) {
return;
}
await interaction.deferReply();
const player =
controller.get(interaction.guild.id, interaction.channel as TextChannel) ||
controller.set(interaction.guild, interaction.channel as TextChannel);
if (!player.connection) {
const channel = (interaction.member as GuildMember).voice.channel;
if (channel) {
player.connect(channel);
await interaction.editReply({ embeds: [I18n.embeds.okConnected(channel.name)] });
} else {
controller.delete(interaction.guild.id);
await interaction.editReply({ embeds: [I18n.embeds.notConnected()] });
return undefined;
}
} else {
await interaction.editReply({ embeds: [I18n.embeds.wasConnected()] });
}
return player;
}
const data = new SlashCommandBuilder()
.setName("connect")
.setDescription("Connect me to a voice channel.");
export { data, execute };
@@ -1,20 +1,25 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js"; import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
export async function run(client: Client, ctx: CommandInteraction) { async function execute(client: Client, interaction: CommandInteraction) {
const filter = ctx.options.get("filter")?.value as string; const filter = interaction.options.get("filter")?.value as string;
if (!ctx.guild || !ctx.channel) return; if (!interaction.guild || !interaction.channel) {
return;
}
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) return; if (!player) {
return;
}
player.setFilter(filter || undefined); player.setFilter(filter || undefined);
await ctx.reply({ embeds: [I18n.en.filtered(filter || undefined)] }); await interaction.reply({ embeds: [I18n.embeds.filtered(filter || undefined)] });
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
@@ -26,15 +31,15 @@ const data = new SlashCommandBuilder()
.setDescription("Filter to apply.") .setDescription("Filter to apply.")
.setChoices( .setChoices(
{ {
name: "bassboost_low", name: "Bassboost low",
value: "bassboost_low" value: "bassboost_low"
}, },
{ {
name: "bassboost", name: "Bassboost",
value: "bassboost" value: "bassboost"
}, },
{ {
name: "bassboost_high", name: "Bassboost high",
value: "bassboost_high" value: "bassboost_high"
}, },
{ {
@@ -42,83 +47,83 @@ const data = new SlashCommandBuilder()
value: "8D" value: "8D"
}, },
{ {
name: "vaporwave", name: "Vaporwave",
value: "vaporwave" value: "vaporwave"
}, },
{ {
name: "nightcore", name: "Nightcore",
value: "nightcore" value: "nightcore"
}, },
{ {
name: "phaser", name: "Phaser",
value: "phaser" value: "phaser"
}, },
{ {
name: "tremolo", name: "Tremolo",
value: "tremolo" value: "tremolo"
}, },
{ {
name: "vibrato", name: "Vibrato",
value: "vibrato" value: "vibrato"
}, },
{ {
name: "reverse", name: "Reverse",
value: "reverse" value: "reverse"
}, },
{ {
name: "treble", name: "Treble",
value: "treble" value: "treble"
}, },
{ {
name: "normalizer", name: "Normalizer",
value: "normalizer" value: "normalizer"
}, },
{ {
name: "surrounding", name: "Surround Sound",
value: "surrounding" value: "surrounding"
}, },
{ {
name: "pulsator", name: "Pulsator",
value: "pulsator" value: "pulsator"
}, },
{ {
name: "subboost", name: "Subboost",
value: "subboost" value: "subboost"
}, },
{ {
name: "mono", name: "Mono Audio",
value: "mono" value: "mono"
}, },
{ {
name: "compressor", name: "Compressor",
value: "compressor" value: "compressor"
}, },
{ {
name: "expander", name: "Expander",
value: "expander" value: "expander"
}, },
{ {
name: "softlimiter", name: "Softlimiter",
value: "softlimiter" value: "softlimiter"
}, },
{ {
name: "chorus", name: "Chorus",
value: "chorus" value: "chorus"
}, },
{ {
name: "fadein", name: "Fadein",
value: "fadein" value: "fadein"
}, },
{ {
name: "dim", name: "Dimming",
value: "dim" value: "dim"
}, },
{ {
name: "earrape", name: "Earrape",
value: "earrape" value: "earrape"
} }
) )
.setRequired(true) .setRequired(true)
); );
export { data }; export { data, execute };
+23
View File
@@ -0,0 +1,23 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
if (!interaction.guild || !interaction.channel || !interaction.member) {
return;
}
await interaction.deferReply();
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
await interaction.editReply({ embeds: [I18n.embeds.playerInfo(player)] });
}
const data = new SlashCommandBuilder()
.setName("info")
.setDescription("Information about the playback.");
export { data, execute };
+38
View File
@@ -0,0 +1,38 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
const pos = interaction.options.get("position")?.value as number;
const query = interaction.options.get("query")?.value as string;
if (!interaction.guild || !query || !pos || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
await interaction.deferReply();
const track = await player.add(query, interaction.user, pos - 1);
await interaction.editReply({ embeds: [I18n.embeds.appended(track)] });
}
const data = new SlashCommandBuilder()
.setName("insert")
.setDescription("Insert a new track to specific position in the queue.")
.addStringOption((option) =>
option.setName("query").setDescription("Title or URL of the song.").setRequired(true)
)
.addIntegerOption((option) =>
option.setName("position").setDescription("Position to insert.").setRequired(true)
);
export { data, execute };
@@ -1,23 +1,28 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js"; import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
export async function run(client: Client, ctx: CommandInteraction) { async function execute(client: Client, interaction: CommandInteraction) {
const query = ctx.options.get("query")?.value as string; const query = interaction.options.get("query")?.value as string;
if (!ctx.guild || !query || !ctx.channel) return; if (!interaction.guild || !query || !interaction.channel) {
return;
}
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) return; if (!player) {
return;
}
const state = const state =
!isNaN(+query) && isFinite(+query) && !/e/i.test(query) !isNaN(+query) && isFinite(+query) && !/e/i.test(query)
? player.jump(parseInt(query) - 1) ? player.jump(parseInt(query) - 1)
: player.jump(query); : player.jump(query);
await ctx.reply({ embeds: [I18n.en.jumped(state)] }); await interaction.reply({ embeds: [I18n.embeds.jumped(state)] });
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
@@ -27,4 +32,4 @@ const data = new SlashCommandBuilder()
option.setName("query").setDescription("Position or title to jump to.").setRequired(true) option.setName("query").setDescription("Position or title to jump to.").setRequired(true)
); );
export { data }; export { data, execute };
@@ -1,21 +1,26 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js"; import I18n from "../lib/i18n.js";
import { loopOption } from "../lib/types.js"; import { LoopOption } from "../lib/types.js";
import { controller } from "../module.js";
export async function run(client: Client, ctx: CommandInteraction) { async function execute(client: Client, interaction: CommandInteraction) {
const query = ctx.options.get("option")?.value as string; const query = interaction.options.get("option")?.value as string;
if (!ctx.guild || !query || !ctx.channel) return; if (!interaction.guild || !query || !interaction.channel) {
return;
}
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) return; if (!player) {
return;
}
player.Queue.Loop = query as loopOption; player.queue.loop = query as LoopOption;
await ctx.reply({ embeds: [I18n.en.looped(query as loopOption)] }); await interaction.reply({ embeds: [I18n.embeds.looped(query as LoopOption)] });
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
@@ -34,4 +39,4 @@ const data = new SlashCommandBuilder()
.setRequired(true) .setRequired(true)
); );
export { data }; export { data, execute };
+25
View File
@@ -0,0 +1,25 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
player.skip();
await interaction.reply({ embeds: [I18n.embeds.skipped()] });
}
const data = new SlashCommandBuilder().setName("next").setDescription("Skip current song.");
export { data, execute };
+25
View File
@@ -0,0 +1,25 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
const state = player.pause();
await interaction.reply({ embeds: [I18n.embeds.paused(state)] });
}
const data = new SlashCommandBuilder().setName("pause").setDescription("Toggle pause on player.");
export { data, execute };
+34
View File
@@ -0,0 +1,34 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { execute as connect } from "./connect.js";
async function execute(client: Client, interaction: CommandInteraction) {
const query = interaction.options.get("query")?.value as string;
const player = await connect(client, interaction);
if (!player) {
return;
}
if (query) {
const track = await player.add(query, interaction.user);
await interaction.editReply({ embeds: [I18n.embeds.appended(track)] });
}
if (!player.resource) {
player.play();
}
}
const data = new SlashCommandBuilder()
.setName("play")
.setDescription("Launch player and add songs to the queue.")
.addStringOption((option) =>
option.setName("query").setDescription("Title or URL of the song.")
);
export { data, execute };
@@ -10,20 +10,25 @@ import {
TextChannel TextChannel
} from "discord.js"; } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js"; import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
export async function run(client: Client, ctx: CommandInteraction) { async function execute(client: Client, interaction: CommandInteraction) {
if (!ctx.guild || !ctx.channel) return; if (!interaction.guild || !interaction.channel) {
return;
}
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) return; if (!player) {
return;
}
await ctx.deferReply(); await interaction.deferReply();
if (player.Queue.empty()) { if (player.queue.empty()) {
await ctx.editReply({ embeds: [I18n.en.queueEmpty()] }); await interaction.editReply({ embeds: [I18n.embeds.queueEmpty()] });
return; return;
} }
@@ -36,8 +41,8 @@ export async function run(client: Client, ctx: CommandInteraction) {
new ButtonBuilder().setCustomId("QueueEnd").setLabel("🔽").setStyle(ButtonStyle.Primary) new ButtonBuilder().setCustomId("QueueEnd").setLabel("🔽").setStyle(ButtonStyle.Primary)
); );
const message = (await ctx.editReply({ const message = (await interaction.editReply({
embeds: [I18n.en.queueDesigner(player.Queue, 0, player.Resource)], embeds: [I18n.embeds.queueDesigner(player.queue, 0, player.resource)],
components: [row] components: [row]
})) as Message; })) as Message;
@@ -54,19 +59,19 @@ export async function run(client: Client, ctx: CommandInteraction) {
break; break;
} }
case "QueueDown": { case "QueueDown": {
page = Math.min(page + 1, Math.floor(player.Queue.Tracks.length / 11)); page = Math.min(page + 1, Math.floor(player.queue.tracks.length / 11));
break; break;
} }
case "QueueEnd": case "QueueEnd":
page = Math.floor(player.Queue.Tracks.length / 11); page = Math.floor(player.queue.tracks.length / 11);
} }
await btn.update({ await btn.update({
embeds: [I18n.en.queueDesigner(player.Queue, page, player.Resource)] embeds: [I18n.embeds.queueDesigner(player.queue, page, player.resource)]
}); });
}); });
} }
const data = new SlashCommandBuilder().setName("queue").setDescription("Print out the queue."); const data = new SlashCommandBuilder().setName("queue").setDescription("Print out the queue.");
export { data }; export { data, execute };
@@ -1,23 +1,28 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../lib/client.js"; import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js"; import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
export async function run(client: Client, ctx: CommandInteraction) { async function execute(client: Client, interaction: CommandInteraction) {
const query = ctx.options.get("query")?.value as string; const query = interaction.options.get("query")?.value as string;
if (!ctx.guild || !query || !ctx.channel) return; if (!interaction.guild || !query || !interaction.channel) {
return;
}
const player = client.modules.music.get(ctx.guild.id, ctx.channel as TextChannel); const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) return; if (!player) {
return;
}
const track = const track =
!isNaN(+query) && isFinite(+query) && !/e/i.test(query) !isNaN(+query) && isFinite(+query) && !/e/i.test(query)
? player.remove(parseInt(query) - 1) ? player.remove(parseInt(query) - 1)
: player.remove(query); : player.remove(query);
await ctx.reply({ embeds: [I18n.en.removed(track)] }); await interaction.reply({ embeds: [I18n.embeds.removed(track)] });
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
@@ -27,4 +32,4 @@ const data = new SlashCommandBuilder()
option.setName("query").setDescription("Position or title to remove.").setRequired(true) option.setName("query").setDescription("Position or title to remove.").setRequired(true)
); );
export { data }; export { data, execute };
+25
View File
@@ -0,0 +1,25 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
player.shuffle();
await interaction.reply({ embeds: [I18n.embeds.reshuffle()] });
}
const data = new SlashCommandBuilder().setName("reshuffle").setDescription("Shuffle the queue.");
export { data, execute };
+38
View File
@@ -0,0 +1,38 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
const hours = (interaction.options.get("hrs")?.value as number) || 0;
const minutes = (interaction.options.get("mins")?.value as number) || 0;
const seconds = (interaction.options.get("secs")?.value as number) || 0;
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
const time = hours * 3600 + minutes * 60 + seconds;
player.seek(time * 1000);
await interaction.reply({
embeds: [I18n.embeds.fastForwarded(time)]
});
}
const data = new SlashCommandBuilder()
.setName("seek")
.setDescription("Fast-forward by the time.")
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."));
export { data, execute };
+25
View File
@@ -0,0 +1,25 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
player.destroy();
await interaction.reply({ embeds: [I18n.embeds.destroyed()] });
}
const data = new SlashCommandBuilder().setName("stop").setDescription("Destroy the player.");
export { data, execute };
+32
View File
@@ -0,0 +1,32 @@
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { controller } from "../module.js";
async function execute(client: Client, interaction: CommandInteraction) {
const volume = (interaction.options.get("volume")?.value as number) || 100;
if (!interaction.guild || !interaction.channel) {
return;
}
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
if (!player) {
return;
}
player.volume = volume;
await interaction.reply({ embeds: [I18n.embeds.volumeSet(player.volume)] });
}
const data = new SlashCommandBuilder()
.setName("volume")
.setDescription("Change player's volume amount.")
.addIntegerOption((option) =>
option.setName("volume").setDescription("Volume in percents.").setRequired(true)
);
export { data, execute };
@@ -0,0 +1,28 @@
import { VoiceState } from "discord.js";
import Client from "../../../lib/client.js";
import I18n from "../lib/i18n.js";
import { asleep } from "../lib/misc.js";
import { controller } from "../module.js";
async function execute(client: Client, eventee: VoiceState[]): Promise<void> {
const [oldState] = eventee;
const player = controller.get(oldState.guild.id);
if (!player || !oldState.channel || !client.user) {
return;
}
if (oldState.channel.members.filter((value) => value.user !== client.user).size === 0) {
await asleep(30000);
if (oldState.channel.members.filter((value) => value.user !== client.user).size === 0) {
await player.channel.send({ embeds: [I18n.embeds.fullyAlone()] });
player.destroy();
}
}
}
const name = "voiceStateUpdate";
export { execute, name };
@@ -2,13 +2,14 @@ import { execFile } from "child_process";
import path from "path"; import path from "path";
import { promisify } from "util"; import { promisify } from "util";
import config from "../config.js"; import config from "../../../config.js";
import { ytdlArgs, ytdlResponse } from "./types.js"; import { YtdlArgs, YtdlResponse } from "./types.js";
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
function argumentsResolver(args: ytdlArgs): string[] { function argumentsResolver(args: YtdlArgs): string[] {
const options = Object.entries(args); const options = Object.entries(args);
return options.map(([key, value]) => { return options.map(([key, value]) => {
if (typeof value === "boolean" && value) { if (typeof value === "boolean" && value) {
return `--${key.split(/(?=[A-Z])/).join("-")}`.toLowerCase(); return `--${key.split(/(?=[A-Z])/).join("-")}`.toLowerCase();
@@ -18,23 +19,23 @@ function argumentsResolver(args: ytdlArgs): string[] {
}); });
} }
async function ytdlExec(query: string, args: ytdlArgs): Promise<ytdlResponse | undefined> { async function ytdlExec(query: string, args: YtdlArgs): Promise<YtdlResponse | undefined> {
try { try {
const executable = await execFileAsync( const executable = await execFileAsync(
path.join(config.binaries, "/youtube-dl"), path.join(config.binariesDir, "/youtube-dl"),
[query].concat(argumentsResolver(args)) [query].concat(argumentsResolver(args))
); );
return JSON.parse(executable.stdout); return JSON.parse(executable.stdout);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return undefined; return;
} }
} }
export async function fetchStream( export async function fetchStream(
query: string, query: string,
args: ytdlArgs args: YtdlArgs
): Promise<ytdlResponse | undefined> { ): Promise<YtdlResponse | undefined> {
let response = await ytdlExec(query, args); let response = await ytdlExec(query, args);
if (response && !response.extractor.includes("youtube")) { if (response && !response.extractor.includes("youtube")) {
+71
View File
@@ -0,0 +1,71 @@
export default class Filter {
public static readonly filters: Record<string, string> = {
bassboost_low: "bass=g=15:f=110:w=0.3",
bassboost: "bass=g=20:f=110:w=0.3",
bassboost_high: "bass=g=30:f=110:w=0.3",
"8D": "apulsator=hz=0.09",
vaporwave: "aresample=48000,asetrate=48000*0.75",
nightcore: "aresample=48000,asetrate=48000*1.25",
phaser: "aphaser=in_gain=0.4",
tremolo: "tremolo",
vibrato: "vibrato=f=6.5",
reverse: "areverse",
treble: "treble=g=5",
normalizer: "dynaudnorm=g=101",
normalizer2: "acompressor",
surrounding: "surround",
pulsator: "apulsator=hz=1",
subboost: "asubboost",
karaoke: "stereotools=mlev=0.03",
flanger: "flanger",
gate: "agate",
haas: "haas",
mcompand: "mcompand",
mono: "pan=mono|c0=.5*c0+.5*c1",
mstlr: "stereotools=mode=ms>lr",
mstrr: "stereotools=mode=ms>rr",
compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6",
expander:
"compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3",
softlimiter: "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8",
chorus: "chorus=0.7:0.9:55:0.4:0.25:2",
chorus2d: "chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3",
chorus3d: "chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3",
fadein: "afade=t=in:ss=0:d=10",
dim: "afftfilt=\"'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'\"",
earrape: "channelsplit,sidechaingate=level_in=64"
};
private switches = new Set<string>();
public toggled(): [string, string][] {
return Object.entries(Filter.filters)
.filter((value) => this.switches.has(value[0]))
.map((value) => [value[0], value[1]]);
}
public toggle(query?: string): void {
if (!query) {
this.switches.clear();
return;
}
if (!this.switches.delete(query) && Object.hasOwn(Filter.filters, query)) {
this.switches.add(query);
}
}
public size(): number {
return this.switches.size;
}
public empty(): boolean {
return this.size() === 0;
}
public toString(): string {
return this.toggled()
.map((value) => value[1])
.join(",");
}
}
@@ -4,11 +4,11 @@ import { EmbedBuilder } from "discord.js";
import { loopMoji, secToISO, statusBarGenerator, volumeMoji } from "./misc.js"; import { loopMoji, secToISO, statusBarGenerator, volumeMoji } from "./misc.js";
import Player from "./player.js"; import Player from "./player.js";
import Queue from "./queue.js"; import Queue from "./queue.js";
import { loopOption, track } from "./types.js"; import { LoopOption, Track } from "./types.js";
export default class I18n { export default class I18n {
public static en = { public static readonly embeds = {
nowPlaying: (track: track) => nowPlaying: (track: Track) =>
new EmbedBuilder() new EmbedBuilder()
.setColor("LuminousVividPink") .setColor("LuminousVividPink")
.setAuthor({ .setAuthor({
@@ -44,7 +44,7 @@ export default class I18n {
? `**✅ ${filter} was applied to the player.**` ? `**✅ ${filter} was applied to the player.**`
: "**✅ All filters were declined.**" : "**✅ All filters were declined.**"
), ),
appended: (track?: track) => appended: (track?: Track) =>
!track !track
? new EmbedBuilder() ? new EmbedBuilder()
.setColor("Red") .setColor("Red")
@@ -64,7 +64,7 @@ export default class I18n {
state state
? new EmbedBuilder().setColor("Green").setTitle("_**Perfoming jump.**_") ? new EmbedBuilder().setColor("Green").setTitle("_**Perfoming jump.**_")
: new EmbedBuilder().setColor("Red").setTitle("**❗ Bad _jump_ request.**"), : new EmbedBuilder().setColor("Red").setTitle("**❗ Bad _jump_ request.**"),
looped: (query: loopOption) => { looped: (query: LoopOption) => {
switch (query) { switch (query) {
case "AUTO": { case "AUTO": {
return new EmbedBuilder() return new EmbedBuilder()
@@ -93,7 +93,7 @@ export default class I18n {
state state
? new EmbedBuilder().setColor("Green").setTitle("_**▶️ Resumed.**_") ? new EmbedBuilder().setColor("Green").setTitle("_**▶️ Resumed.**_")
: new EmbedBuilder().setColor("Yellow").setTitle("_**⏸️ Paused.**_"), : new EmbedBuilder().setColor("Yellow").setTitle("_**⏸️ Paused.**_"),
removed: (track?: track) => removed: (track?: Track) =>
track track
? new EmbedBuilder() ? new EmbedBuilder()
.setColor("Green") .setColor("Green")
@@ -115,54 +115,57 @@ export default class I18n {
.setTitle(`**${volumeMoji(amount)} Volume has been set to ${amount}%**`), .setTitle(`**${volumeMoji(amount)} Volume has been set to ${amount}%**`),
queueEmpty: () => new EmbedBuilder().setColor("Yellow").setTitle("**❎ Queue is empty.**"), queueEmpty: () => new EmbedBuilder().setColor("Yellow").setTitle("**❎ Queue is empty.**"),
playerInfo: (player?: Player) => { playerInfo: (player?: Player) => {
if (!player) if (!player) {
return new EmbedBuilder() return new EmbedBuilder()
.setColor("Red") .setColor("Red")
.setTitle("**🚫 Player does not exist.**"); .setTitle("**🚫 Player does not exist.**");
}
if (player.Resource) { if (player.resource) {
return new EmbedBuilder() return new EmbedBuilder()
.setColor("LuminousVividPink") .setColor("LuminousVividPink")
.setTitle(`${player.Queue.Position + 1}. ${player.Resource.metadata.title}`) .setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`)
.setDescription( .setDescription(
statusBarGenerator( statusBarGenerator(
player.Resource.playbackDuration / 1000, player.resource.playbackDuration / 1000,
player.Resource.metadata.duration player.resource.metadata.duration
) )
) )
.setThumbnail(player.Resource.metadata.thumbnail) .setThumbnail(player.resource.metadata.thumbnail)
.setURL(player.Resource.metadata.url) .setURL(player.resource.metadata.url)
.addFields([ .addFields([
{ {
name: "Applied Filters", name: "Applied Filters",
value: value:
player.Filter.Filters.map((value) => value[0]).join("\n") + player.filter
"\ndefault", .toggled()
.map((value) => value[0])
.join("\n") + "\ndefault",
inline: true inline: true
}, },
{ {
name: "Queue Length", name: "Queue Length",
value: `🔢 ${player.Queue.Tracks.length}`, value: `🔢 ${player.queue.tracks.length}`,
inline: true inline: true
}, },
{ {
name: "Queue Duration", name: "Queue Duration",
value: `💿 ${secToISO(player.Queue.Duration)}`, value: `💿 ${secToISO(player.queue.duration)}`,
inline: true inline: true
}, },
{ {
name: "Queue Looping", name: "Queue Looping",
value: `${loopMoji(player.Queue.Loop)} ${player.Queue.Loop}`, value: `${loopMoji(player.queue.loop)} ${player.queue.loop}`,
inline: true inline: true
}, },
{ {
name: "Volume amount", name: "Volume amount",
value: `${volumeMoji(player.Volume)} ${player.Volume}/150`, value: `${volumeMoji(player.volume)} ${player.volume}/150`,
inline: true inline: true
}, },
{ {
name: "Player lifetime", name: "Player lifetime",
value: `🕓 ${secToISO((Date.now() - player.Created.getTime()) / 1000)}`, value: `🕓 ${secToISO((Date.now() - player.created.getTime()) / 1000)}`,
inline: true inline: true
} }
]); ]);
@@ -176,46 +179,48 @@ export default class I18n {
{ {
name: "Applied Filters", name: "Applied Filters",
value: value:
player.Filter.Filters.map((value) => value[0]).join("\n") + player.filter
"\ndefault", .toggled()
.map((value) => value[0])
.join("\n") + "\ndefault",
inline: true inline: true
}, },
{ {
name: "Queue Length", name: "Queue Length",
value: `🔢 ${player.Queue.Tracks.length}`, value: `🔢 ${player.queue.tracks.length}`,
inline: true inline: true
}, },
{ {
name: "Queue Duration", name: "Queue Duration",
value: `💿 ${secToISO(player.Queue.Duration)}`, value: `💿 ${secToISO(player.queue.duration)}`,
inline: true inline: true
}, },
{ {
name: "Queue Looping", name: "Queue Looping",
value: `${loopMoji(player.Queue.Loop)} ${player.Queue.Loop}`, value: `${loopMoji(player.queue.loop)} ${player.queue.loop}`,
inline: true inline: true
}, },
{ {
name: "Volume amount", name: "Volume amount",
value: `${volumeMoji(player.Volume)} ${player.Volume}/150`, value: `${volumeMoji(player.volume)} ${player.volume}/150`,
inline: true inline: true
}, },
{ {
name: "Player lifetime", name: "Player lifetime",
value: `🕓 ${secToISO((Date.now() - player.Created.getTime()) / 1000)}`, value: `🕓 ${secToISO((Date.now() - player.created.getTime()) / 1000)}`,
inline: true inline: true
} }
]); ]);
} }
}, },
queueDesigner: (queue: Queue, page: number, resource: AudioResource<track> | undefined) => { queueDesigner: (queue: Queue, page: number, resource: AudioResource<Track> | undefined) => {
page = Math.max(0, Math.min(Math.floor(queue.Tracks.length / 11), page)); page = Math.max(0, Math.min(Math.floor(queue.tracks.length / 11), page));
return new EmbedBuilder() return new EmbedBuilder()
.setColor("LuminousVividPink") .setColor("LuminousVividPink")
.setTitle( .setTitle(
resource resource
? `${queue.Position + 1}. ${resource.metadata.title}` ? `${queue.position + 1}. ${resource.metadata.title}`
: "💿 Insert the disk..." : "💿 Insert the disk..."
) )
.setURL( .setURL(
@@ -237,7 +242,7 @@ export default class I18n {
: statusBarGenerator(0, 0) : statusBarGenerator(0, 0)
) )
.setFields( .setFields(
queue.Tracks.slice(page * 10, page * 10 + 10).map((value, index) => ({ queue.tracks.slice(page * 10, page * 10 + 10).map((value, index) => ({
name: `${page * 10 + index + 1}. ${value.title.padEnd(35)}`, name: `${page * 10 + index + 1}. ${value.title.padEnd(35)}`,
value: `**[${secToISO(value.duration)}]**` value: `**[${secToISO(value.duration)}]**`
})) }))
@@ -249,22 +254,6 @@ export default class I18n {
.setTitle("🥬 I have decided to leave to conserve my energy.") .setTitle("🥬 I have decided to leave to conserve my energy.")
.setDescription("_**Since there was nobody in the voice channel.**_"), .setDescription("_**Since there was nobody in the voice channel.**_"),
reshuffle: () => reshuffle: () =>
new EmbedBuilder().setColor("Green").setTitle("**✅ Successfuly reshuffled queue.**"), new EmbedBuilder().setColor("Green").setTitle("**✅ Successfuly reshuffled queue.**")
ping: (ping?: number) => {
const title = ping ? `I have pinged ${ping} ms late.` : "There is no application.";
const comment = !ping
? "How am I speaking then?"
: ping > 250
? "I'm kinda slowpoke ngl."
: ping > 200
? "I can do better than this."
: "Dream's speed!";
return new EmbedBuilder()
.setColor("Random")
.setTitle(`**${title}**`)
.setDescription(`_**${comment}**_`);
}
}; };
} }
@@ -1,17 +1,17 @@
import { loopOption } from "./types.js"; import { LoopOption } from "./types.js";
const PLAYBACK_LENGTH = 15;
export function statusBarGenerator(playback: number, duration: number): string {
const filled = Array<string>(
Math.floor((PLAYBACK_LENGTH * playback) / (duration === 0 ? 1 : duration))
).fill("🔹");
export function statusBarGenerator(duration: number, length: number): string {
duration = Math.max(0, duration);
length = Math.max(0, length);
if (duration > length) duration = length;
const filled = Array<string>(Math.floor((15 * duration) / (length === 0 ? 1 : length))).fill(
":small_blue_diamond:"
);
const empty = Array<string>( const empty = Array<string>(
15 - Math.floor((15 * duration) / (length === 0 ? 1 : length)) PLAYBACK_LENGTH - Math.floor((PLAYBACK_LENGTH * playback) / (duration === 0 ? 1 : duration))
).fill(":white_small_square:"); ).fill("▫️");
return `**[${filled.join("")}${empty.join("")}] ${secToISO(duration)}/${secToISO(length)}**`; return `**[${filled.join("")}${empty.join("")}] ${secToISO(playback)}/${secToISO(duration)}**`;
} }
export function secToISO(amount: number): string { export function secToISO(amount: number): string {
@@ -22,6 +22,7 @@ export function secToISO(amount: number): string {
const hDisplay = h > 0 ? `${h}:` : ""; const hDisplay = h > 0 ? `${h}:` : "";
const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`; const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`;
const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`; const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`;
return hDisplay + mDisplay + sDisplay; return hDisplay + mDisplay + sDisplay;
} }
@@ -33,7 +34,7 @@ export function volumeMoji(amount: number): string {
return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇"; return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇";
} }
export function loopMoji(loop: loopOption): string { export function loopMoji(loop: LoopOption): string {
return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂"; return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂";
} }
@@ -42,10 +43,14 @@ export function largestCommonSequence(s1: string, s2: string) {
s2 = s2.toLowerCase().slice(0, 100); s2 = s2.toLowerCase().slice(0, 100);
const buffer = Array.from(Array(s1.length + 1), () => new Array<number>(s2.length + 1).fill(0)); const buffer = Array.from(Array(s1.length + 1), () => new Array<number>(s2.length + 1).fill(0));
for (let i = 1; i <= s1.length; i++) { for (let i = 1; i <= s1.length; i++) {
for (let j = 1; j <= s2.length; j++) { for (let j = 1; j <= s2.length; j++) {
if (s1[i - 1] === s2[j - 1]) buffer[i][j] = buffer[i - 1][j - 1] + 1; if (s1[i - 1] === s2[j - 1]) {
else buffer[i][j] = Math.max(buffer[i - 1][j], buffer[i][j - 1]); buffer[i][j] = buffer[i - 1][j - 1] + 1;
} else {
buffer[i][j] = Math.max(buffer[i - 1][j], buffer[i][j - 1]);
}
} }
} }
@@ -1,26 +1,31 @@
import { Guild, TextChannel } from "discord.js"; import { Collection, Guild, TextChannel } from "discord.js";
import Player from "./player.js"; import Player from "./player.js";
export default class Music { export default class Music {
private players = new Map<string, Player>(); private players = new Collection<string, Player>();
public get(guildId: string, channel?: TextChannel): Player | undefined { public get(guildId: string, channel?: TextChannel): Player | undefined {
const player = this.players.get(guildId); const player = this.players.get(guildId);
if (player) { if (player) {
if (channel) player.Channel = channel; if (channel) {
player.channel = channel;
}
return player; return player;
} }
} }
public set(guild: Guild, channel: TextChannel): Player { public set(guild: Guild, channel: TextChannel): Player {
const player = new Player(guild, this, channel); const player = new Player(guild, this, channel);
this.players.set(guild.id, player); this.players.set(guild.id, player);
return player; return player;
} }
public del(guildId: string): void { public delete(guildId: string): void {
this.players.delete(guildId); this.players.delete(guildId);
} }
} }
@@ -19,72 +19,66 @@ import I18n from "./i18n.js";
import { largestCommonSequence } from "./misc.js"; import { largestCommonSequence } from "./misc.js";
import Music from "./music.js"; import Music from "./music.js";
import Queue from "./queue.js"; import Queue from "./queue.js";
import { track } from "./types.js"; import { Track } from "./types.js";
export default class Player { export default class Player {
private music: Music; private music: Music;
private guild: Guild; private guild: Guild;
private channel: TextChannel; private msgChannel: TextChannel;
public get Channel(): TextChannel { public get channel(): TextChannel {
return this.channel; return this.msgChannel;
} }
public set Channel(x: TextChannel) { public set channel(x: TextChannel) {
this.channel = x; this.msgChannel = x;
}
private connection: VoiceConnection | undefined;
public get Connection(): VoiceConnection | undefined {
return this.connection;
}
private player: AudioPlayer;
public get Player(): AudioPlayer {
return this.player;
}
private filter = new Filter();
public get Filter(): Filter {
return this.filter;
}
private queue = new Queue();
public get Queue(): Queue {
return this.queue;
}
private resource: AudioResource<track> | undefined;
public get Resource(): AudioResource<track> | undefined {
return this.resource;
} }
private message: Message | undefined; private message: Message | undefined;
private created: Date = new Date();
public get Created(): Date { public internalConnection: VoiceConnection | undefined;
return this.created; public get connection(): VoiceConnection | undefined {
return this.internalConnection;
} }
private volume = 100; private internalPlayer: AudioPlayer;
public get Volume(): number { public get player(): AudioPlayer {
return this.volume; return this.internalPlayer;
} }
public set Volume(x: number) {
this.volume = Math.max(0, Math.min(150, x));
if (this.resource && this.resource.volume) private playerResource: AudioResource<Track> | undefined;
this.resource.volume.setVolumeLogarithmic(this.volume / 100); public get resource(): AudioResource<Track> | undefined {
return this.playerResource;
} }
private resourceVolume = 100;
public get volume(): number {
return this.resourceVolume;
}
public set volume(x: number) {
this.resourceVolume = Math.max(0, Math.min(150, x));
if (this.playerResource && this.playerResource.volume) {
this.playerResource.volume.setVolumeLogarithmic(this.resourceVolume / 100);
}
}
public readonly queue = new Queue();
public readonly filter = new Filter();
public readonly created: Date = new Date();
public constructor(guild: Guild, music: Music, textChannel: TextChannel) { public constructor(guild: Guild, music: Music, textChannel: TextChannel) {
this.music = music; this.music = music;
this.guild = guild; this.guild = guild;
this.channel = textChannel; this.msgChannel = textChannel;
this.player = createAudioPlayer(); this.internalPlayer = createAudioPlayer();
this.player.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable()); this.internalPlayer.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
} }
private resourceGenerator(track: track, seek?: number) { private resourceGenerator(track: Track, seek?: number) {
const stream = ytdl(track.url, { const stream = ytdl(track.url, {
opusEncoded: false, opusEncoded: false,
fmt: "mp3", fmt: "mp3",
@@ -94,37 +88,46 @@ export default class Player {
encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
}); });
this.resource = createAudioResource(stream, { this.playerResource = createAudioResource(stream, {
inlineVolume: true, inlineVolume: true,
metadata: track metadata: track
}); });
if (seek) this.resource.playbackDuration = seek; if (seek) {
if (this.resource.volume) this.resource.volume.setVolumeLogarithmic(this.volume / 100); this.playerResource.playbackDuration = seek;
}
return this.resource; if (this.playerResource.volume) {
this.playerResource.volume.setVolumeLogarithmic(this.resourceVolume / 100);
}
return this.playerResource;
} }
private resourceEndResolvable(): void { private resourceEndResolvable(): void {
if (this.resource) this.resource.encoder?.destroy(); if (this.playerResource) {
this.playerResource.encoder?.destroy();
}
if (this.queue.next()) { if (this.queue.next()) {
this.play(); this.play();
} else { } else {
this.resource = undefined; this.playerResource = undefined;
this.player.stop(); this.internalPlayer.stop();
} }
} }
private async messageResolvable(track: track) { private async messageResolvable(track: Track) {
if (this.message) await this.message.delete(); if (this.message) {
await this.message.delete();
}
this.message = await this.channel.send({ this.message = await this.msgChannel.send({
embeds: [I18n.en.nowPlaying(track)] embeds: [I18n.embeds.nowPlaying(track)]
}); });
} }
private async search(query: string, requester: User): Promise<track[] | undefined> { private async search(query: string, requester: User): Promise<Track[] | undefined> {
const response = await fetchStream(query, { const response = await fetchStream(query, {
dumpSingleJson: true, dumpSingleJson: true,
noWarnings: true, noWarnings: true,
@@ -134,9 +137,11 @@ export default class Player {
defaultSearch: "ytsearch" defaultSearch: "ytsearch"
}); });
if (!response) return undefined; if (!response) {
return undefined;
}
const tracks: track[] = []; const tracks: Track[] = [];
if (response.entries) { if (response.entries) {
for (const entry of response.entries) { for (const entry of response.entries) {
@@ -164,34 +169,38 @@ export default class Player {
public play(seek?: number): void { public play(seek?: number): void {
const track = this.queue.current(); const track = this.queue.current();
if (!this.connection || !track) return; if (!this.internalConnection || !track) {
return;
}
this.player.play(this.resourceGenerator(track, seek)); this.internalPlayer.play(this.resourceGenerator(track, seek));
this.connection.subscribe(this.player); this.internalConnection.subscribe(this.internalPlayer);
this.messageResolvable(track); this.messageResolvable(track);
} }
public connect(channel: VoiceBasedChannel): void { public connect(channel: VoiceBasedChannel): void {
if (this.connection) this.connection.destroy(); if (this.internalConnection) {
this.internalConnection.destroy();
}
this.connection = joinVoiceChannel({ this.internalConnection = joinVoiceChannel({
channelId: channel.id, channelId: channel.id,
guildId: channel.guildId, guildId: channel.guildId,
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
}); });
this.connection.on(VoiceConnectionStatus.Disconnected, async () => { this.internalConnection.on(VoiceConnectionStatus.Disconnected, async () => {
try { try {
await Promise.race([ await Promise.race([
entersState( entersState(
this.connection as VoiceConnection, this.internalConnection as VoiceConnection,
VoiceConnectionStatus.Signalling, VoiceConnectionStatus.Signalling,
10_000 10_000
), ),
entersState( entersState(
this.connection as VoiceConnection, this.internalConnection as VoiceConnection,
VoiceConnectionStatus.Connecting, VoiceConnectionStatus.Connecting,
10_000 10_000
) )
@@ -202,10 +211,12 @@ export default class Player {
}); });
} }
public async add(query: string, requester: User, index?: number): Promise<track | undefined> { public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
const tracks = await this.search(query, requester); const tracks = await this.search(query, requester);
if (!tracks) return; if (!tracks) {
return;
}
this.queue.push(tracks, index); this.queue.push(tracks, index);
@@ -217,7 +228,7 @@ export default class Player {
let diff = 0; let diff = 0;
if (typeof query === "string") { if (typeof query === "string") {
this.queue.Tracks.forEach((track, index) => { this.queue.tracks.forEach((track, index) => {
const tempDiff = largestCommonSequence(track.title, query); const tempDiff = largestCommonSequence(track.title, query);
if (diff < tempDiff) { if (diff < tempDiff) {
diff = tempDiff; diff = tempDiff;
@@ -234,14 +245,14 @@ export default class Player {
return false; return false;
} }
public remove(query?: number | string): track | undefined { public remove(query?: number | string): Track | undefined {
const oldPosition = this.queue.Position; const oldPosition = this.queue.position;
let newPosition = typeof query === "number" ? query : this.queue.Position; let newPosition = typeof query === "number" ? query : this.queue.position;
let diff = 0; let diff = 0;
if (typeof query === "string") { if (typeof query === "string") {
this.queue.Tracks.forEach((track, index) => { this.queue.tracks.forEach((track, index) => {
const tempDiff = largestCommonSequence(track.title, query); const tempDiff = largestCommonSequence(track.title, query);
if (diff < tempDiff) { if (diff < tempDiff) {
diff = tempDiff; diff = tempDiff;
@@ -253,7 +264,9 @@ export default class Player {
const track = this.queue.pop(newPosition); const track = this.queue.pop(newPosition);
if (track) { if (track) {
if (oldPosition === newPosition) this.play(); if (oldPosition === newPosition) {
this.play();
}
return track; return track;
} }
@@ -266,40 +279,50 @@ export default class Player {
public clear(): void { public clear(): void {
this.queue.clear(); this.queue.clear();
this.resource = undefined; this.playerResource = undefined;
if (this.player) this.player.stop();
if (this.internalPlayer) {
this.internalPlayer.stop();
}
} }
public destroy(): void { public destroy(): void {
if (this.connection) this.connection.destroy(); if (this.internalConnection) {
this.player.stop(); this.internalConnection.destroy();
this.music.del(this.guild.id); }
this.internalPlayer.stop();
this.music.delete(this.guild.id);
} }
public pause(): boolean { public pause(): boolean {
if (this.player.state.status === AudioPlayerStatus.Paused) { if (this.internalPlayer.state.status === AudioPlayerStatus.Paused) {
this.player.unpause(); this.internalPlayer.unpause();
return false; return false;
} else { } else {
this.player.pause(); this.internalPlayer.pause();
return true; return true;
} }
} }
public shuffle(): void { public shuffle(): void {
this.queue.shuffle(); this.queue.shuffle();
if (this.resource) {
this.player.stop(); if (this.playerResource) {
this.internalPlayer.stop();
this.play(); this.play();
} }
} }
public skip(): void { public skip(): void {
this.player.stop(); this.internalPlayer.stop();
} }
public setFilter(filter?: string): void { public setFilter(filter?: string): void {
this.filter.toggle(filter); this.filter.toggle(filter);
if (this.resource) this.play(this.resource.playbackDuration);
if (this.playerResource) {
this.play(this.playerResource.playbackDuration);
}
} }
} }
+117
View File
@@ -0,0 +1,117 @@
import { LoopOption, Track } from "./types.js";
export default class Queue {
private entries: Track[] = [];
public get tracks(): Track[] {
return this.entries;
}
private currentIndex = 0;
public get position(): number {
return this.currentIndex;
}
private loopStatus: LoopOption = "NONE";
public get loop(): LoopOption {
return this.loopStatus;
}
public set loop(x: LoopOption) {
this.loopStatus = x;
}
private overallDuration = 0;
public get duration(): number {
return this.overallDuration;
}
public next(): Track | undefined {
if (this.loopStatus === "SONG") {
return this.current();
}
if (this.loopStatus === "AUTO") {
this.currentIndex = Math.floor(Math.random() * this.entries.length);
return this.current();
}
if (this.currentIndex + 1 < this.entries.length) {
this.currentIndex++;
return this.current();
} else if (this.loopStatus === "LOOP") {
this.currentIndex = 0;
return this.current();
} else if (this.loopStatus === "NONE") {
this.currentIndex = 0;
return undefined;
}
}
public shuffle(): void {
let currentIndex = this.entries.length;
while (currentIndex != 0) {
const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[this.entries[currentIndex], this.entries[randomIndex]] = [
this.entries[randomIndex],
this.entries[currentIndex]
];
}
this.currentIndex = 0;
}
public push(tracks: Track[], index?: number): Track {
if (!index || index >= this.entries.length) {
this.entries = this.entries.concat(tracks);
} else {
this.entries.splice(Math.max(0, index), 0, ...tracks);
if (this.currentIndex >= index) {
this.currentIndex += tracks.length;
}
}
for (const track of tracks) {
this.overallDuration += track.duration;
}
return this.entries[index || this.entries.length - 1];
}
public pop(query: number): Track | undefined {
if (query >= 0 && query < this.entries.length) {
const track = this.entries[query];
this.entries.splice(query, 1);
this.currentIndex = Math.max(0, this.currentIndex - 1);
this.overallDuration -= track.duration;
return track;
}
return undefined;
}
public jump(query: number): Track | undefined {
if (query >= 0 && query < this.entries.length) {
const track = this.entries[query];
this.currentIndex = query;
return track;
}
return undefined;
}
public current(): Track {
return this.entries[this.currentIndex];
}
public empty(): boolean {
return this.entries.length === 0;
}
public clear(): void {
this.entries = [];
this.currentIndex = 0;
}
}
+261
View File
@@ -0,0 +1,261 @@
import { User } from "discord.js";
export interface Track {
url: string;
title: string;
thumbnail: string;
duration: number;
requester: User;
}
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
export interface YtdlArgs {
help?: boolean;
version?: boolean;
update?: boolean;
ignoreErrors?: boolean;
abortOnError?: boolean;
dumpUserAgent?: boolean;
listExtractors?: boolean;
extractorDescriptions?: boolean;
forceGenericExtractor?: boolean;
defaultSearch?: string;
igonreConfig?: boolean;
configLocation?: string;
flatPlaylist?: boolean;
markWatched?: boolean;
noColor?: boolean;
proxy?: string;
socketTimeout?: number;
sourceAddress?: string;
forceIpv4?: boolean;
forceIpv6?: boolean;
geoVerificationProxy?: string;
geoBypass?: boolean;
geoBypassCountry?: string;
geoBypassIpBlock?: string;
playlistStart?: number;
playlistEnd?: number | "last";
playlistItems?: string;
matchTitle?: string;
rejectTitle?: string;
maxDownloads?: number;
minFilesize?: string;
maxFilesize?: string;
date?: string;
datebefore?: string;
dateafter?: string;
minViews?: number;
maxViews?: number;
matchFilter?: string;
noPlaylist?: boolean;
yesPlaylist?: boolean;
ageLimit?: number;
downloadArchive?: string;
includeAds?: boolean;
limitRate?: string;
retries?: number | "infinite";
skipUnavailableFragments?: boolean;
abortOnUnavailableFragment?: boolean;
keepFragments?: boolean;
bufferSize?: string;
noResizeBuffer?: boolean;
httpChunkSize?: string;
playlistReverse?: boolean;
playlistRandom?: boolean;
xattrSetFilesize?: boolean;
hlsPreferNative?: boolean;
hlsPreferFfmpeg?: boolean;
hlsUseMpegts?: boolean;
externalDownloader?: string;
externalDownloaderArgs?: string;
batchFile?: string;
id?: boolean;
output?: string;
outputNaPlaceholder?: string;
autonumberStart?: number;
restrictFilenames?: boolean;
noOverwrites?: boolean;
continue?: boolean;
noPart?: boolean;
noMtime?: boolean;
writeDescription?: boolean;
writeInfoJson?: boolean;
writeAnnotations?: boolean;
loadInfoJson?: string;
cookies?: string;
cacheDir?: string;
noCacheDir?: boolean;
rmCacheDir?: boolean;
writeThumbnail?: boolean;
writeAllThumbnails?: boolean;
listThumbnails?: boolean;
quiet?: boolean;
noWarnings?: boolean;
simulate?: boolean;
skipDownload?: boolean;
getUrl?: boolean;
getTitle?: boolean;
getId?: boolean;
getThumbnail?: boolean;
getDuration?: boolean;
getFilename?: boolean;
getFormat?: boolean;
dumpJson?: boolean;
dumpSingleJson?: boolean;
printJson?: boolean;
newline?: boolean;
noProgress?: boolean;
consoleTitle?: boolean;
verbose?: boolean;
dumpPages?: boolean;
writePages?: boolean;
printTraffic?: boolean;
callHome?: boolean;
encoding?: string;
noCheckCertificate?: boolean;
preferInsecure?: boolean;
userAgent?: string;
referer?: string;
addHeader?: string;
bidiWorkaround?: boolean;
sleepInterval?: number;
maxSleepInterval?: number;
format?: string;
allFormats?: boolean;
preferFreeFormats?: boolean;
listFormats?: boolean;
youtubeSkipDashManifest?: boolean;
mergeOutputFormat?: string;
writeSub?: boolean;
writeAutoSub?: boolean;
allSubs?: boolean;
listSubs?: boolean;
subFormat?: string;
subLang?: string;
username?: string;
password?: string;
twofactor?: string;
netrc?: boolean;
videoPassword?: string;
apMso?: string;
apUsername?: string;
apPassword?: string;
apListMso?: boolean;
extractAudio?: boolean;
audioFormat?: string;
audioQuality?: number;
recodeVideo?: string;
postprocessorArgs?: string;
keepVideo?: boolean;
noPostOverwrites?: boolean;
embedSubs?: boolean;
embedThumbnail?: boolean;
addMetadata?: boolean;
metadataFromFile?: string;
xattrs?: boolean;
fixup?: string;
preferAvconv?: boolean;
preferFfmpeg?: boolean;
ffmpegLocation?: string;
exec?: string;
convertSubs?: string;
}
interface YtdlFormat {
asr: number;
filesize: number;
format_id: string;
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: unknown;
container: string;
format: string;
protocol: string;
http_headers: unknown;
}
interface YtdlThumbnail {
height: number;
url: string;
width: number;
resolution: string;
id: string;
}
interface YtdlEntry {
uploader: string;
duration: number;
id: string;
title: string;
url: string;
}
export interface YtdlResponse {
id: string;
title: string;
formats: YtdlFormat[];
thumbnails: YtdlThumbnail[];
description: string;
upload_date: string;
uploader: string;
uploader_id: string;
uploader_url: string;
channel_id: string;
channel_url: string;
duration: number;
view_count: number;
average_rating: number;
age_limit: number;
webpage_url: string;
categories: string[];
tags: string[];
is_live: boolean;
like_count: number;
dislike_count: number;
channel: string;
track: string;
artist: string;
creator: string;
alt_title: string;
extractor: string;
webpage_url_basename: string;
extractor_key: string;
playlist: string;
playlist_index: number;
thumbnail: string;
display_id: string;
requested_subtitles: unknown;
asr: number;
filesize: number;
format_id: string;
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: { http_chunk_size: number };
container: string;
format: string;
protocol: string;
http_headers: unknown;
fulltitle: string;
_filename: string;
entries: YtdlEntry[];
}
+15
View File
@@ -0,0 +1,15 @@
import path from "path";
import { fileURLToPath } from "url";
import Music from "./lib/music.js";
const name = "music";
const description = "Music module";
const author = "unknowableshade";
const tag = "2302.1";
const controller = new Music();
const rootDir = path.dirname(fileURLToPath(import.meta.url));
export { author, controller, description, name, rootDir, tag };
+1 -1
View File
@@ -15,5 +15,5 @@
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist", "src/commands", "src/events"]
} }