2205.1 major update!

This commit is contained in:
UnknowableShade committed 2022-05-29 23:01:47 +03:00
1 parent 913b718026
commit a86faf06d5
75 files changed
+4457 -2704

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
env:
browser: true
es2021: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/recommended
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- '@typescript-eslint'
rules:
indent:
- error
- 4
linebreak-style:
- error
- windows
quotes:
- error
- double
semi:
- error
- always
+1 -1
View File
@@ -367,5 +367,5 @@ dist/
*.js
*.js.map
#binaries
# binaries will bot bundle
bin/
-25
View File
@@ -1,25 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31808.319
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "5a0b-bot", "5a0b-bot.njsproj", "{0DDE9DED-3734-43AC-A218-393675657D81}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0DDE9DED-3734-43AC-A218-393675657D81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DDE9DED-3734-43AC-A218-393675657D81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DDE9DED-3734-43AC-A218-393675657D81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DDE9DED-3734-43AC-A218-393675657D81}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {86AD99D1-E808-449F-9D8E-174746181BE1}
EndGlobalSection
EndGlobal
-134
View File
@@ -1,134 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<Name>5a0b-bot</Name>
<RootNamespace>NodejsConsoleApp1</RootNamespace>
<SaveNodeJsSettingsInProjectFile>True</SaveNodeJsSettingsInProjectFile>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>0dde9ded-3734-43ac-a218-393675657d81</ProjectGuid>
<ProjectHome>
</ProjectHome>
<StartupFile>dist\app.js</StartupFile>
<StartWebBrowser>False</StartWebBrowser>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>./dist</OutputPath>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
<EnableTypeScript>true</EnableTypeScript>
<StartWebBrowser>False</StartWebBrowser>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Content Include=".gitignore" />
<Content Include="package.json" />
<Content Include="README.md" />
<Content Include="requirements.txt" />
<Content Include="tsconfig.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="src\" />
<Folder Include="src\assets\" />
<Folder Include="src\assets\DiscordMusic\" />
<Folder Include="src\commands\" />
<Folder Include="src\commands\music\" />
<Folder Include="src\events\" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="src\app.ts" />
<TypeScriptCompile Include="src\assets\Misc.ts" />
<TypeScriptCompile Include="src\assets\DiscordMusic\Music.ts" />
<TypeScriptCompile Include="src\assets\QuetzaClient.ts" />
<TypeScriptCompile Include="src\assets\DiscordMusic\QueueStorage.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\assets\Types.ts" />
<TypeScriptCompile Include="src\commands\music\clear.ts" />
<TypeScriptCompile Include="src\commands\music\connect.ts" />
<TypeScriptCompile Include="src\commands\music\delete.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\commands\music\insert.ts" />
<TypeScriptCompile Include="src\commands\music\jump.ts" />
<TypeScriptCompile Include="src\commands\music\load.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\commands\music\loop.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\commands\music\next.ts" />
<TypeScriptCompile Include="src\commands\music\pause.ts" />
<TypeScriptCompile Include="src\commands\music\peak.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\commands\music\play.ts" />
<TypeScriptCompile Include="src\commands\music\queue.ts" />
<TypeScriptCompile Include="src\commands\music\remove.ts" />
<TypeScriptCompile Include="src\commands\music\save.ts" />
<TypeScriptCompile Include="src\commands\music\seek.ts" />
<TypeScriptCompile Include="src\commands\music\stop.ts" />
<TypeScriptCompile Include="src\config.ts" />
<TypeScriptCompile Include="src\events\interactionCreate.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\events\messageCreate.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\events\ready.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="src\events\voiceStateUpdate.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
</ItemGroup>
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>0</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:48022/</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-present unknowableshade
Copyright (c) 2021-present Vladimir Eremin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+3 -55
View File
@@ -1,59 +1,7 @@
# QuetzaBot - 2201.1 <soundrewrite>
Typescript/node js discord bot.
# Quetza - 2205.1 <soundrewrite>
## Latest update
> _**Higher perfomance - better design.**_
- Rebuilt structure allows to write seperated modules for bot.
## Features
### Music
- Play music from YouTube and many other sites.
- Create your own queues and save them
- Loop queue, song.
- Play next, stop, pause, resume.
- Seek for a time code.
- Queue jump, remove.
- Lots of different filters (nightcore, earrape).
## Installation
### Download bot repository
- **[Download](https://github.com/unknowableshade/quetza-bot/archive/refs/heads/master.zip)**
### Requirements
> :warning: **Read the docs!**
- **Install** [nodejs](https://nodejs.org/en/download/)
- **Install** required libraries:
```bash
$ npm install
```
- **Install** [ffmpeg](https://www.ffmpeg.org/download.html)
- **Install** [youtube-dl](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.12.17)
- **Place** youtube-dl in "/bin" directory.
### Bot startup
- **Configure** "config.ts" file.
- **Build**
```bash
npm run build
```
- **Start**
```bash
npm run start
```
## Licence
## Links
**[Licence](https://github.com/unknowableshade/5a0b-bot/blob/master/LICENSE)**
+2550 -321
View File
File diff suppressed because it is too large. Load diff
+19 -13
View File
@@ -1,39 +1,45 @@
{
"name": "5a0a-bot",
"name": "quetza-bot",
"engines": {
"node": "17.x.x",
"node": "16.14.x",
"npm": "*"
},
"version": "1.5.0",
"version": "2205.1",
"description": "Discord multifunctional bot",
"main": "dist/app.js",
"author": {
"name": "unknowableshade"
"name": "unknowableshade",
"email": "60275123+unknowableshade@users.noreply.github.com",
"url": "https://unknowableshade.ru"
},
"repository": {
"type": "git",
"url": "git+https://github.com/unknowableshade/5a0b-bot.git"
"url": "git+https://github.com/unknowableshade/quetza-bot.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/unknowableshade/5a0b-bot/issues"
"url": "https://github.com/unknowableshade/quetza-bot/issues"
},
"homepage": "https://github.com/unknowableshade/5a0b-bot#readme",
"readme": "https://github.com/unknowableshade/quetza-bot#readme",
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"build": "tsc --build",
"clean": "tsc --build --clean",
"start": "node dist/app.js"
"postinstall": "npm run build"
},
"dependencies": {
"@discordjs/opus": "^0.7.0",
"@discordjs/voice": "^0.8.0",
"discord-ytdl-core": "^5.0.4",
"discord.js": "^13.6.0",
"libsodium-wrappers": "^0.7.9",
"typescript": "^4.5.5",
"tweetnacl": "^1.0.3",
"ytdl-core": "^4.10.1"
},
"devDependencies": {
"@types/node": "^17.0.17"
"@types/node": "^17.0.17",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"eslint": "^8.16.0",
"typescript": "^4.5.5"
}
}
-8
View File
@@ -1,8 +0,0 @@
"use strict";
import { QuetzaClient } from "./assets/QuetzaClient";
import { quetzaConfig } from "./config";
const client = new QuetzaClient();
client.login(quetzaConfig.token);
-82
View File
@@ -1,82 +0,0 @@
import { ApplicationCommandData, Client as DiscordClient, Intents, CommandInteraction } from "discord.js";
import { existsSync, readdirSync } from "fs";
import { quetzaConfig } from "../config";
type commandMetadata = {
data: ApplicationCommandData,
run: (arg0: QuetzaClient, arg1: CommandInteraction) => Promise<void>
};
type eventMetadata = {
name: string,
run: (arg0: QuetzaClient, arg1: unknown[]) => Promise<void>
}
type moduleMetadata = {
data: {
key: string,
name: string,
description: string,
author: string
},
control: any
}
/**
* Extended client class for bot.
* @field commands Contains all bot commands.
* @field players Is a music feature.
*/
export class QuetzaClient extends DiscordClient {
private commands = new Map<string, commandMetadata>();
private modules: Record<string, moduleMetadata> = {};
get Commands(): Map<string, commandMetadata> {
return this.commands;
}
get Modules(): Record<string, moduleMetadata> {
return this.modules;
}
public constructor() {
super({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_MESSAGES,
Intents.FLAGS.GUILD_VOICE_STATES
]
});
quetzaConfig.modules.forEach((dir) => {
const moduleDir = quetzaConfig.rootDir + "modules/" + dir;
import(moduleDir + "qmodule.js")
.then((qmodule: moduleMetadata) => this.modules[qmodule.data.key] = qmodule);
if (existsSync(moduleDir + "commands")) {
const commandFiles = readdirSync(moduleDir + "commands").filter(file => file.endsWith(".js"));
for (const file of commandFiles) {
import(moduleDir + "commands/" + file)
.then((command: commandMetadata) => this.commands.set(command.data.name, command));
}
}
if (existsSync(moduleDir + "events")) {
const eventFiles = readdirSync(moduleDir + "events").filter(file => file.endsWith(".js"));
for (const file of eventFiles) {
import(moduleDir + "events/" + file).then((event: eventMetadata) => {
this.on(event.name, (...args: unknown[]) => {
event.run(this, args);
});
});
}
}
});
}
}
+23
View File
@@ -0,0 +1,23 @@
import Client from "../lib/Client";
import { CommandInteraction } from "discord.js";
import I8n from "../lib/I8n";
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);
if (!player) return;
player.clear();
await ctx.reply({ embeds: [I8n.en.cleared(player.Queue.Tracks.length)] });
}
const data = {
name: "clear",
description: "Clears queue.",
};
export { data };
+39
View File
@@ -0,0 +1,39 @@
import Client from "../lib/Client";
import { CommandInteraction, GuildMember } from "discord.js";
import I8n from "../lib/I8n";
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) ||
client.modules.music.set(ctx.guild, ctx.channel);
if (!player.Connection) {
const channel = (ctx.member as GuildMember).voice.channel;
if (channel) {
player.connect(channel);
await ctx.editReply({ embeds: [I8n.en.okConnected(channel.name)] });
} else {
client.modules.music.del(ctx.guild.id);
await ctx.editReply({ embeds: [I8n.en.notConnected()] });
return undefined;
}
} else {
await ctx.editReply({ embeds: [I8n.en.wasConnected()] });
}
return player;
}
const data = {
name: "connect",
description: "Connect to voice channel.",
};
export { data };
@@ -1,36 +1,19 @@
import { quetzaConfig } from "../../../config";
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
export async function run(client: Client, ctx: CommandInteraction) {
const filter = ctx.options.getString("filter");
if (ctx.guild === null)
return;
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player) return;
if (!player)
return;
player.setFilter(filter || undefined);
await ctx.deferReply();
if (await player.filter(filter)) {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(filter ? `${filter} filter is setted up.` : "Filters have been cleared.");
await ctx.editReply({ embeds: [embed] });
}
else {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`An error occurred while setting up the filter.`);
await ctx.editReply({ embeds: [embed] });
}
await ctx.reply({ embeds: [I8n.en.filtered(filter || undefined)] });
}
const data = {
@@ -133,11 +116,11 @@ const data = {
{
name: "earrape",
value: "earrape",
}
},
],
required: false
}
]
}
required: false,
},
],
};
export { data }
export { data };
+21
View File
@@ -0,0 +1,21 @@
import Client from "../lib/Client";
import { CommandInteraction } from "discord.js";
import I8n from "../lib/I8n";
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);
await ctx.editReply({ embeds: [I8n.en.playerInfo(player)] });
}
const data = {
name: "info",
description: "Information about the server's music player.",
};
export { data };
+41
View File
@@ -0,0 +1,41 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const pos = ctx.options.getInteger("position");
const query = ctx.options.getString("query");
if (!ctx.guild || !query || !pos || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
if (!player) return;
await ctx.deferReply();
const track = await player.add(query, ctx.user, pos - 1);
await ctx.editReply({ embeds: [I8n.en.appended(track)] });
}
const data = {
name: "insert",
description: "Insert new track to specific position.",
options: [
{
name: "query",
description: "Title or URL of the song.",
type: "STRING",
required: true,
},
{
name: "position",
description: "Position to insert.",
type: "INTEGER",
required: true,
},
],
};
export { data };
+32
View File
@@ -0,0 +1,32 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const query = ctx.options.getInteger("position");
if (!ctx.guild || !query || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
if (!player) return;
const state = player.jump(query - 1);
await ctx.reply({ embeds: [I8n.en.jumped(state)] });
}
const data = {
name: "jump",
description: "Jump to specific track in queue.",
options: [
{
name: "position",
description: "Position to jump to.",
type: "INTEGER",
required: true,
},
],
};
export { data };
+39
View File
@@ -0,0 +1,39 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import { loopOption } from "../lib/Types";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const query = ctx.options.getString("option");
if (!ctx.guild || !query || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
if (!player) return;
player.Queue.Loop = query as loopOption;
await ctx.reply({ embeds: [I8n.en.looped(query as loopOption)] });
}
const data = {
name: "loop",
description: "Set player loop option.",
options: [
{
name: "option",
description: "Update player loop option.",
type: "STRING",
required: true,
choices: [
{ name: "loop queue", value: "LOOP" },
{ name: "loop current track", value: "SONG" },
{ name: "Destroy player on end", value: "NONE" },
{ name: "Random order without suffling queue.", value: "AUTO" },
],
},
],
};
export { data };
+22
View File
@@ -0,0 +1,22 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
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);
if (!player) return;
player.skip();
await ctx.reply({ embeds: [I8n.en.skipped()] });
}
const data = {
name: "next",
description: "Skip current song.",
};
export { data };
+22
View File
@@ -0,0 +1,22 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
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);
if (!player) return;
const state = player.pause();
await ctx.reply({ embeds: [I8n.en.paused(state)] });
}
const data = {
name: "pause",
description: "Toggle pause.",
};
export { data };
+36
View File
@@ -0,0 +1,36 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import { run as connect } from "./connect";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const query = ctx.options.getString("query");
const player = await connect(client, ctx);
if (!player) return;
if (query) {
const track = await player.add(query, ctx.user);
await ctx.editReply({ embeds: [I8n.en.appended(track)] });
}
if (!player.Resource) {
player.play();
}
}
const data = {
name: "play",
description: "Play a song or add it to queue.",
options: [
{
name: "query",
description: "Title or URL of the song.",
type: "STRING"
},
],
};
export { data };
+83
View File
@@ -0,0 +1,83 @@
import {
ButtonInteraction,
CommandInteraction,
Message,
MessageActionRow,
MessageButton,
} from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
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);
if (!player) return;
await ctx.deferReply();
if (player.Queue.empty()) {
await ctx.editReply({ embeds: [I8n.en.queueEmpty()] });
return;
}
let page = 0;
const row = new MessageActionRow().addComponents(
new MessageButton()
.setCustomId("QueueTop")
.setLabel("🔼")
.setStyle("PRIMARY"),
new MessageButton()
.setCustomId("QueueUp")
.setLabel("🔺")
.setStyle("SECONDARY"),
new MessageButton()
.setCustomId("QueueDown")
.setLabel("🔻")
.setStyle("SECONDARY"),
new MessageButton()
.setCustomId("QueueEnd")
.setLabel("🔽")
.setStyle("PRIMARY")
);
const message = (await ctx.editReply({
embeds: [I8n.en.queueDesigner(player.Queue, 0, player.Resource)],
components: [row],
})) as Message;
const collector = message.createMessageComponentCollector({ time: 30000 });
collector.on("collect", async (btn: ButtonInteraction) => {
switch (btn.customId) {
case "QueueTop": {
page = 0;
break;
}
case "QueueUp": {
page = Math.max(page - 1, 0);
break;
}
case "QueueDown": {
page = Math.min(page + 1, Math.floor(player.Queue.Tracks.length / 11));
break;
}
case "QueueEnd":
page = Math.floor(player.Queue.Tracks.length / 11);
}
await btn.update({
embeds: [I8n.en.queueDesigner(player.Queue, page, player.Resource)],
});
});
}
const data = {
name: "queue",
description: "Send player queue.",
};
export { data };
+32
View File
@@ -0,0 +1,32 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const query = ctx.options.getInteger("position");
if (!ctx.guild || !query || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
if (!player) return;
const track = player.remove(query - 1);
await ctx.reply({ embeds: [I8n.en.removed(track)] });
}
const data = {
name: "remove",
description: "Remove specific track in queue.",
options: [
{
name: "position",
description: "Position to remove.",
type: "INTEGER",
required: true,
},
],
};
export { data };
+22
View File
@@ -0,0 +1,22 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
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);
if (!player) return;
player.shuffle();
await ctx.reply({ embeds: [I8n.en.reshuffle()] });
}
const data = {
name: "reshuffle",
description: "Shuffle the queue.",
};
export { data };
+50
View File
@@ -0,0 +1,50 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const hours = ctx.options.getInteger("hrs") || 0;
const minutes = ctx.options.getInteger("mins") || 0;
const seconds = ctx.options.getInteger("secs") || 0;
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
if (!player) return;
const time = hours * 3600 + minutes * 60 + seconds;
player.seek(time * 1000);
await ctx.reply({
embeds: [I8n.en.fastForwarded(time)],
});
}
const data = {
name: "seek",
description: "Fast-forward to timecode.",
options: [
{
name: "hrs",
description: "Hours to seek.",
type: "INTEGER",
required: false,
},
{
name: "mins",
description: "Minutes to seek.",
type: "INTEGER",
required: false,
},
{
name: "secs",
description: "Seconds to seek.",
type: "INTEGER",
required: false,
},
],
};
export { data };
+22
View File
@@ -0,0 +1,22 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
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);
if (!player) return;
player.destroy();
await ctx.reply({ embeds: [I8n.en.destroyed()] });
}
const data = {
name: "stop",
description: "Destroys player.",
};
export { data };
+32
View File
@@ -0,0 +1,32 @@
import { CommandInteraction } from "discord.js";
import Client from "../lib/Client";
import I8n from "../lib/I8n";
export async function run(client: Client, ctx: CommandInteraction) {
const volume = ctx.options.getInteger("volume") || 100;
if (!ctx.guild || !ctx.channel) return;
const player = client.modules.music.get(ctx.guild.id, ctx.channel);
if (!player) return;
player.Volume = volume;
await ctx.reply({ embeds: [I8n.en.volumeSet(player.Volume)] });
}
const data = {
name: "volume",
description: "Set music volume.",
options: [
{
name: "volume",
description: "Volume in percents.",
type: "INTEGER",
required: true,
},
],
};
export { data };
+9 -20
View File
@@ -1,21 +1,10 @@
/**
* Bot configuration file.
* @field opId Contains bot author Discord ID for moderating on go.
* @field token Discord API token.
* @field color Bot color theme for Embeds.
* @field rootDir Directory that goes before commands and events.
* @field modules Directories for bot's modules.
*/
const quetzaConfig = {
opId: "214422162906415106",
token: process.env.DISCORD_TOKEN || "NzgzNDQwMjU3NjE3MzYyOTQ1.X8axmQ.wu37vNARAr-NlexYIXL5zIxrHUM",
color: "#C4A4BE",
rootDir: __dirname + "/",
modules: [
"quetza/",
"music/",
"codeshr/"
]
}
import path from "path";
export {quetzaConfig}
const config = {
token: process.env.DISCORD_TOKEN,
commands: path.join(__dirname, "/commands/"),
events: path.join(__dirname, "/events/"),
binaries: path.join(__dirname, "/../bin/")
};
export default config;
+14
View File
@@ -0,0 +1,14 @@
import { Interaction } from "discord.js";
import Client from "../lib/Client";
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";
+21
View File
@@ -0,0 +1,21 @@
import Client from "../lib/Client";
import { generateDependencyReport } from "@discordjs/voice";
export async function run(client: Client): Promise<void> {
if (!client.user || !client.application) {
console.log("No user or application is provided!");
process.exit(1);
}
console.log(`${client.user.username} logged in as ${client.user.tag}!`);
console.log(generateDependencyReport());
client.user.setActivity("over you.", { type: "WATCHING" });
const restCommands = Array.from(client.Commands.values()).map(
value => value.data
);
client.application.commands.set(restCommands);
}
export const name = "ready";
+24
View File
@@ -0,0 +1,24 @@
import { VoiceState } from "discord.js";
import I8n from "../lib/I8n";
import Client from "../lib/Client";
import { asleep } from "../lib/Misc";
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: [I8n.en.fullyAlone()] });
player.destroy();
}
}
}
export const name = "voiceStateUpdate";
+6
View File
@@ -0,0 +1,6 @@
import Client from "./lib/Client";
import config from "./config";
const client = new Client();
client.login(config.token);
+57
View File
@@ -0,0 +1,57 @@
import {
Client as DiscordClient,
Intents,
} from "discord.js";
import { existsSync, readdirSync } from "fs";
import path from "path";
import Music from "./Music";
import { command, event } from "./Types";
import config from "../config";
export default class Client extends DiscordClient {
private commands = new Map<string, command>();
get Commands(): Map<string, command> {
return this.commands;
}
public readonly modules = {
music: new Music(),
};
public constructor() {
super({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_MESSAGES,
Intents.FLAGS.GUILD_VOICE_STATES,
],
});
if (existsSync(config.commands)) {
const files = readdirSync(config.commands).filter((file) =>
file.endsWith(".js")
);
for (const file of files) {
import(path.join(config.commands, file)).then((cmd: command) =>
this.commands.set(cmd.data.name, cmd)
);
}
}
if (existsSync(config.events)) {
const files = readdirSync(config.events).filter((file) =>
file.endsWith(".js")
);
for (const file of files) {
import(path.join(config.events, file)).then((evnt: event) => {
this.on(evnt.name, (...args: unknown[]) =>
evnt.run(this, args)
);
});
}
}
}
}
+77
View File
@@ -0,0 +1,77 @@
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;
}
}
+317
View File
@@ -0,0 +1,317 @@
import { MessageEmbed } from "discord.js";
import Player from "./Player";
import { loopOption, track } from "./Types";
import { loopMoji, secToISO, statusBarGenerator, volumeMoji } from "./Misc";
import Queue from "./Queue";
import { AudioResource } from "@discordjs/voice";
export default class I8n {
public static en = {
nowPlaying: (track: track) => {
return new MessageEmbed()
.setColor("LUMINOUS_VIVID_PINK")
.setAuthor({
iconURL: track.requester.avatarURL() || undefined,
name: track.requester.tag,
})
.setTitle(track.title)
.setURL(track.url)
.setDescription("**playing now.**")
.setThumbnail(track.thumbnail);
},
cleared: (amount: number) => {
return new MessageEmbed()
.setColor("GREEN")
.setTitle(
`**♻️ ${amount} tracks have been cleared from the queue.**`
)
.setTimestamp(new Date());
},
okConnected: (channel: string) => {
return new MessageEmbed()
.setColor("GREEN")
.setTitle(`**✅ Successfuly connected to:** _${channel}_.`);
},
notConnected: () => {
return new MessageEmbed()
.setColor("RED")
.setTitle(
"**🚫 Make yourself connected to a voice channel beforehand.**"
);
},
wasConnected: () => {
return new MessageEmbed()
.setColor("YELLOW")
.setTitle("**⚠️ Already connected to a voice channel.**");
},
filtered: (filter?: string) => {
return new MessageEmbed()
.setColor("GREEN")
.setTitle(`**✅ ${filter} was applied to the player.**`);
},
appended: (track?: track) => {
return !track
? new MessageEmbed()
.setColor("RED")
.setTitle("**🚫 Could not add track to the queue.**")
: new MessageEmbed()
.setColor("GREEN")
.setTitle(track.title)
.setURL(track.url)
.setThumbnail(track.thumbnail)
.setAuthor({
iconURL: track.requester.avatarURL() || undefined,
name: track.requester.tag,
})
.setDescription("**has been added to the queue.**")
.setTimestamp(new Date());
},
jumped: (state: boolean) => {
return state
? new MessageEmbed()
.setColor("GREEN")
.setTitle("_**Perfoming jump.**_")
: new MessageEmbed()
.setColor("RED")
.setTitle("**❗ Bad _jump_ request.**");
},
looped: (query: loopOption) => {
switch (query) {
case "AUTO": {
return new MessageEmbed()
.setColor("LUMINOUS_VIVID_PINK")
.setTitle(
"**🔀 Shuffle mode** _(does not affect the queue order)_."
);
}
case "LOOP": {
return new MessageEmbed()
.setColor("BLUE")
.setTitle("**🔁 Looping over the queue.**");
}
case "NONE": {
return new MessageEmbed()
.setColor("RED")
.setTitle("**⤵️ Player will stop on the queue end.**");
}
case "SONG": {
return new MessageEmbed()
.setColor("YELLOW")
.setTitle("**🔂 Looping a single track.**");
}
}
},
skipped: () => {
return new MessageEmbed()
.setColor("GREEN")
.setTitle("**⏭️ Skipping.**");
},
paused: (state: boolean) => {
return state
? new MessageEmbed()
.setColor("GREEN")
.setTitle("_**▶️ Resumed.**_")
: new MessageEmbed()
.setColor("YELLOW")
.setTitle("_**⏸️ Paused.**_");
},
removed: (track?: track) => {
return track
? new MessageEmbed()
.setColor("GREEN")
.setTitle(track.title)
.setDescription("**was removed from the queue.**")
.setTimestamp(new Date())
.setURL(track.url)
.setThumbnail(track.thumbnail)
: new MessageEmbed()
.setColor("RED")
.setTitle("**❗ Bad _remove_ request.**");
},
fastForwarded: (amount: number) => {
return new MessageEmbed()
.setColor("BLUE")
.setTitle(
`**⏩ Fast-forwarding to ${secToISO(
amount
)}**`
);
},
destroyed: () => {
return new MessageEmbed()
.setColor("RED")
.setTitle("**💀 Destroying the player.**");
},
volumeSet: (amount: number) => {
return new MessageEmbed()
.setColor("GREEN")
.setTitle(`**${volumeMoji(amount)} Volume has been set to ${amount}%**`);
},
queueEmpty: () => {
return new MessageEmbed()
.setColor("YELLOW")
.setTitle("**❎ Queue is empty.**");
},
playerInfo: (player?: Player) => {
if (!player)
return new MessageEmbed()
.setColor("RED")
.setTitle("**🚫 Player does not exist.**");
if (player.Resource) {
return new MessageEmbed()
.setColor("LUMINOUS_VIVID_PINK")
.setTitle(
`${player.Queue.Position}. ${player.Resource.metadata.title}`
)
.setDescription(
statusBarGenerator(
player.Resource.playbackDuration / 1000,
player.Resource.metadata.duration
)
)
.setThumbnail(player.Resource.metadata.thumbnail)
.setURL(player.Resource.metadata.url)
.addFields([
{
name: "Applied Filters",
value:
player.Filter.Filters.map(
(value) => value[0]
).join("\n") + "\ndefault",
inline: true,
},
{
name: "Queue Length",
value: `🔢 ${player.Queue.Tracks.length}`,
inline: true,
},
{
name: "Queue Duration",
value: `💿 ${secToISO(player.Queue.Duration)}`,
inline: true,
},
{
name: "Queue Looping",
value: `${loopMoji(player.Queue.Loop)} ${player.Queue.Loop}`,
inline: true,
},
{
name: "Volume amount",
value: `${volumeMoji(player.Volume)} ${player.Volume}/150`,
inline: true,
},
{
name: "Player lifetime",
value: `🕓 ${secToISO((Date.now() - player.Created.getTime()) / 1000)}`,
inline: true,
},
]);
} else {
return new MessageEmbed()
.setColor("BLUE")
.setTitle("💿 Insert the disk...")
.setDescription(statusBarGenerator(0, 0))
.setURL(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley"
)
.addFields([
{
name: "Applied Filters",
value:
player.Filter.Filters.map(
(value) => value[0]
).join("\n") + "\ndefault",
inline: true,
},
{
name: "Queue Length",
value: `🔢 ${player.Queue.Tracks.length}`,
inline: true,
},
{
name: "Queue Duration",
value: `💿 ${secToISO(player.Queue.Duration)}`,
inline: true,
},
{
name: "Queue Looping",
value: `${loopMoji(player.Queue.Loop)} ${player.Queue.Loop}`,
inline: true,
},
{
name: "Volume amount",
value: `${volumeMoji(player.Volume)} ${player.Volume}/150`,
inline: true,
},
{
name: "Player lifetime",
value: `🕓 ${secToISO((Date.now() - player.Created.getTime()) / 1000)}`,
inline: true,
},
]);
}
},
queueDesigner: (
queue: Queue,
page: number,
resource: AudioResource<track> | undefined
) => {
page = Math.max(
0,
Math.min(Math.floor(queue.Tracks.length / 11), page)
);
return new MessageEmbed()
.setColor("LUMINOUS_VIVID_PINK")
.setTitle(
resource
? `${queue.Position + 1}. ${resource.metadata.title}`
: "💿 Insert the disk..."
)
.setURL(
resource
? resource.metadata.url
: "https://www.youtube.com/watch?v=5mGuCdlCcNM&ab_channel=Ra%C3%BAlBlanco"
)
.setThumbnail(
resource
? resource.metadata.thumbnail
: "https/img.youtube.com/vi/5mGuCdlCcNM/default.jpg"
)
.setDescription(
resource
? statusBarGenerator(
resource.playbackDuration / 1000,
resource.metadata.duration
)
: statusBarGenerator(0, 0)
)
.setFields(
queue.Tracks.slice(page * 10, page * 10 + 10).map(
(value, index) => {
return {
name: `${
page * 10 + index + 1
}. ${value.title.padEnd(35)}`,
value: `**[${secToISO(value.duration)}]**`,
};
}
)
);
},
fullyAlone: () => {
return new MessageEmbed()
.setColor("LUMINOUS_VIVID_PINK")
.setTitle("🥬 I have decided to leave to conserve my energy.")
.setDescription(
"_**Since there was nobody in the voice channel.**_"
);
},
reshuffle: () => {
return new MessageEmbed()
.setColor("GREEN")
.setTitle("✅ Successfuly reshuffled queue.");
}
};
}
+34
View File
@@ -0,0 +1,34 @@
import { loopOption } from "./Types";
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>(15 - Math.floor(15 * duration / (length === 0 ? 1 : length))).fill(":white_small_square:");
return `**[${filled.join("")}${empty.join("")}] ${secToISO(duration)}/${secToISO(length)}**`;
}
export function secToISO(amount: number): string {
const h = Math.floor(amount / 3600);
const m = Math.floor(amount % 3600 / 60);
const s = Math.floor(amount % 3600 % 60);
const hDisplay = h > 0 ? `${h}:` : "";
const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`;
const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`;
return hDisplay + mDisplay + sDisplay;
}
export async function asleep(amount: number): Promise<unknown> {
return new Promise(r => setTimeout(r, amount));
}
export function volumeMoji(amount: number): string {
return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇";
}
export function loopMoji(loop: loopOption): string {
return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂";
}
+25
View File
@@ -0,0 +1,25 @@
import { Guild, TextBasedChannel } from "discord.js";
import Player from "./Player";
export default class Music {
private players = new Map<string, Player>();
public get(guildId: string, channel?: TextBasedChannel): Player | undefined {
const player = this.players.get(guildId);
if (player) {
if (channel) player.Channel = channel;
return player;
}
}
public set(guild: Guild, channel: TextBasedChannel): Player {
const player = new Player(guild, this, channel);
this.players.set(guild.id, player);
return player;
}
public del(guildId: string): void {
this.players.delete(guildId);
}
}
+305
View File
@@ -0,0 +1,305 @@
import {
VoiceConnection,
AudioPlayer,
createAudioResource,
AudioPlayerStatus,
joinVoiceChannel,
createAudioPlayer,
AudioResource,
DiscordGatewayAdapterCreator,
VoiceConnectionStatus,
entersState,
} from "@discordjs/voice";
import {
Guild,
Message,
TextBasedChannel,
User,
VoiceBasedChannel,
} from "discord.js";
import Music from "./Music";
import Filter from "./Filter";
import Queue from "./Queue";
import trackDL from "./TrackDl";
import I8n from "./I8n";
import ytdl from "discord-ytdl-core";
import { track } from "./Types";
export default class Player {
private guild: Guild;
private music: Music;
private channel: TextBasedChannel;
public get Channel(): TextBasedChannel {
return this.channel;
}
public set Channel(x: TextBasedChannel) {
this.channel = 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 created: Date = new Date();
public get Created(): Date {
return this.created;
}
private volume = 100;
public get Volume(): number {
return this.volume;
}
public set Volume(x: number) {
this.volume = Math.max(0, Math.min(150, x));
if (this.resource && this.resource.volume) this.resource.volume.setVolumeLogarithmic(this.volume / 100);
}
public constructor(
guild: Guild,
music: Music,
textBasedChannel: TextBasedChannel
) {
this.music = music;
this.guild = guild;
this.channel = textBasedChannel;
this.player = createAudioPlayer();
this.player.on(AudioPlayerStatus.Idle, async () =>
this.resourceEndResolvable()
);
}
private resourceGenerator(track: track, seek?: number) {
const stream = ytdl(track.url, {
opusEncoded: false,
fmt: "mp3",
filter: "audioonly",
highWaterMark: 1 << 25,
seek: seek ? seek / 1000 : undefined,
encoderArgs: !this.filter.empty()
? ["-af", this.filter.toString()]
: undefined,
});
this.resource = createAudioResource(stream, {
inlineVolume: true,
metadata: track,
});
if (seek) this.resource.playbackDuration = seek;
if (this.resource.volume) this.resource.volume.setVolumeLogarithmic(this.volume / 100);
return this.resource;
}
private resourceEndResolvable(): void {
if (this.resource) this.resource.encoder?.destroy();
if (this.queue.next()) {
this.play();
}
else {
this.resource = undefined;
this.player.stop();
}
}
private async messageResolvable(track: track) {
if (this.message) await this.message.delete();
this.message = await this.channel.send({
embeds: [I8n.en.nowPlaying(track)],
});
}
private async search(
query: string,
requester: User
): Promise<track[] | undefined> {
const response = await trackDL(query, {
dumpSingleJson: true,
noWarnings: true,
noCheckCertificate: true,
flatPlaylist: true,
skipDownload: true,
defaultSearch: "ytsearch",
});
if (!response) return undefined;
const tracks: track[] = [];
if (response.entries) {
for (const entry of response.entries) {
tracks.push({
url: `https://youtube.com/watch?v=${entry.id}`,
title: entry.title,
thumbnail: `https://img.youtube.com/vi/${entry.id}/default.jpg`,
duration: entry.duration,
requester: requester,
});
}
} else {
tracks.push({
url: `https://youtube.com/watch?v=${response.id}`,
title: response.title,
thumbnail: `https://img.youtube.com/vi/${response.id}/default.jpg`,
duration: response.duration,
requester: requester,
});
}
return tracks;
}
public play(seek?: number): void {
const track = this.queue.current();
if (!this.connection || !track) return;
this.player.play(this.resourceGenerator(track, seek));
this.connection.subscribe(this.player);
this.messageResolvable(track);
}
public connect(channel: VoiceBasedChannel): void {
if (this.connection) this.connection.destroy();
this.connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guildId,
adapterCreator: channel.guild
.voiceAdapterCreator as DiscordGatewayAdapterCreator,
});
this.connection.on(
VoiceConnectionStatus.Disconnected,
async () => {
try {
await Promise.race([
entersState(
this.connection as VoiceConnection,
VoiceConnectionStatus.Signalling,
10_000
),
entersState(
this.connection as VoiceConnection,
VoiceConnectionStatus.Connecting,
10_000
),
]);
} catch (error) {
this.destroy();
}
}
);
}
public async add(
query: string,
requester: User,
index?: number
): Promise<track | undefined> {
const tracks = await this.search(query, requester);
if (!tracks) return;
this.queue.push(tracks, index);
return tracks[0];
}
public jump(query: number): boolean {
if (this.queue.jump(query)) {
this.play();
return true;
}
return false;
}
public remove(query: number): track | undefined {
const oldPosition = this.queue.Position;
const track = this.queue.pop(query);
if (track) {
if (oldPosition === query) this.play();
return track;
}
return undefined;
}
public seek(time: number): void {
this.play(time);
}
public clear(): void {
this.queue.clear();
this.resource = undefined;
if (this.player) this.player.stop();
}
public destroy(): void {
if (this.connection) this.connection.destroy();
this.player.stop();
this.music.del(this.guild.id);
}
public pause(): boolean {
if (this.player.state.status === AudioPlayerStatus.Paused) {
this.player.unpause();
return false;
} else {
this.player.pause();
return true;
}
}
public shuffle(): void {
this.queue.shuffle();
if (this.resource) {
this.player.stop();
this.play();
}
}
public skip(): void {
this.player.stop();
}
public setFilter(filter?: string): void {
this.filter.toggle(filter);
if (this.resource) this.play(this.resource.playbackDuration);
}
}
+112
View File
@@ -0,0 +1,112 @@
import { loopOption, track } from "./Types";
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;
}
}
+52
View File
@@ -0,0 +1,52 @@
import { ytdlArgs, ytdlResponse } from "./Types";
import { execFile } from "child_process";
import { promisify } from "util";
import config from "../config";
import path from "path";
const execFileAsync = promisify(execFile);
function argumentsResolver(args: ytdlArgs): string[] {
const options = Object.entries(args);
return options.map(([key, value]) => {
if (typeof value === "boolean" && value) {
return `--${key.split(/(?=[A-Z])/).join("-")}`.toLowerCase();
} else {
return `--${key
.split(/(?=[A-Z])/)
.join("-")}=${value}`.toLowerCase();
}
});
}
async function ytdlExec(
query: string,
args: ytdlArgs
): Promise<ytdlResponse | undefined> {
try {
const executable = await execFileAsync(
path.join(config.binaries, "/youtube-dl"),
[`${query}`].concat(argumentsResolver(args))
);
return JSON.parse(executable.stdout);
} catch (error) {
console.log(error);
return undefined;
}
}
export default async function trackDL(
query: string,
args: ytdlArgs
): Promise<ytdlResponse | undefined> {
let response = await ytdlExec(query, args);
if (response && !response.extractor.includes("youtube")) {
response = await ytdlExec(
`${response.title} - ${response.creator}`,
args
);
}
return response;
}
+272
View File
@@ -0,0 +1,272 @@
import { ApplicationCommandData, User, CommandInteraction } from "discord.js";
import Client from "./Client";
export type track = {
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;
run: (arg0: Client, arg1: unknown[]) => Promise<void>;
};
export type loopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
export type 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;
};
type 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;
};
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[];
};
-48
View File
@@ -1,48 +0,0 @@
import { quetzaConfig } from "../../../config";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { ColorResolvable, CommandInteraction, ContextMenuInteraction, MessageEmbed } from "discord.js";
import fetch from "node-fetch";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const content = ctx.options.getString("content");
if (!content) {
return;
}
await ctx.deferReply();
try {
const response: {id: string} = await fetch("https://unknowableshade.ru/api/share", { method: "post", body: JSON.stringify({content: content, lang: "plain text", isBigId: false}), headers: {'Content-Type': 'application/json'} }).then(res => res.json());
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`https://unknowableshade.ru/csh/${response.id}`)
.setURL(`https://unknowableshade.ru/csh/${response.id}`);
await ctx.editReply({ embeds: [embed] });
}
catch {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Unable to maintain this request.");
ctx.editReply({embeds: [embed]});
}
}
const data = {
name: "csh",
description: "Create shareable text note.",
options: [
{
name: "content",
description: "Text to share.",
type: "STRING",
required: true
}
]
}
export { data }
-7
View File
@@ -1,7 +0,0 @@
const data = {
key: "codeshr",
name: "CodeShr",
description: "CodeShr module v.1.0.0",
author: "unknowableshade"
}
export {data};
-50
View File
@@ -1,50 +0,0 @@
import { AudioResource } from "@discordjs/voice";
import { ColorResolvable, MessageEmbed } from "discord.js";
import { quetzaConfig } from "../../../config";
import { track } from "./QuetzaMusic/Types";
export function randomShuffle(array: Array<any>) {
let currentIndex = array.length;
while (currentIndex != 0) {
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
}
export function queueDesigner(queue: track[], page: number, nowPlaying: AudioResource<track> | undefined, nowPlayingPos: number): MessageEmbed {
let response = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Server queue:");
if (nowPlaying) {
const hDuration = `${Math.floor(nowPlaying.metadata.duration / 60 / 10)}${Math.floor(nowPlaying.metadata.duration / 60) % 10}`;
const mDuration = `${Math.floor(nowPlaying.metadata.duration % 60 / 10)}${nowPlaying.metadata.duration % 60 % 10}`;
const hPlayed = `${Math.floor(nowPlaying.playbackDuration / 1000 / 60 / 10)}${Math.floor(nowPlaying.playbackDuration / 1000 / 60) % 10}`;
const mPlayed = `${Math.floor(Math.floor(nowPlaying.playbackDuration / 1000) % 60 / 10)}${Math.floor(nowPlaying.playbackDuration / 1000) % 60 % 10}`;
response.setTitle(`${nowPlayingPos + 1}. ${nowPlaying.metadata.title}`)
.setURL(nowPlaying.metadata.url)
.setThumbnail(nowPlaying.metadata.thumbnail)
.setDescription(`${hPlayed}:${mPlayed} / ${hDuration}:${mDuration}`);
}
for (let i = page * 10; i < Math.min(queue.length, (page + 1) * 10); i++) {
const track = queue[i];
const h = `${Math.floor(track.duration / 60 / 10)}${Math.floor(track.duration / 60) % 10}`;
const m = `${Math.floor(track.duration % 60 / 10)}${track.duration % 60 % 10}`;
response.addField(`${i + 1}. ${track.title.slice(0, 35)}`, `${h}:${m}\n`, true);
}
return response;
}
export async function asleep(ms: number): Promise<unknown> {
return new Promise(r => setTimeout(r, ms));
}
@@ -1,39 +0,0 @@
import { ytdlArgs, ytdlResponse } from "./Types"
import { execFile } from "child_process"
import { promisify } from "util";
import { quetzaConfig } from "../../../../config";
const execFileAsync = promisify(execFile);
function argumentsResolver(args: ytdlArgs): string[] {
const options = Object.entries(args);
return options.map(([key, value]) => {
if (typeof value === "boolean" && value) {
return `--${key.split(/(?=[A-Z])/).join("-")}`.toLowerCase();
}
else {
return `--${key.split(/(?=[A-Z])/).join("-")}=${value}`.toLowerCase();
}
});
}
async function ytdlExec(query: string, args: ytdlArgs): Promise<ytdlResponse | undefined> {
try {
const executable = await execFileAsync(quetzaConfig.rootDir + "../bin/youtube-dl", [`${query}`].concat(argumentsResolver(args)));
return JSON.parse(executable.stdout);
}
catch (error) {
console.log(error);
return undefined;
}
}
export async function searchMusic(query: string, args: ytdlArgs): Promise<ytdlResponse | undefined> {
let response = await ytdlExec(query, args);
if (response && !response.extractor.includes("youtube")) {
response = await ytdlExec(`${response.title} - ${response.creator}`, args);
}
return response;
}
-259
View File
@@ -1,259 +0,0 @@
import { User } from "discord.js";
export interface track {
url: string;
title: string;
thumbnail: string;
duration: number;
requester: User;
}
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[]
}
@@ -1,67 +0,0 @@
export class Filter {
public 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 activeFilters: Map<string, string> = new Map();
public get ActiveFilters(): string[] {
return Array.from(this.activeFilters.keys());
}
public toggleFilter(filter: string | null): boolean {
if (!filter) {
this.activeFilters.clear();
return true;
}
const arg = this.filters[filter];
if (arg) {
this.activeFilters.has(filter) ? this.activeFilters.delete(filter) : this.activeFilters.set(filter, arg);
return true;
}
return false;
}
public empty(): boolean {
return this.activeFilters.size == 0;
}
public toString(): string {
return Array.from(this.activeFilters.values()).join(",");
}
}
@@ -1,374 +0,0 @@
import {
VoiceConnection, AudioPlayer, createAudioResource,
AudioPlayerStatus, joinVoiceChannel, createAudioPlayer,
AudioResource, DiscordGatewayAdapterCreator, VoiceConnectionStatus,
entersState
} from "@discordjs/voice";
import {
ColorResolvable, Guild, Message,
MessageEmbed, StageChannel, TextChannel, User,
VoiceChannel
} from "discord.js";
import ytdl from "discord-ytdl-core";
import { track, loopOption } from "./Types";
import { randomShuffle } from "../Misc";
import { searchMusic } from "../Music-DL/SearchMusic";
import { Filter } from "./Filter";
import { quetzaConfig } from "../../../../config";
export class Player {
private guild: Guild;
private music: Music;
private channel: TextChannel;
public get Channel(): TextChannel {
return this.channel;
}
private connection: VoiceConnection | undefined;
public get Connection(): VoiceConnection | undefined {
return this.connection;
}
private player: AudioPlayer | undefined;
public get Player(): AudioPlayer | undefined {
return this.player;
}
private queue: track[] = [];
public get Queue(): track[] {
return this.queue;
}
public set Queue(queue: track[]) {
this.queue = queue;
}
private nowPlayingPos: number;
public get NowPlayingPos(): number {
return this.nowPlayingPos;
}
private nowPlaying: AudioResource<track> | undefined;
public get NowPlaying(): AudioResource<track> | undefined {
return this.nowPlaying;
}
private filters: Filter;
public get Filters(): string[] {
return this.filters.ActiveFilters;
}
private repeatMode: loopOption = "LOOP";
private message: Message | undefined;
public constructor(playerGuild: Guild, clientMusic: Music, ctxChannel: TextChannel) {
this.music = clientMusic;
this.guild = playerGuild;
this.channel = ctxChannel;
this.filters = new Filter();
this.nowPlayingPos = 0;
}
private async playerCreator(track: track, seek: number): Promise<AudioPlayer> {
const player = createAudioPlayer();
const stream = ytdl(track.url, {
opusEncoded: true,
filter: "audioonly",
highWaterMark: 1 << 25,
seek: seek,
encoderArgs: !this.filters.empty() ? ["-af", this.filters.toString()] : []
});
this.nowPlaying = createAudioResource(stream, {
inlineVolume: true,
metadata: track
});
player.play(this.nowPlaying);
return player;
}
private resourceEndResolvable(): boolean {
if (this.nowPlaying)
this.nowPlaying.encoder?.destroy();
switch (this.repeatMode) {
case "NONE": {
if (++this.nowPlayingPos >= this.queue.length) {
this.nowPlayingPos = 0;
this.nowPlaying = undefined;
return false;
}
return true;
}
case "LOOP": {
this.nowPlayingPos = (this.nowPlayingPos + 1) % this.queue.length;
return true;
}
case "SONG": return true;
}
return true;
}
private async messageResolvable(track: track): Promise<void> {
if (this.message) {
this.channel = this.message.channel as TextChannel;
try {
await this.message.delete();
} catch (err) {
console.log(err);
}
}
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(track.title)
.setURL(track.url)
.setThumbnail(track.thumbnail)
.setDescription("Is now beeing played.");
embed.setAuthor({ name: `${track.requester.tag}`, iconURL: track.requester.avatarURL() || track.requester.avatarURL as unknown as string});
this.message = await this.channel.send({ embeds: [embed] });
}
private async searchStream(query: string, requester: User): Promise<track[] | undefined> {
const response = await searchMusic(query, {
dumpSingleJson: true,
noWarnings: true,
noCheckCertificate: true,
flatPlaylist: true,
skipDownload: true,
defaultSearch: "ytsearch"
});
if (!response)
return undefined;
let tracks: track[] = [];
if (response.entries) {
for (const entry of response.entries) {
tracks.push({
url: `https://youtube.com/watch?v=${entry.id}`,
title: entry.title,
thumbnail: `https://img.youtube.com/vi/${entry.id}/default.jpg`,
duration: entry.duration,
requester: requester
});
}
}
else {
tracks.push({
url: `https://youtube.com/watch?v=${response.id}`,
title: response.title,
thumbnail: `https://img.youtube.com/vi/${response.id}/default.jpg`,
duration: response.duration,
requester: requester
});
}
return tracks;
}
public async play(seek = 0): Promise<void> {
if (!this.connection || this.queue.length === 0)
return;
if (this.nowPlaying)
this.nowPlaying.encoder?.destroy();
const track = this.queue[this.nowPlayingPos];
this.player = await this.playerCreator(track, seek);
this.connection.subscribe(this.player);
this.player.on(AudioPlayerStatus.Idle, async () => {
if (this.resourceEndResolvable())
await this.play();
});
await this.messageResolvable(track);
}
public connect(channel: VoiceChannel | StageChannel): void {
this.connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guildId,
adapterCreator: channel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator
});
this.connection.on(VoiceConnectionStatus.Disconnected, async (oldState, newState) => {
try {
await Promise.race([
entersState(this.connection as VoiceConnection, VoiceConnectionStatus.Signalling, 10_000),
entersState(this.connection as VoiceConnection, VoiceConnectionStatus.Connecting, 10_000),
]);
}
catch (error) {
this.destroy();
}
});
}
public async addTrack(query: string, requester: User, index?: number): Promise<track | undefined> {
const metadata = await this.searchStream(query, requester);
if (!metadata) {
return undefined;
}
if (!index || index >= this.queue.length) {
this.queue = this.queue.concat(metadata);
}
else {
this.queue.splice(Math.max(0, index), 0, ...metadata)
if (this.nowPlayingPos >= index)
this.nowPlayingPos += metadata.length;
}
return metadata[0];
}
public clear() {
this.queue = [];
this.nowPlayingPos = 0;
this.nowPlaying = undefined;
if (this.player)
this.player.stop();
}
public destroy() {
if (this.connection)
this.connection.destroy();
if (this.nowPlaying)
this.nowPlaying.encoder?.destroy;
this.music.delPlayer(this.guild.id);
}
public async jump(query: number | string): Promise<boolean> {
if (typeof query === "number" && query >= 0 && query < this.queue.length) {
this.nowPlayingPos = query;
await this.play();
return true;
}
else {
for (let i = 0; i < this.queue.length; i++) {
const track = this.queue[i];
if (track.title.toLowerCase().includes(String(query).toLowerCase())) {
this.nowPlayingPos = i;
await this.play();
return true;
}
}
}
return false;
}
public async remove(query: number | string): Promise<boolean> {
if (typeof query === "number" && query >= 0 && query < this.queue.length) {
this.queue.splice(query, 1);
if (query === this.nowPlayingPos)
await this.play();
else if (query >= this.nowPlayingPos)
this.nowPlayingPos--;
return true;
}
else {
for (let i = 0; i < this.queue.length; i++) {
const track = this.queue[i];
if (track.title.toLowerCase().includes(String(query).toLowerCase())) {
this.queue.splice(i, 1);
if (i === this.nowPlayingPos)
await this.play();
else if (i >= this.nowPlayingPos)
this.nowPlayingPos--;
return true;
}
}
}
return false;
}
public async seek(time: number): Promise<void> {
await this.play(time);
}
public pause(): boolean {
if (!this.player)
return false;
if (this.player.state.status === AudioPlayerStatus.Paused) {
this.player.unpause();
return false;
}
else {
this.player.pause();
return true;
}
}
public repeat(mode: loopOption): void {
this.repeatMode = mode;
}
public volume(amount: number): boolean {
if (amount > 0 && amount < Infinity && this.nowPlaying && this.nowPlaying.volume) {
this.nowPlaying.volume.setVolumeLogarithmic(amount / 100);
return true;
}
return false;
}
public reshuffle(): void {
this.queue = randomShuffle(this.queue);
}
public skip(): void {
if (this.player)
this.player.stop();
}
public async filter(filter: string | null): Promise<boolean> {
if (this.filters.toggleFilter(filter)) {
await this.play(this.nowPlaying?.playbackDuration);
return true;
}
return false;
}
}
export class Music {
private players = new Map<string, Player>();
public getPlayer(guildId: string): Player | undefined {
const player = this.players.get(guildId);
if (player)
return player;
}
public genPlayer(guild: Guild, music: Music, channel: TextChannel): Player {
const player = this.players.get(guild.id);
if (!player) {
const newPlayer = new module.exports.Player(guild, music, channel);
this.players.set(guild.id, newPlayer);
return newPlayer;
}
else {
return player;
}
}
public delPlayer(guildId: string) {
this.players.delete(guildId);
}
}
@@ -1,44 +0,0 @@
import { readFile, readFileSync, writeFile, writeFileSync } from "fs";
import { saveEntry, track } from "./Types";
export class QueueStorage {
private data: Map<string, saveEntry>;
public get Data() {
return this.data;
}
public constructor(path: string) {
try {
this.data = new Map(Object.entries(JSON.parse(readFileSync(path).toString("utf-8"))));
}
catch {
this.data = new Map<string, saveEntry>();
}
}
public createEntry(queue: track[], title: string, description: string, requester: string): void {
this.data.set(title, {
owner: requester,
description: description,
tracks: queue
});
}
public removeEntry(title: string): void {
this.data.delete(title);
}
public getEntry(title: string): saveEntry | undefined {
return this.data.get(title);
}
public saveData(path: string): boolean {
try {
writeFileSync(path, JSON.stringify(Object.fromEntries(this.data)));
return true;
}
catch {
return false;
}
}
}
@@ -1,17 +0,0 @@
import { User } from "discord.js";
export interface track {
url: string;
title: string;
thumbnail: string;
duration: number;
requester: User;
}
export interface saveEntry {
description: string,
owner: string,
tracks: track[]
}
export type loopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
-31
View File
@@ -1,31 +0,0 @@
import { quetzaConfig } from "../../../config";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
await ctx.deferReply();
if (ctx.guild === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (player) {
player.clear();
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Queue has been cleared.");
await ctx.editReply({ embeds: [embed] });
}
}
const data = {
name: "clear",
description: "Clears queue."
}
export { data }
-52
View File
@@ -1,52 +0,0 @@
import { quetzaConfig } from "../../../config";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import {
ColorResolvable, CommandInteraction, GuildMember,
MessageEmbed, TextChannel
} from "discord.js";
export async function run(client: QuetzaClient, ctx: CommandInteraction, reply = true) {
if (ctx.guild === null)
return;
if (reply)
await ctx.deferReply();
const player = client.Modules["music"].control.genPlayer(ctx.guild, client.Modules["music"].control, ctx.channel as TextChannel);
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Mistake in connection.")
if (!player.Connection) {
const channel = (ctx.member as GuildMember).voice.channel;
if (channel) {
await player.connect(channel);
embed.setTitle(`Successfully connected to ${channel.name}.`)
}
else {
client.Modules["music"].control.delPlayer(ctx.guild.id);
embed.setTitle("Please connect to a voice channel.")
}
}
else {
embed.setTitle("Already connected to ${channel.name}..")
}
if (reply)
await ctx.editReply({ embeds: [embed] });
else
return player;
}
const data = {
name: "connect",
description: "Connect to voice channel."
}
export { data }
-54
View File
@@ -1,54 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { QueueStorage } from "../assets/QuetzaMusic/QueueStorage";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const title = ctx.options.getString("title");
if (ctx.guild === null || title === null)
return;
const data = new QueueStorage(`./data/queues/${ctx.guild.id}.json`);
const slot = data.getEntry(title);
if (slot) {
if (slot.owner === ctx.user.id) {
data.removeEntry(title);
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`${title} has been deleted.`)
.setDescription(slot.description);
await ctx.reply({ embeds: [embed] });
data.saveData(`./data/queues/${ctx.guild.id}.json`);
}
else {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Sorry, but ${title} save is not yours. You can not do that.`);
await ctx.editReply({ embeds: [embed] });
}
}
}
const data = {
name: "delete",
description: "Load queue to current queue.",
options: [
{
name: "title",
description: "Delete slot title.",
type: "STRING",
required: true
}
]
}
export { data }
-40
View File
@@ -1,40 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
const filters: string[] = player.Filters;
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(player.Filters.length != 0 ? `Player active filters` : `There are no active filters.`);
filters.forEach((filter: string) => {
embed.addField(
filter,
"*",
true
);
});
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "filters",
description: "Print out list of active filters."
}
export { data }
-65
View File
@@ -1,65 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const pos = ctx.options.getInteger("position");
const query = ctx.options.getString("query");
if (!ctx.guild || !query || pos === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply()
const track = await player.addTrack(query, ctx.user, pos - 1);
let embed: MessageEmbed;
if (!track) {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Cannot append the track or playlist.")
.setAuthor({ name: `@${ctx.user.tag}`, iconURL: ctx.user.avatarURL() as string });
}
else {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(track.title)
.setDescription("Has been pushed to queue.")
.setURL(track.url)
.setThumbnail(track.thumbnail)
.setAuthor({ name: `@${ctx.user.tag}`, iconURL: ctx.user.avatarURL() as string });
console.log(`${track.title} added to ${ctx.guildId}`);
}
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "insert",
description: "Insert new track to specific position.",
options: [
{
name: "query",
description: "Title or URL of the song.",
type: "STRING",
required: true
},
{
name: "position",
description: "Position to insert.",
type: "INTEGER",
required: true
}
]
}
export { data }
-56
View File
@@ -1,56 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const query = ctx.options.getString("position");
if (ctx.guild === null || query === null)
return;
let player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
let state: boolean;
if (!isNaN(Number(query))) {
state = await player.jump(Number(query) - 1);
} else {
state = await player.jump(query);
}
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Could not find track specified.");
if (state) {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Jumped.");
}
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "jump",
description: "Jump to specific track in queue.",
options: [
{
name: "position",
description: "Position to jump to.",
type: "STRING",
required: true
}
]
}
export { data }
-54
View File
@@ -1,54 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { QueueStorage } from "../assets/QuetzaMusic/QueueStorage";
import { run as connect } from "./connect";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const title = ctx.options.getString("title");
if (ctx.guild === null || title === null)
return;
const player = await connect(client, ctx, false);
if (!player)
return;
const data = new QueueStorage(`./data/queues/${ctx.guild.id}.json`);
const slot = data.getEntry(title);
if (slot) {
player.Queue = player.Queue.concat(slot.tracks);
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`${title} has been appended to queue.`)
.setDescription(slot.description);
await ctx.reply({ embeds: [embed] });
}
if (!player.NowPlaying) {
await player.play();
}
}
const data = {
name: "load",
description: "Load queue to current queue.",
options: [
{
name: "title",
description: "Load slot title.",
type: "STRING",
required: true
}
]
}
export { data }
-62
View File
@@ -1,62 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const query = ctx.options.getString("option");
if (ctx.guild === null || query === null)
return;
let player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
player.repeat(query);
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable);
switch (query) {
case "LOOP": {
embed.setTitle("Now looping queue.");
break;
}
case "SONG": {
embed.setTitle("Now looping current song.");
break;
}
case "NONE": {
embed.setTitle("After queue end player will be destroyed.")
}
}
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "loop",
description: "Set player loop option.",
options: [
{
name: "option",
description: "Set player to...",
type: "STRING",
required: true,
choices: [
{ name: "loop queue", value: "LOOP" },
{ name: "loop current track", value: "SONG" },
{ name: "Destroy player on end", value: "NONE" }
]
}
]
}
export { data }
-51
View File
@@ -1,51 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { Music } from "../assets/QuetzaMusic/Music";
import fetch from "node-fetch";
const dividers = [
`</div></div></div></div><div class="hwc"><div class="BNeawe tAd8D AP7Wnd"><div><div class="BNeawe tAd8D AP7Wnd">`,
`</div></div></div></div></div><div><span class="hwc"><div class="BNeawe uEec3 AP7Wnd">`
]
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
const player = (client.Modules["music"].control as Music).getPlayer(ctx.guild.id);
if (!player || !player.NowPlaying)
return;
await ctx.deferReply();
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(player.NowPlaying.metadata.title)
.setDescription("Lyrics powered by Google and their partners.");
const response = await fetch(`https://www.google.com/search?q=${player.NowPlaying.metadata.title.replace(" ", "+").replace(/ *\[[^)]*\] */g, "") + "+Lyrics"}`)
const data = await response.text();
if (!data.includes(dividers[0])) {
embed.setTitle("Could not find lyrics for that one.");
ctx.editReply({ embeds: [embed] });
return;
}
await ctx.editReply({ content: `\`\`\`\n${data.split(dividers[0])[1].split(dividers[1])[0]}\`\`\``, embeds: [embed]});
}
const data = {
name: "lyrics",
description: "Get lyrics for current song.",
}
export { data }
-31
View File
@@ -1,31 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (player) {
player.skip();
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Skipped.");
await ctx.reply({ embeds: [embed] });
}
}
const data = {
name: "next",
description: "Skip current song."
}
export { data }
-30
View File
@@ -1,30 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (player) {
const state = player.pause();
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Player is now ${state ? "paused" : "resumed"}.`);
await ctx.reply({ embeds: [embed] });
}
}
const data = {
name: "pause",
description: "Toggle pause.",
}
export { data }
-33
View File
@@ -1,33 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { QueueStorage } from "../assets/QuetzaMusic/QueueStorage";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
await ctx.deferReply();
const data = new QueueStorage(`./data/queues/${ctx.guild.id}.json`);
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`${ctx.guild.name} queues:`);
data.Data.forEach((entry, title) => {
embed.addField(title, entry.description);
});
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "peak",
description: "Peak at guild storage."
}
export { data }
-66
View File
@@ -1,66 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { run as connect } from "./connect";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const query = ctx.options.getString("query");
const player = await connect(client, ctx, false);
if (!player)
return;
await ctx.deferReply();
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Now playing.")
.setAuthor({ name:`@${ctx.user.tag}`, iconURL: ctx.user.avatarURL() as string });
if (query) {
const track = await player.addTrack(query, ctx.user);
if (!track) {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Cannot append the track or playlist.")
.setAuthor({ name: `@${ctx.user.tag}`, iconURL: ctx.user.avatarURL() as string });
}
else {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(track.title)
.setDescription("Has been pushed to queue.")
.setURL(track.url)
.setThumbnail(track.thumbnail)
.setAuthor({ name: `@${ctx.user.tag}`, iconURL: ctx.user.avatarURL() as string });
console.log(`${track.title} added to ${ctx.guildId}`);
}
}
await ctx.editReply({ embeds: [embed] });
if (!player.NowPlaying) {
await player.play();
}
}
const data = {
name: "play",
description: "Play a song or add it to queue.",
options: [
{
name: "query",
description: "Title or URL of the song.",
type: "STRING",
required: true
}
]
}
export { data }
-84
View File
@@ -1,84 +0,0 @@
import { quetzaConfig } from "../../../config";
import {
ButtonInteraction, ColorResolvable,
CommandInteraction, Message, MessageActionRow,
MessageButton, MessageEmbed
} from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { queueDesigner } from "../assets/Misc"
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
const queue = player.Queue;
if (queue.length === 0) {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Queue is empty.");
await ctx.editReply({ embeds: [embed] });
return;
}
let page = 0;
const maxPage = Math.floor(queue.length / 11);
const row = new MessageActionRow()
.addComponents(
new MessageButton()
.setCustomId("QueueTop")
.setLabel("TOP")
.setStyle("SECONDARY"),
new MessageButton()
.setCustomId("QueueUp")
.setLabel("UP")
.setStyle("SECONDARY"),
new MessageButton()
.setCustomId("QueueDown")
.setLabel("DOWN")
.setStyle("SECONDARY"),
new MessageButton()
.setCustomId("QueueEnd")
.setLabel("END")
.setStyle("SECONDARY")
);
const message = await ctx.editReply({
embeds: [ queueDesigner(queue, page, player.NowPlaying, player.NowPlayingPos) ],
components: [row]
}) as Message;
const collector = message.createMessageComponentCollector({ time: 15000 });
collector.on("collect", async (btn: ButtonInteraction) => {
console.log(page);
switch (btn.customId) {
case "QueueTop": { page = 0; break; }
case "QueueUp": { page = Math.max(page - 1, 0); break; }
case "QueueDown": { page = Math.min(page + 1, maxPage); break; }
case "QueueEnd": page = maxPage;
}
await btn.update({
embeds: [ queueDesigner(queue, page, player.NowPlaying, player.NowPlayingPos) ]
});
});
}
const data = {
name: "queue",
description: "Send player queue.",
}
export { data }
-56
View File
@@ -1,56 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const query = ctx.options.getString("position");
if (ctx.guild === null || query === null)
return;
let player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
let state: boolean;
if (!isNaN(Number(query))) {
state = await player.remove(Number(query) - 1);
} else {
state = await player.remove(query);
}
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Could not find track specified.");
if (state) {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Removed.");
}
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "remove",
description: "Remove specific track in queue.",
options: [
{
name: "position",
description: "Position to remove.",
type: "STRING",
required: true
}
]
}
export { data }
-84
View File
@@ -1,84 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { QueueStorage } from "../assets/QuetzaMusic/QueueStorage";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const title = ctx.options.getString("title");
const description = ctx.options.getString("description");
if (ctx.guild === null || title === null || description === null)
return;
let player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
if (player.Queue.length !== 0) {
const data = new QueueStorage(`./data/queues/${ctx.guild.id}.json`);
const slot = data.getEntry(title);
if (slot) {
if (slot.owner === ctx.user.id) {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Aww... ${title} already exists. Delete it first before saving a new one.`);
await ctx.editReply({ embeds: [embed] });
return;
}
else {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Sorry, but ${title} save is not yours. You can not do that.`);
await ctx.editReply({ embeds: [embed] });
return;
}
}
data.createEntry(player.Queue, title, description, ctx.user.id);
data.saveData(`./data/queues/${ctx.guild.id}.json`)
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`I will create new save entry ${title}`);
await ctx.editReply({ embeds: [embed] });
}
else {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Looks like an issue. Make sure you are doing it well`);
await ctx.editReply({ embeds: [embed] });
}
}
const data = {
name: "save",
description: "Save queue to guild storage.",
options: [
{
name: "title",
description: "Save slot title.",
type: "STRING",
required: true
},
{
name: "description",
description: "Save slot description.",
type: "STRING",
required: true
}
]
}
export { data }
-58
View File
@@ -1,58 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const hours = ctx.options.getInteger("hrs") as number;
const minutes = ctx.options.getInteger("mins") as number;
const seconds = ctx.options.getInteger("secs") as number;
if (ctx.guild === null)
return;
const time = hours * 3600 + minutes * 60 + seconds;
let player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (player) {
await ctx.deferReply();
await player.seek(time);
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Fastforwarded to ${hours}:${minutes}.${seconds}.`);
await ctx.editReply({ embeds: [embed] });
}
}
const data = {
name: "seek",
description: "Fastforward to timecode.",
options: [
{
name: "hrs",
description: "Hour to seek.",
type: "INTEGER",
required: false
},
{
name: "mins",
description: "Minute to seek.",
type: "INTEGER",
required: false
},
{
name: "secs",
description: "Second to seek.",
type: "INTEGER",
required: false
}
]
}
export { data }
-30
View File
@@ -1,30 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
if (ctx.guild === null)
return;
const player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (player) {
player.destroy();
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Player is now destroyed.`);
await ctx.reply({ embeds: [embed] });
}
}
const data = {
name: "stop",
description: "Destroys player.",
}
export { data }
-48
View File
@@ -1,48 +0,0 @@
import { quetzaConfig } from "../../../config";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
const volume = ctx.options.getInteger("volume");
if (ctx.guild === null || volume === null)
return;
let player = client.Modules["music"].control.getPlayer(ctx.guild.id);
if (!player)
return;
await ctx.deferReply();
let embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Error occured while setting the volume.`);
if (player.volume(volume)) {
embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle(`Volume has been set to ${volume / 100}.`);
}
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "volume",
description: "Set music volume.",
options: [
{
name: "volume",
description: "Volume to switch to in percents.",
type: "INTEGER",
required: true
}
]
}
export { data }
@@ -1,29 +0,0 @@
import { ColorResolvable, MessageEmbed, VoiceState } from "discord.js";
import { quetzaConfig } from "../../../config";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { asleep } from "../assets/Misc";
export async function run(client: QuetzaClient, args: VoiceState[]): Promise<void> {
const [oldState, newState] = args;
const player = client.Modules["music"].control.getPlayer(newState.guild.id);
if (!player)
return;
if (oldState.channel?.members.size === 1 && oldState.channel.members.first()?.id === client.user?.id) {
await asleep(30000);
const channel = player.Channel;
if (oldState.channel.members.size === 1) {
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("I've decided to leave to conserve my energy")
.setDescription("Since there was nobody in the voice channel.")
await channel.send({ embeds: [embed] });
player.destroy();
}
}
}
export const name = "voiceStateUpdate";
-12
View File
@@ -1,12 +0,0 @@
import { Music } from "./assets/QuetzaMusic/Music";
const data = {
key: "music",
name: "QuetzaMusic",
description: "Quetza music module v.1.5.0",
author: "unknowableshade"
}
const control = new Music();
export {data, control};
-31
View File
@@ -1,31 +0,0 @@
import { quetzaConfig } from "../../../config";
import { QuetzaClient } from "../../../assets/QuetzaClient";
import { ColorResolvable, CommandInteraction, MessageEmbed } from "discord.js";
export async function run(client: QuetzaClient, ctx: CommandInteraction) {
await ctx.deferReply();
const embed = new MessageEmbed()
.setColor(quetzaConfig.color as ColorResolvable)
.setTitle("Quetza applied modules:");
for (const value of Object.values(client.Modules)) {
embed.addField(
value.data.name,
`${value.data.description} (${value.data.author})`
);
}
await ctx.editReply({ embeds: [embed] });
}
const data = {
name: "modules",
description: "Print out list of accessable modules."
}
export { data }
@@ -1,15 +0,0 @@
import { Interaction } from "discord.js";
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, 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";
-24
View File
@@ -1,24 +0,0 @@
import { QuetzaClient } from "../../../assets/QuetzaClient";
export async function run(client: QuetzaClient, args: unknown[]): Promise<void> {
if (!client.user)
return;
Object.values(client.Modules).forEach((value) => {
console.log(`Module ${value.data.name} is active...`);
});
console.log(`${client.user.username} logged in as ${client.user.tag}!`);
client.user.setActivity("on sound waves.", { type: "PLAYING" });
let restCommands = [];
for (const value of client.Commands.values()) {
restCommands.push(value.data);
}
client.application?.commands.set(restCommands);
}
export const name = "ready";
-10
View File
@@ -1,10 +0,0 @@
const data = {
key: "quetza",
name: "Quetza Main Frame",
description: "Quetza default module v.1.5.0",
author: "unknowableshade"
}
const control = undefined;
export {data, control};
+2 -1
View File
@@ -1,12 +1,13 @@
{
"compilerOptions": {
"baseUrl": "src",
"module": "CommonJS",
"outDir": "./dist",
"strict": true,
"target": "ESNext",
"esModuleInterop": true,
"moduleResolution": "Node",
"lib": [ "ESNext", "DOM" ],
"lib": [ "ESNext" ],
"sourceMap": true,
"types": [ "node" ]
},