2205.1 major update!
This commit is contained in:
1 parent
913b718026
commit
a86faf06d5
75 files changed
+4457
-2704
No files matched your search
@@ -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
@@ -367,5 +367,5 @@ dist/
|
||||
*.js
|
||||
*.js.map
|
||||
|
||||
#binaries
|
||||
# binaries will bot bundle
|
||||
bin/
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)**
|
||||
Generated
+2550
-321
File diff suppressed because it is too large.
Load diff
+19
-13
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import { QuetzaClient } from "./assets/QuetzaClient";
|
||||
import { quetzaConfig } from "./config";
|
||||
|
||||
const client = new QuetzaClient();
|
||||
|
||||
client.login(quetzaConfig.token);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -0,0 +1,6 @@
|
||||
import Client from "./lib/Client";
|
||||
import config from "./config";
|
||||
|
||||
const client = new Client();
|
||||
|
||||
client.login(config.token);
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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" ? "⤵️" : "🔂";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -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 }
|
||||
@@ -1,7 +0,0 @@
|
||||
const data = {
|
||||
key: "codeshr",
|
||||
name: "CodeShr",
|
||||
description: "CodeShr module v.1.0.0",
|
||||
author: "unknowableshade"
|
||||
}
|
||||
export {data};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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";
|
||||
@@ -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};
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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
@@ -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" ]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user