Compare commits
No files matched your search
+3
-1
@@ -6,9 +6,11 @@
|
|||||||
|
|
||||||
# Source
|
# Source
|
||||||
!src/
|
!src/
|
||||||
|
!prisma/
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
!package*.json
|
!package.json
|
||||||
|
!pnpm-lock.yaml
|
||||||
|
|
||||||
# Typescript compiller config
|
# Typescript compiller config
|
||||||
!tsconfig.json
|
!tsconfig.json
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
name: actions
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
||||||
|
|
||||||
name: CI/CD
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -9,41 +6,61 @@ on:
|
|||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
mocha:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [18.x]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
pnpm-version: [8]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.pnpm-version }}
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: "npm"
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build --if-present
|
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: ESLint & Prettier
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm run --if-present test
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: mocha
|
||||||
|
|
||||||
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
with:
|
with:
|
||||||
images: unknowableshade/quetza-bot
|
images: unknowableshade/quetza-bot
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"endOfLine": "crlf",
|
"endOfLine": "lf",
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["**/*.json, **/*.yml"],
|
"files": ["**/*.json, **/*.yml"],
|
||||||
|
|||||||
+38
-5
@@ -1,3 +1,22 @@
|
|||||||
|
FROM node:18-alpine as builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache python3
|
||||||
|
RUN apk add --no-cache make
|
||||||
|
RUN apk add --no-cache gcc
|
||||||
|
RUN apk add --no-cache build-base
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
@@ -5,14 +24,28 @@ RUN apk add --no-cache bash
|
|||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
RUN apk add --no-cache ffmpeg
|
RUN apk add --no-cache ffmpeg
|
||||||
RUN apk add --no-cache python3
|
RUN apk add --no-cache python3
|
||||||
|
RUN apk add --no-cache make
|
||||||
|
RUN apk add --no-cache gcc
|
||||||
|
RUN apk add --no-cache build-base
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
USER node
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN mkdir bin
|
RUN mkdir bin
|
||||||
RUN curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/src/app/bin/youtube-dl
|
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/src/app/bin/yt-dlp
|
||||||
RUN chmod a+rx /usr/src/app/bin/youtube-dl
|
RUN chmod a+rx /usr/src/app/bin/yt-dlp
|
||||||
|
|
||||||
COPY . .
|
COPY prisma/ .
|
||||||
RUN npm ci
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
CMD [ "npm", "start" ]
|
RUN pnpm install --prod
|
||||||
|
|
||||||
|
RUN pnpx prisma generate
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/app/dist ./dist
|
||||||
|
|
||||||
|
CMD [ "pnpm", "start" ]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Quetza - 2302.1
|
# Quetza 4
|
||||||
|
|
||||||
_Neat typescript discord bot._
|
_Neat typescript discord bot._
|
||||||
|
|
||||||
|
|||||||
Generated
-3369
File diff suppressed because it is too large.
Load diff
+40
-29
@@ -1,50 +1,61 @@
|
|||||||
{
|
{
|
||||||
"name": "quetza-bot",
|
"name": "quetza-bot",
|
||||||
"engines": {
|
"version": "4.2.2",
|
||||||
"node": "18.x.x",
|
"description": "Discord multipurpose bot",
|
||||||
"npm": "*"
|
|
||||||
},
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2303.1",
|
"main": "./dist/index.js",
|
||||||
"description": "Discord multifunctional bot",
|
"scripts": {
|
||||||
|
"start": "node ./dist/index.js",
|
||||||
|
"build": "pnpm exec tsc --build",
|
||||||
|
"clean": "rm -rf ./dist && pnpm run build",
|
||||||
|
"lint": "pnpm exec eslint . && pnpm exec prettier --check .",
|
||||||
|
"format": "pnpm exec eslint --fix . && pnpm exec prettier --write .",
|
||||||
|
"preinstall": "pnpm dlx only-allow pnpm"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"discord",
|
||||||
|
"bot",
|
||||||
|
"music"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/unknowableshade/quetza-bot.git"
|
||||||
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "unknowableshade",
|
"name": "unknowableshade",
|
||||||
"email": "60275123+unknowableshade@users.noreply.github.com",
|
"email": "60275123+unknowableshade@users.noreply.github.com",
|
||||||
"url": "https://unknowableshade.ru"
|
"url": "https://unknowableshade.ru"
|
||||||
},
|
},
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/unknowableshade/quetza-bot.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/unknowableshade/quetza-bot/issues"
|
"url": "https://github.com/unknowableshade/quetza-bot/issues"
|
||||||
},
|
},
|
||||||
"readme": "https://github.com/unknowableshade/quetza-bot#readme",
|
"readme": "https://github.com/unknowableshade/quetza-bot#readme",
|
||||||
"license": "MIT",
|
"engines": {
|
||||||
"main": "dist/index.js",
|
"node": "18.x.x",
|
||||||
"scripts": {
|
"npm": "*"
|
||||||
"start": "node dist/index.js",
|
|
||||||
"build": "npx tsc --build",
|
|
||||||
"clean": "npx tsc --build --clean",
|
|
||||||
"lint": "npx eslint --fix . && npx prettier --write .",
|
|
||||||
"postinstall": "npm run build"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.8.0",
|
"@discordjs/opus": "^0.9.0",
|
||||||
"@discordjs/voice": "^0.11.0",
|
"@discordjs/voice": "^0.16.0",
|
||||||
"discord-ytdl-core": "^5.0.4",
|
"@prisma/client": "5.0.0",
|
||||||
"discord.js": "^14.3.0",
|
"discord.js": "^14.11.0",
|
||||||
|
"prism-media": "^1.3.5",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"ytdl-core": "^4.11.1"
|
"winston": "^3.10.0",
|
||||||
|
"ytdl-core": "^4.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.7.13",
|
"@types/node": "^20.4.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^5.35.1",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"eslint": "^8.22.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"typescript": "^4.8.2"
|
"mocha": "^10.2.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"prisma": "^5.0.0",
|
||||||
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+3557
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Guild" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Guild_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Event" (
|
||||||
|
"objectId" SERIAL NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"Roles" TEXT[],
|
||||||
|
"guildId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Event_pkey" PRIMARY KEY ("objectId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Event_guildId_title_key" ON "Event"("guildId", "title");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Event" ADD CONSTRAINT "Event_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `Event` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to drop the column `Roles` on the `Event` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `objectId` on the `Event` table. All the data in the column will be lost.
|
||||||
|
- A unique constraint covering the columns `[title,action,guildId]` on the table `Event` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `action` to the `Event` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `metadata` to the `Event` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "PenaltyType" AS ENUM ('BAN', 'TIMEOUT', 'WARN');
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "Event_guildId_title_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" DROP CONSTRAINT "Event_pkey",
|
||||||
|
DROP COLUMN "Roles",
|
||||||
|
DROP COLUMN "objectId",
|
||||||
|
ADD COLUMN "action" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "metadata" JSONB NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Queue" (
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"Tracks" JSONB NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"authorId" TEXT NOT NULL,
|
||||||
|
"guildId" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Member" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"level" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"guildId" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Penalty" (
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"type" "PenaltyType" NOT NULL,
|
||||||
|
"reason" TEXT,
|
||||||
|
"expires" TIMESTAMP(3),
|
||||||
|
"guildId" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Queue_title_guildId_key" ON "Queue"("title", "guildId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Member_userId_guildId_key" ON "Member"("userId", "guildId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Penalty_type_userId_guildId_key" ON "Penalty"("type", "userId", "guildId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Event_title_action_guildId_key" ON "Event"("title", "action", "guildId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Queue" ADD CONSTRAINT "Queue_userId_guildId_fkey" FOREIGN KEY ("userId", "guildId") REFERENCES "Member"("userId", "guildId") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Queue" ADD CONSTRAINT "Queue_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Member" ADD CONSTRAINT "Member_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Penalty" ADD CONSTRAINT "Penalty_userId_guildId_fkey" FOREIGN KEY ("userId", "guildId") REFERENCES "Member"("userId", "guildId") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Penalty" ADD CONSTRAINT "Penalty_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Guild {
|
||||||
|
id String @id
|
||||||
|
events Event[]
|
||||||
|
members Member[]
|
||||||
|
penalties Penalty[]
|
||||||
|
queues Queue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Queue {
|
||||||
|
title String
|
||||||
|
description String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
Tracks Json
|
||||||
|
Member Member @relation(fields: [userId, guildId], references: [userId, guildId])
|
||||||
|
userId String
|
||||||
|
authorId String
|
||||||
|
Guild Guild @relation(fields: [guildId], references: [id])
|
||||||
|
guildId String
|
||||||
|
@@unique([title, guildId], name: "queueIdentifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Member {
|
||||||
|
userId String
|
||||||
|
level Int @default(0)
|
||||||
|
queues Queue[]
|
||||||
|
Guild Guild @relation(fields: [guildId], references: [id])
|
||||||
|
guildId String
|
||||||
|
penalties Penalty[]
|
||||||
|
@@unique([userId, guildId], name: "memberIdentifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PenaltyType {
|
||||||
|
BAN
|
||||||
|
TIMEOUT
|
||||||
|
WARN
|
||||||
|
}
|
||||||
|
|
||||||
|
model Penalty {
|
||||||
|
Member Member @relation(fields: [userId, guildId], references: [userId, guildId])
|
||||||
|
userId String
|
||||||
|
type PenaltyType
|
||||||
|
reason String?
|
||||||
|
expires DateTime?
|
||||||
|
Guild Guild @relation(fields: [guildId], references: [id])
|
||||||
|
guildId String
|
||||||
|
@@unique([type, userId, guildId], name: "penaltyIdentifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Event {
|
||||||
|
title String
|
||||||
|
action String
|
||||||
|
metadata Json
|
||||||
|
Guild Guild @relation(fields: [guildId], references: [id])
|
||||||
|
guildId String
|
||||||
|
@@unique([title, action, guildId], name: "eventIdentifier")
|
||||||
|
}
|
||||||
+17
-3
@@ -1,12 +1,26 @@
|
|||||||
|
import { ActivityOptions, ActivityType, ColorResolvable } from "discord.js";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const discordToken = process.env.DISCORD_TOKEN;
|
||||||
|
const modulesDir = path.join(ROOT, "/modules/");
|
||||||
|
const binariesDir = path.join(ROOT, "/../bin/");
|
||||||
|
const activityStatus: ActivityOptions = { type: ActivityType.Watching, name: "over you" };
|
||||||
|
const colors = {
|
||||||
|
default: "#ff6e9b" as ColorResolvable,
|
||||||
|
error: "#b94e59" as ColorResolvable,
|
||||||
|
warning: "#e5925f" as ColorResolvable,
|
||||||
|
info: "#367df6" as ColorResolvable,
|
||||||
|
success: "#83d18e" as ColorResolvable
|
||||||
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
discordToken: process.env.DISCORD_TOKEN,
|
discordToken,
|
||||||
modulesDir: path.join(ROOT, "/modules/"),
|
modulesDir,
|
||||||
binariesDir: path.join(ROOT, "/../bin/")
|
binariesDir,
|
||||||
|
activityStatus,
|
||||||
|
colors
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
+5
-1
@@ -4,7 +4,11 @@ import config from "./config.js";
|
|||||||
import Client from "./lib/client.js";
|
import Client from "./lib/client.js";
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates]
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
|
GatewayIntentBits.GuildMembers
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
client.login(config.discordToken);
|
client.login(config.discordToken);
|
||||||
+13
-7
@@ -1,3 +1,4 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
import { Client as DiscordClient, ClientOptions, Collection } from "discord.js";
|
import { Client as DiscordClient, ClientOptions, Collection } from "discord.js";
|
||||||
import { existsSync, lstatSync, readdirSync } from "fs";
|
import { existsSync, lstatSync, readdirSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
@@ -15,6 +16,8 @@ export default class Client extends DiscordClient {
|
|||||||
|
|
||||||
public readonly modules = new Collection<string, ModuleMetadata>();
|
public readonly modules = new Collection<string, ModuleMetadata>();
|
||||||
|
|
||||||
|
public readonly db = new PrismaClient();
|
||||||
|
|
||||||
public constructor(options: ClientOptions) {
|
public constructor(options: ClientOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
@@ -50,12 +53,15 @@ export default class Client extends DiscordClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => {
|
importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => {
|
||||||
this.commands.set(command.data.name, command)
|
this.commands.set(command.data.name, command);
|
||||||
commands.push(command.data.name);
|
commands.push(command.data.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
|
importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
|
||||||
this.on(event.name, (...eventee: unknown[]) => event.execute(this, eventee));
|
this.on(
|
||||||
|
event.name,
|
||||||
|
async (...eventee: unknown[]) => await event.execute(this, eventee)
|
||||||
|
);
|
||||||
events.push(event.name);
|
events.push(event.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,23 +69,23 @@ export default class Client extends DiscordClient {
|
|||||||
...data,
|
...data,
|
||||||
commands,
|
commands,
|
||||||
events
|
events
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateApplicationStatus(): string {
|
public generateApplicationStatus(): string[] {
|
||||||
const modulesStatus = this.modules.map((module) => {
|
const modulesStatus = this.modules.map((module) => {
|
||||||
const tag = `✓ ${module.name}@${module.tag} module is loaded.`
|
const tag = `✓ ${module.name}@${module.tag} module is loaded.`;
|
||||||
const commands = module.commands.map((name) => " - " + name).join("\n");
|
const commands = module.commands.map((name) => " - " + name).join("\n");
|
||||||
const events = module.events.map((name) => " - " + name).join("\n");
|
const events = module.events.map((name) => " - " + name).join("\n");
|
||||||
|
|
||||||
return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n");
|
return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n");
|
||||||
}).join("\n" + "-".repeat(50) + "\n");
|
});
|
||||||
|
|
||||||
const clientStatus =
|
const clientStatus =
|
||||||
this.user && this.application
|
this.user && this.application
|
||||||
? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
|
? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
|
||||||
: "✖ Application is not availiable.";
|
: "✖ Application is not availiable.";
|
||||||
|
|
||||||
return ["-".repeat(50), modulesStatus, "-".repeat(50), clientStatus].join("\n");
|
return modulesStatus.concat(clientStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import winston from "winston";
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: "info",
|
||||||
|
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({ format: winston.format.simple() }),
|
||||||
|
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
|
||||||
|
new winston.transports.File({ filename: "logs/combined.log" })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default logger;
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction): Promise<void> {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
await interaction.reply(I18n.replies.modules(client.modules));
|
if (!interaction.isChatInputCommand()) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply(replies.modules(client.modules));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction): Promise<void> {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const start = Date.now();
|
if (!interaction.isChatInputCommand()) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!client.application) {
|
|
||||||
await interaction.reply(I18n.replies.ping());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.application.fetch();
|
const reply = await interaction.reply({ ...replies.ping(client.ws.ping), fetchReply: true });
|
||||||
|
|
||||||
await interaction.reply(I18n.replies.ping(Date.now() - start));
|
await interaction.editReply(
|
||||||
|
replies.ping(client.ws.ping, reply.createdTimestamp - interaction.createdTimestamp)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me.");
|
const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me.");
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import { Interaction } from "discord.js";
|
import { Events, Interaction } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
|
import logger from "../../../lib/logger.js";
|
||||||
|
|
||||||
async function execute(client: Client, eventee: Interaction[]): Promise<void> {
|
async function execute(client: Client, eventee: [Interaction]): Promise<void> {
|
||||||
const [interaction] = eventee;
|
const [interaction] = eventee;
|
||||||
|
|
||||||
if (interaction.isCommand()) {
|
if (interaction.isChatInputCommand()) {
|
||||||
const command = client.commands.get(interaction.commandName);
|
const command = client.commands.get(interaction.commandName);
|
||||||
|
|
||||||
if (command) {
|
if (!command) {
|
||||||
command.execute(client, interaction);
|
logger.error(`No command matching ${interaction.commandName} was found.`, {
|
||||||
|
interaction
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Interaction ${interaction.commandName} was created.`, interaction);
|
||||||
|
|
||||||
|
await command.execute(client, interaction);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Interaction error occured. ", { error, interaction });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = "interactionCreate";
|
const name = Events.InteractionCreate;
|
||||||
|
|
||||||
export { execute, name };
|
export { execute, name };
|
||||||
@@ -1,23 +1,37 @@
|
|||||||
import { generateDependencyReport } from "@discordjs/voice";
|
import { generateDependencyReport } from "@discordjs/voice";
|
||||||
import { ActivityType } from "discord.js";
|
import { Events } from "discord.js";
|
||||||
|
|
||||||
|
import config from "../../../config.js";
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
|
import logger from "../../../lib/logger.js";
|
||||||
|
|
||||||
async function execute(client: Client): Promise<void> {
|
async function execute(client: Client): Promise<void> {
|
||||||
if (!client.user || !client.application) {
|
if (!client.user || !client.application) {
|
||||||
throw new Error("No application or user for it is provided.");
|
throw new Error("No application or user for it is provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(generateDependencyReport());
|
const commandData = Array.from(client.commands.values()).map((value) => value.data);
|
||||||
console.log(client.generateApplicationStatus());
|
|
||||||
|
|
||||||
client.user.setActivity("over you", { type: ActivityType.Watching });
|
if (process.env.NODE_ENV === "development" && process.env.TEST_GUILD) {
|
||||||
|
logger.info("Development mode: Commands will be pushed to the test guild!");
|
||||||
|
|
||||||
const restCommands = Array.from(client.commands.values()).map((value) => value.data);
|
logger.info(generateDependencyReport());
|
||||||
|
|
||||||
client.application.commands.set(restCommands);
|
const guild = await client.guilds.fetch(process.env.TEST_GUILD);
|
||||||
|
guild.commands.set(commandData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
logger.info("Production mode: Commands will be pushed to the application!");
|
||||||
|
|
||||||
|
await client.application.commands.set(commandData);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.generateApplicationStatus().forEach((report) => logger.info(report));
|
||||||
|
|
||||||
|
client.user.setActivity(config.activityStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = "ready";
|
const name = Events.ClientReady;
|
||||||
|
|
||||||
export { execute, name };
|
export { execute, name };
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { bold, Collection, EmbedBuilder, InteractionReplyOptions, italic } from "discord.js";
|
|
||||||
|
|
||||||
import { ModuleMetadata } from "../../../lib/types.js";
|
|
||||||
export default class I18n {
|
|
||||||
public static readonly replies = {
|
|
||||||
modules: (modules: Collection<string, ModuleMetadata>): InteractionReplyOptions => {
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor("Random")
|
|
||||||
.setTitle("Modules")
|
|
||||||
.addFields(
|
|
||||||
modules.map((value) => {
|
|
||||||
return {
|
|
||||||
name: value.name + "@" + value.tag,
|
|
||||||
value: (value.description ?? "") + " | " + value.author,
|
|
||||||
inline: true
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return { embeds: [embed] };
|
|
||||||
},
|
|
||||||
ping: (ping?: number): InteractionReplyOptions => {
|
|
||||||
const title = ping ? `I have pinged ${ping} ms late.` : "There is no application.";
|
|
||||||
|
|
||||||
const comment = !ping
|
|
||||||
? "How am I speaking then?"
|
|
||||||
: ping > 250
|
|
||||||
? "I'm kinda slowpoke ngl."
|
|
||||||
: ping > 200
|
|
||||||
? "I can do better than this."
|
|
||||||
: "Dream's speed!";
|
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor("Random")
|
|
||||||
.setTitle(bold(title))
|
|
||||||
.setDescription(italic(bold(comment)));
|
|
||||||
|
|
||||||
return { embeds: [embed] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { BaseMessageOptions, Collection, EmbedBuilder, italic } from "discord.js";
|
||||||
|
|
||||||
|
import config from "../../../config.js";
|
||||||
|
import { ModuleMetadata } from "../../../lib/types.js";
|
||||||
|
|
||||||
|
const replies = {
|
||||||
|
modules: (modules: Collection<string, ModuleMetadata>): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setTitle("Applied modules")
|
||||||
|
.addFields(
|
||||||
|
modules.map((value) => {
|
||||||
|
return {
|
||||||
|
name: value.name + "@" + value.tag,
|
||||||
|
value: italic(value.description ?? "")
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
ping: (websocket: number, roundtrip?: number): BaseMessageOptions => {
|
||||||
|
const title = roundtrip ? `I've pinged ${roundtrip} ms. late` : "Pinging...";
|
||||||
|
|
||||||
|
const comment = `Websocket ping is ${websocket} ms.`;
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setTitle(title)
|
||||||
|
.setDescription(italic(comment));
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default replies;
|
||||||
@@ -4,7 +4,7 @@ import { fileURLToPath } from "url";
|
|||||||
const name = "core";
|
const name = "core";
|
||||||
const description = "Quetza core functionality";
|
const description = "Quetza core functionality";
|
||||||
const author = "unknowableshade";
|
const author = "unknowableshade";
|
||||||
const tag = "2302.1";
|
const tag = "4.2.2";
|
||||||
|
|
||||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction): Promise<void> {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
if (!interaction.guild || !interaction.channel) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,9 +24,12 @@ async function execute(client: Client, interaction: CommandInteraction): Promise
|
|||||||
|
|
||||||
player.clear();
|
player.clear();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.cleared(amount)] });
|
await interaction.reply(replies.cleared(amount));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("clear").setDescription("Clear the player's queue.");
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("clear")
|
||||||
|
.setDescription("Clear the player's queue.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,41 +1,43 @@
|
|||||||
import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
if (!interaction.guild || !interaction.channel || !interaction.member) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
const player =
|
const player =
|
||||||
controller.get(interaction.guild.id, interaction.channel as TextChannel) ||
|
controller.get(interaction.guild, interaction.channel) ??
|
||||||
controller.set(interaction.guild, interaction.channel as TextChannel);
|
controller.set(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
|
const channel = interaction.member.voice.channel;
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
controller.delete(interaction.guildId);
|
||||||
|
|
||||||
|
await interaction.editReply(replies.notConnected());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!player.connection) {
|
|
||||||
const channel = (interaction.member as GuildMember).voice.channel;
|
|
||||||
if (channel) {
|
|
||||||
player.connect(channel);
|
player.connect(channel);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.okConnected(channel.name)] });
|
await interaction.editReply(replies.okConnected(channel.name));
|
||||||
} else {
|
|
||||||
controller.delete(interaction.guild.id);
|
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.notConnected()] });
|
return;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.wasConnected()] });
|
|
||||||
}
|
|
||||||
|
|
||||||
return player;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
.setName("connect")
|
.setName("connect")
|
||||||
.setDescription("Connect me to a voice channel.");
|
.setDescription("Connect me explicitly to the voice channel.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,129 +1,46 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import Filter from "../lib/filter.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const filter = interaction.options.get("filter")?.value as string;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!interaction.guild || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const filter = interaction.options.getString("filter") ?? undefined;
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.setFilter(filter || undefined);
|
const status = player.setFilter(filter);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.filtered(filter || undefined)] });
|
await interaction.reply(replies.filtered(filter, status));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
.setName("filter")
|
.setName("filter")
|
||||||
.setDescription("Apply unique filters to the tracks.")
|
.setDescription("Apply unique filters to the playback.")
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("filter")
|
.setName("filter")
|
||||||
.setDescription("Filter to apply.")
|
.setDescription("Filter to apply.")
|
||||||
.setChoices(
|
.setChoices(
|
||||||
{
|
...Object.keys(Filter.filters).map((key) => {
|
||||||
name: "Bassboost low",
|
return { name: key, value: key };
|
||||||
value: "bassboost_low"
|
})
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bassboost",
|
|
||||||
value: "bassboost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bassboost high",
|
|
||||||
value: "bassboost_high"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "8D",
|
|
||||||
value: "8D"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vaporwave",
|
|
||||||
value: "vaporwave"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Nightcore",
|
|
||||||
value: "nightcore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Phaser",
|
|
||||||
value: "phaser"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Tremolo",
|
|
||||||
value: "tremolo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vibrato",
|
|
||||||
value: "vibrato"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Reverse",
|
|
||||||
value: "reverse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Treble",
|
|
||||||
value: "treble"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Normalizer",
|
|
||||||
value: "normalizer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Surround Sound",
|
|
||||||
value: "surrounding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pulsator",
|
|
||||||
value: "pulsator"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Subboost",
|
|
||||||
value: "subboost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Mono Audio",
|
|
||||||
value: "mono"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Compressor",
|
|
||||||
value: "compressor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Expander",
|
|
||||||
value: "expander"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Softlimiter",
|
|
||||||
value: "softlimiter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Chorus",
|
|
||||||
value: "chorus"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Fadein",
|
|
||||||
value: "fadein"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Dimming",
|
|
||||||
value: "dim"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Earrape",
|
|
||||||
value: "earrape"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.setRequired(true)
|
)
|
||||||
);
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,23 +1,31 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
if (!interaction.guild || !interaction.channel || !interaction.member) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.deferReply();
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.playerInfo(player)] });
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply(replies.info(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
.setName("info")
|
.setName("info")
|
||||||
.setDescription("Information about the playback.");
|
.setDescription("Verbose information about player and stats.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,28 +1,33 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const pos = interaction.options.get("position")?.value as number;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
const query = interaction.options.get("query")?.value as string;
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!interaction.guild || !query || !pos || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const query = interaction.options.getString("query", true);
|
||||||
|
const pos = interaction.options.getInteger("position", true);
|
||||||
if (!player) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
await interaction.editReply(replies.notExists());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const track = await player.add(query, interaction.user, pos - 1);
|
const track = await player.add(query, interaction.user, pos - 1);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.appended(track)] });
|
await interaction.editReply(replies.appended(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
@@ -33,6 +38,7 @@ const data = new SlashCommandBuilder()
|
|||||||
)
|
)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option.setName("position").setDescription("Position to insert.").setRequired(true)
|
option.setName("position").setDescription("Position to insert.").setRequired(true)
|
||||||
);
|
)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,28 +1,35 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const query = interaction.options.get("query")?.value as string;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!interaction.guild || !query || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const query = interaction.options.getString("query", true);
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state =
|
const previous = player.resource?.metadata;
|
||||||
|
|
||||||
|
const position =
|
||||||
!isNaN(+query) && isFinite(+query) && !/e/i.test(query)
|
!isNaN(+query) && isFinite(+query) && !/e/i.test(query)
|
||||||
? player.jump(parseInt(query) - 1)
|
? player.jump(parseInt(query) - 1)
|
||||||
: player.jump(query);
|
: player.jump(query);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.jumped(state)] });
|
await interaction.reply(replies.jumped(previous, position));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
@@ -30,6 +37,7 @@ const data = new SlashCommandBuilder()
|
|||||||
.setDescription("Jump to specific track in the queue.")
|
.setDescription("Jump to specific track in the queue.")
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option.setName("query").setDescription("Position or title to jump to.").setRequired(true)
|
option.setName("query").setDescription("Position or title to jump to.").setRequired(true)
|
||||||
);
|
)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,26 +1,31 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { LoopOption } from "../lib/types.js";
|
import { LoopOption } from "../lib/types.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const query = interaction.options.get("option")?.value as string;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!interaction.guild || !query || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const option = interaction.options.getString("option", true) as LoopOption;
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.queue.loop = query as LoopOption;
|
player.queue.loop = option;
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.looped(query as LoopOption)] });
|
await interaction.reply(replies.looped(option));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
@@ -29,14 +34,15 @@ const data = new SlashCommandBuilder()
|
|||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("option")
|
.setName("option")
|
||||||
.setDescription("Select loop option.")
|
.setDescription("Loop option.")
|
||||||
.setChoices(
|
.setChoices(
|
||||||
{ name: "loop queue", value: "LOOP" },
|
{ name: "Loop queue", value: "LOOP" },
|
||||||
{ name: "loop single track", value: "SONG" },
|
{ name: "Loop single track", value: "SONG" },
|
||||||
{ name: "Till end", value: "NONE" },
|
{ name: "Drop on final", value: "NONE" },
|
||||||
{ name: "Random order", value: "AUTO" }
|
{ name: "Random", value: "AUTO" }
|
||||||
)
|
)
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
);
|
)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
if (!interaction.guild || !interaction.channel) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.skip();
|
player.skip();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.skipped()] });
|
await interaction.reply(replies.skipped());
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("next").setDescription("Skip current song.");
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("next")
|
||||||
|
.setDescription("Skip current song.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
if (!interaction.guild || !interaction.channel) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = player.pause();
|
const state = player.pause();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.paused(state)] });
|
await interaction.reply(replies.paused(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("pause").setDescription("Toggle pause on player.");
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("pause")
|
||||||
|
.setDescription("Toggle pause on playback.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,26 +1,49 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
import { execute as connect } from "./connect.js";
|
import replies from "../lib/replies.js";
|
||||||
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const query = interaction.options.get("query")?.value as string;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
const player = await connect(client, interaction);
|
|
||||||
|
|
||||||
if (!player) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = interaction.options.getString("query");
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
await interaction.reply(replies.searching());
|
||||||
|
} else {
|
||||||
|
await interaction.deferReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
const player =
|
||||||
|
controller.get(interaction.guild, interaction.channel) ??
|
||||||
|
controller.set(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
|
const channel = interaction.member.voice.channel;
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
controller.delete(interaction.guildId);
|
||||||
|
|
||||||
|
await interaction.editReply(replies.notConnected());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.connect(channel);
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
const track = await player.add(query, interaction.user);
|
const track = await player.add(query, interaction.user);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.appended(track)] });
|
await interaction.editReply(replies.appended(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!player.resource) {
|
if (!player.resource) {
|
||||||
player.play();
|
await player.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +52,7 @@ const data = new SlashCommandBuilder()
|
|||||||
.setDescription("Launch player and add songs to the queue.")
|
.setDescription("Launch player and add songs to the queue.")
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option.setName("query").setDescription("Title or URL of the song.")
|
option.setName("query").setDescription("Title or URL of the song.")
|
||||||
);
|
)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -3,75 +3,84 @@ import {
|
|||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ButtonInteraction,
|
ButtonInteraction,
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
CommandInteraction,
|
ChatInputCommandInteraction,
|
||||||
Message,
|
|
||||||
MessageActionRowComponentBuilder,
|
MessageActionRowComponentBuilder,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder
|
||||||
TextChannel
|
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
if (!interaction.guild || !interaction.channel) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
return;
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
}
|
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
|
||||||
|
|
||||||
if (!player) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
if (player.queue.empty()) {
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
await interaction.editReply({ embeds: [I18n.embeds.queueEmpty()] });
|
|
||||||
|
if (!player) {
|
||||||
|
await interaction.editReply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let page = 0;
|
if (player.queue.empty()) {
|
||||||
|
await interaction.editReply(replies.queue(player.queue));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = 1;
|
||||||
|
|
||||||
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
|
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
|
||||||
new ButtonBuilder().setCustomId("QueueTop").setLabel("🔼").setStyle(ButtonStyle.Primary),
|
new ButtonBuilder().setCustomId("Top").setLabel("🔼").setStyle(ButtonStyle.Primary),
|
||||||
new ButtonBuilder().setCustomId("QueueUp").setLabel("🔺").setStyle(ButtonStyle.Secondary),
|
new ButtonBuilder().setCustomId("Up").setLabel("🔺").setStyle(ButtonStyle.Secondary),
|
||||||
new ButtonBuilder().setCustomId("QueueDown").setLabel("🔻").setStyle(ButtonStyle.Secondary),
|
new ButtonBuilder().setCustomId("Down").setLabel("🔻").setStyle(ButtonStyle.Secondary),
|
||||||
new ButtonBuilder().setCustomId("QueueEnd").setLabel("🔽").setStyle(ButtonStyle.Primary)
|
new ButtonBuilder().setCustomId("End").setLabel("🔽").setStyle(ButtonStyle.Primary)
|
||||||
);
|
);
|
||||||
|
|
||||||
const message = (await interaction.editReply({
|
const message = await interaction.editReply({
|
||||||
embeds: [I18n.embeds.queueDesigner(player.queue, 0, player.resource)],
|
...replies.queue(player.queue, 0, player.resource),
|
||||||
components: [row]
|
components: [row]
|
||||||
})) as Message;
|
});
|
||||||
|
|
||||||
const collector = message.createMessageComponentCollector({ time: 30000 });
|
const collector = message.createMessageComponentCollector({ time: 30000 });
|
||||||
|
|
||||||
collector.on("collect", async (btn: ButtonInteraction) => {
|
collector.on("collect", async (btn: ButtonInteraction) => {
|
||||||
switch (btn.customId) {
|
switch (btn.customId) {
|
||||||
case "QueueTop": {
|
case "Top": {
|
||||||
page = 0;
|
position = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "QueueUp": {
|
case "Up": {
|
||||||
page = Math.max(page - 1, 0);
|
position = Math.max(position - 10, 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "QueueDown": {
|
case "Down": {
|
||||||
page = Math.min(page + 1, Math.floor(player.queue.tracks.length / 11));
|
position = Math.min(
|
||||||
|
position + 10,
|
||||||
|
Math.floor(player.queue.tracks.length / 10) * 10
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "QueueEnd":
|
case "End": {
|
||||||
page = Math.floor(player.queue.tracks.length / 11);
|
position = Math.floor(player.queue.tracks.length / 10) * 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await btn.update({
|
await btn.update(replies.queue(player.queue, position - 1, player.resource));
|
||||||
embeds: [I18n.embeds.queueDesigner(player.queue, page, player.resource)]
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("queue").setDescription("Print out the queue.");
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("queue")
|
||||||
|
.setDescription("Print interactive queue pointing to current track.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const query = interaction.options.get("query")?.value as string;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!interaction.guild || !query || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const query = interaction.options.getString("query", true);
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +27,7 @@ async function execute(client: Client, interaction: CommandInteraction) {
|
|||||||
? player.remove(parseInt(query) - 1)
|
? player.remove(parseInt(query) - 1)
|
||||||
: player.remove(query);
|
: player.remove(query);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.removed(track)] });
|
await interaction.reply(replies.removed(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
@@ -30,6 +35,7 @@ const data = new SlashCommandBuilder()
|
|||||||
.setDescription("Remove specific track in the queue.")
|
.setDescription("Remove specific track in the queue.")
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option.setName("query").setDescription("Position or title to remove.").setRequired(true)
|
option.setName("query").setDescription("Position or title to remove.").setRequired(true)
|
||||||
);
|
)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
if (!interaction.guild || !interaction.channel) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.shuffle();
|
player.shuffle();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.reshuffle()] });
|
await interaction.reply(replies.reshuffle());
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("reshuffle").setDescription("Shuffle the queue.");
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("reshuffle")
|
||||||
|
.setDescription("Reshuffle the queue. (Will change queue order)")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,38 +1,50 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const hours = (interaction.options.get("hrs")?.value as number) || 0;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
const minutes = (interaction.options.get("mins")?.value as number) || 0;
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
const seconds = (interaction.options.get("secs")?.value as number) || 0;
|
|
||||||
|
|
||||||
if (!interaction.guild || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const hours = interaction.options.getInteger("hrs") ?? 0;
|
||||||
|
const minutes = interaction.options.getInteger("mins") ?? 0;
|
||||||
|
const seconds = interaction.options.getInteger("secs") ?? 0;
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = hours * 3600 + minutes * 60 + seconds;
|
if (!player.resource) {
|
||||||
|
await interaction.reply(replies.notPlaying());
|
||||||
|
|
||||||
player.seek(time * 1000);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await interaction.reply({
|
const prevTime = player.resource.playbackDuration / 1000;
|
||||||
embeds: [I18n.embeds.fastForwarded(time)]
|
|
||||||
});
|
const nextTime = hours * 3600 + minutes * 60 + seconds;
|
||||||
|
|
||||||
|
player.seek(nextTime * 1000);
|
||||||
|
|
||||||
|
await interaction.reply(replies.seeked(prevTime, nextTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
.setName("seek")
|
.setName("seek")
|
||||||
.setDescription("Fast-forward by the time.")
|
.setDescription("Fast-forward/Fast-reverse by the given time.")
|
||||||
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
|
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
|
||||||
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
|
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
|
||||||
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."));
|
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."))
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
if (!interaction.guild || !interaction.channel) {
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.destroy();
|
player.destroy();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.destroyed()] });
|
await interaction.reply(replies.destroyed());
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder().setName("stop").setDescription("Destroy the player.");
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("stop")
|
||||||
|
.setDescription("Destroy the player.")
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, interaction: CommandInteraction) {
|
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
const volume = (interaction.options.get("volume")?.value as number) || 100;
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
if (!interaction.guild || !interaction.channel) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = controller.get(interaction.guild.id, interaction.channel as TextChannel);
|
const volume = interaction.options.getInteger("volume") ?? 100;
|
||||||
|
|
||||||
|
const player = controller.get(interaction.guild, interaction.channel);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
|
await interaction.reply(replies.notExists());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.volume = volume;
|
player.volume = volume;
|
||||||
|
|
||||||
await interaction.reply({ embeds: [I18n.embeds.volumeSet(player.volume)] });
|
await interaction.reply(replies.volumeSet(player.volume));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = new SlashCommandBuilder()
|
const data = new SlashCommandBuilder()
|
||||||
@@ -27,6 +32,7 @@ const data = new SlashCommandBuilder()
|
|||||||
.setDescription("Change player's volume amount.")
|
.setDescription("Change player's volume amount.")
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option.setName("volume").setDescription("Volume in percents.").setRequired(true)
|
option.setName("volume").setDescription("Volume in percents.").setRequired(true)
|
||||||
);
|
)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
export { data, execute };
|
export { data, execute };
|
||||||
@@ -1,23 +1,31 @@
|
|||||||
import { VoiceState } from "discord.js";
|
import { VoiceState } from "discord.js";
|
||||||
|
|
||||||
import Client from "../../../lib/client.js";
|
import Client from "../../../lib/client.js";
|
||||||
import I18n from "../lib/i18n.js";
|
|
||||||
import { asleep } from "../lib/misc.js";
|
import { asleep } from "../lib/misc.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
import { controller } from "../module.js";
|
import { controller } from "../module.js";
|
||||||
|
|
||||||
async function execute(client: Client, eventee: VoiceState[]): Promise<void> {
|
const WAIT_TIME = 30_000;
|
||||||
|
|
||||||
|
async function execute(client: Client, eventee: [VoiceState]): Promise<void> {
|
||||||
const [oldState] = eventee;
|
const [oldState] = eventee;
|
||||||
|
|
||||||
const player = controller.get(oldState.guild.id);
|
const player = controller.get(oldState.guild);
|
||||||
|
|
||||||
if (!player || !oldState.channel || !client.user) {
|
if (!player || !oldState.channel || !client.user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldState.channel.members.filter((value) => value.user !== client.user).size === 0) {
|
if (
|
||||||
await asleep(30000);
|
oldState.channel.members.size === 1 &&
|
||||||
if (oldState.channel.members.filter((value) => value.user !== client.user).size === 0) {
|
oldState.channel.members.first()?.user === client.user
|
||||||
await player.channel.send({ embeds: [I18n.embeds.fullyAlone()] });
|
) {
|
||||||
|
await asleep(WAIT_TIME);
|
||||||
|
if (
|
||||||
|
oldState.channel.members.size === 1 &&
|
||||||
|
oldState.channel.members.first()?.user === client.user
|
||||||
|
) {
|
||||||
|
await player.channel.send(replies.alone());
|
||||||
player.destroy();
|
player.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+227
-28
@@ -1,46 +1,245 @@
|
|||||||
import { execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import prismMedia from "prism-media";
|
||||||
|
import { Duplex, Readable } from "stream";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import ytdl from "ytdl-core";
|
||||||
|
|
||||||
import config from "../../../config.js";
|
import config from "../../../config.js";
|
||||||
import { YtdlArgs, YtdlResponse } from "./types.js";
|
import logger from "../../../lib/logger.js";
|
||||||
|
import { StreamOptions, YtdlDump, YtdlResult, YtdlStreamOptions } from "./types.js";
|
||||||
|
|
||||||
|
const { opus: Opus, FFmpeg } = prismMedia;
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
function argumentsResolver(args: YtdlArgs): string[] {
|
const SEARCH_DEPTH = 5;
|
||||||
const options = Object.entries(args);
|
const YTDL_EVENTS = [
|
||||||
|
"info",
|
||||||
|
"progress",
|
||||||
|
"abort",
|
||||||
|
"request",
|
||||||
|
"response",
|
||||||
|
"error",
|
||||||
|
"redirect",
|
||||||
|
"retry",
|
||||||
|
"reconnect"
|
||||||
|
];
|
||||||
|
|
||||||
return options.map(([key, value]) => {
|
function isURL(url: string): boolean {
|
||||||
if (typeof value === "boolean" && value) {
|
return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test(
|
||||||
return `--${key.split(/(?=[A-Z])/).join("-")}`.toLowerCase();
|
url
|
||||||
} 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.binariesDir, "/youtube-dl"),
|
|
||||||
[query].concat(argumentsResolver(args))
|
|
||||||
);
|
);
|
||||||
return JSON.parse(executable.stdout);
|
}
|
||||||
|
|
||||||
|
async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> {
|
||||||
|
if (depth === SEARCH_DEPTH) {
|
||||||
|
logger.warn("yt-dlp failed to find anything.", { query });
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const executable = await execFileAsync(path.join(config.binariesDir, "/yt-dlp"), [
|
||||||
|
query,
|
||||||
|
"-O %(.{id,original_url,title,uploader,duration,extractor_key,thumbnail})j",
|
||||||
|
"-s",
|
||||||
|
"--flat-playlist"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dump: YtdlDump[] = JSON.parse("[" + executable.stdout.replace(/\n(?=.+)/g, ",") + "]");
|
||||||
|
|
||||||
|
const result: YtdlResult[] = [];
|
||||||
|
|
||||||
|
for (const entry of dump) {
|
||||||
|
if (entry.title && entry.thumbnail && entry.duration && entry.extractor_key === "Youtube") {
|
||||||
|
result.push({
|
||||||
|
url: entry.original_url,
|
||||||
|
title: entry.title,
|
||||||
|
thumbnail: entry.thumbnail,
|
||||||
|
duration: entry.duration
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("yt-dlp has found corresponding youtube video.", {
|
||||||
|
result: result[result.length - 1]
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unwind: YtdlResult[];
|
||||||
|
|
||||||
|
if (entry.extractor_key !== "Youtube" && entry.title) {
|
||||||
|
unwind = await fetchInfoRecursive(
|
||||||
|
`ytsearch1:${entry.uploader ? entry.uploader + " - " : ""} ${entry.title}`,
|
||||||
|
depth + 1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unwind = await fetchInfoRecursive(entry.original_url, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unwind.length !== 0) {
|
||||||
|
result.push(unwind[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInfo(query: string): Promise<YtdlResult[]> {
|
||||||
|
logger.info("yt-dlp was invoked.", { query });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isURL(query)) {
|
||||||
|
return await fetchInfoRecursive("ytsearch1:" + query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetchInfoRecursive(query);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error("yt-dlp execution invoked error.", { error });
|
||||||
return;
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStream(
|
export function ytdlStream(url: string, options?: YtdlStreamOptions) {
|
||||||
query: string,
|
logger.info("Created ytdl stream.", { url, options });
|
||||||
args: YtdlArgs
|
|
||||||
): Promise<YtdlResponse | undefined> {
|
|
||||||
let response = await ytdlExec(query, args);
|
|
||||||
|
|
||||||
if (response && !response.extractor.includes("youtube")) {
|
let ffmpegArgs: string[] = [
|
||||||
response = await ytdlExec(`${response.title} - ${response.creator}`, args);
|
"-analyzeduration",
|
||||||
|
"0",
|
||||||
|
"-loglevel",
|
||||||
|
"0",
|
||||||
|
"-f",
|
||||||
|
`${options?.fmt ? options.fmt : "s16le"}`,
|
||||||
|
"-ar",
|
||||||
|
"48000",
|
||||||
|
"-ac",
|
||||||
|
"2"
|
||||||
|
];
|
||||||
|
|
||||||
|
if (options?.seek) {
|
||||||
|
ffmpegArgs.unshift("-ss", options.seek.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
if (options?.encoderArgs) {
|
||||||
|
ffmpegArgs = ffmpegArgs.concat(options.encoderArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcoder = new FFmpeg({
|
||||||
|
args: ffmpegArgs
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputStream = ytdl(url, options);
|
||||||
|
const transcoderStream = inputStream.pipe(transcoder);
|
||||||
|
|
||||||
|
if (options && !options.opusEncoded) {
|
||||||
|
YTDL_EVENTS.forEach((event) =>
|
||||||
|
inputStream.on(event, (...args) => transcoderStream.emit(event, ...args))
|
||||||
|
);
|
||||||
|
inputStream.on("error", () => transcoder.destroy());
|
||||||
|
transcoderStream.on("close", () => transcoder.destroy());
|
||||||
|
|
||||||
|
return transcoderStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opus = new Opus.Encoder({
|
||||||
|
rate: 48000,
|
||||||
|
channels: 2,
|
||||||
|
frameSize: 960
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputStream = transcoderStream.pipe(opus);
|
||||||
|
|
||||||
|
transcoderStream.on("error", (e) => outputStream.emit("error", e));
|
||||||
|
|
||||||
|
YTDL_EVENTS.forEach((event) =>
|
||||||
|
inputStream.on(event, (...args) => outputStream.emit(event, ...args))
|
||||||
|
);
|
||||||
|
|
||||||
|
outputStream.on("close", () => {
|
||||||
|
transcoder.destroy();
|
||||||
|
opus.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arbitraryStream(stream: string | Readable | Duplex, options?: StreamOptions) {
|
||||||
|
logger.info("Created arbitrary stream.", { stream, options });
|
||||||
|
|
||||||
|
let ffmpegArgs: string[];
|
||||||
|
|
||||||
|
if (typeof stream === "string") {
|
||||||
|
ffmpegArgs = [
|
||||||
|
"-reconnect",
|
||||||
|
"1",
|
||||||
|
"-reconnect_streamed",
|
||||||
|
"1",
|
||||||
|
"-reconnect_delay_max",
|
||||||
|
"5",
|
||||||
|
"-i",
|
||||||
|
stream,
|
||||||
|
"-analyzeduration",
|
||||||
|
"0",
|
||||||
|
"-loglevel",
|
||||||
|
"0",
|
||||||
|
"-f",
|
||||||
|
`${options?.fmt ? options.fmt : "s16le"}`,
|
||||||
|
"-ar",
|
||||||
|
"48000",
|
||||||
|
"-ac",
|
||||||
|
"2"
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
ffmpegArgs = [
|
||||||
|
"-analyzeduration",
|
||||||
|
"0",
|
||||||
|
"-loglevel",
|
||||||
|
"0",
|
||||||
|
"-f",
|
||||||
|
`${options?.fmt ? options.fmt : "s16le"}`,
|
||||||
|
"-ar",
|
||||||
|
"48000",
|
||||||
|
"-ac",
|
||||||
|
"2"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.seek) {
|
||||||
|
ffmpegArgs.unshift("-ss", options.seek.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.encoderArgs) {
|
||||||
|
ffmpegArgs = ffmpegArgs.concat(options.encoderArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transcoder = new FFmpeg({
|
||||||
|
args: ffmpegArgs
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof stream !== "string") {
|
||||||
|
transcoder = stream.pipe(transcoder);
|
||||||
|
|
||||||
|
stream.on("error", () => transcoder.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && !options.opusEncoded) {
|
||||||
|
transcoder.on("close", () => transcoder.destroy());
|
||||||
|
|
||||||
|
return transcoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opus = new Opus.Encoder({
|
||||||
|
rate: 48000,
|
||||||
|
channels: 2,
|
||||||
|
frameSize: 960
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputStream = transcoder.pipe(opus);
|
||||||
|
|
||||||
|
outputStream.on("close", () => {
|
||||||
|
transcoder.destroy();
|
||||||
|
opus.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputStream;
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,57 @@
|
|||||||
export default class Filter {
|
export default class Filter {
|
||||||
public static readonly filters: Record<string, string> = {
|
public static readonly filters = {
|
||||||
bassboost_low: "bass=g=15:f=110:w=0.3",
|
"Bassboost 1.5": "bass=g=15:f=110:w=0.3",
|
||||||
bassboost: "bass=g=20:f=110:w=0.3",
|
"bassboost 2.0": "bass=g=20:f=110:w=0.3",
|
||||||
bassboost_high: "bass=g=30:f=110:w=0.3",
|
"bassboost 3.0": "bass=g=30:f=110:w=0.3",
|
||||||
"8D": "apulsator=hz=0.09",
|
"8D": "apulsator=hz=0.09",
|
||||||
vaporwave: "aresample=48000,asetrate=48000*0.75",
|
Daycore: "aresample=48000,asetrate=48000*0.75",
|
||||||
nightcore: "aresample=48000,asetrate=48000*1.25",
|
Nightcore: "aresample=48000,asetrate=48000*1.25",
|
||||||
phaser: "aphaser=in_gain=0.4",
|
Phaser: "aphaser=in_gain=0.4",
|
||||||
tremolo: "tremolo",
|
Tremolo: "tremolo",
|
||||||
vibrato: "vibrato=f=6.5",
|
Vibrato: "vibrato=f=6.5",
|
||||||
reverse: "areverse",
|
Reverse: "areverse",
|
||||||
treble: "treble=g=5",
|
Treble: "treble=g=5",
|
||||||
normalizer: "dynaudnorm=g=101",
|
"Normalizer 1": "dynaudnorm=g=101",
|
||||||
normalizer2: "acompressor",
|
"Normalizer 2": "acompressor",
|
||||||
surrounding: "surround",
|
"Surround Sound": "surround",
|
||||||
pulsator: "apulsator=hz=1",
|
Pulsator: "apulsator=hz=1",
|
||||||
subboost: "asubboost",
|
Subboost: "asubboost",
|
||||||
karaoke: "stereotools=mlev=0.03",
|
Karaoke: "stereotools=mlev=0.03",
|
||||||
flanger: "flanger",
|
Compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6",
|
||||||
gate: "agate",
|
Expander:
|
||||||
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",
|
"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",
|
"Soft Limiter": "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",
|
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",
|
"Fade In": "afade=t=in:ss=0:d=10",
|
||||||
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",
|
Dimming:
|
||||||
fadein: "afade=t=in:ss=0:d=10",
|
"afftfilt=\"'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'\"",
|
||||||
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"
|
||||||
earrape: "channelsplit,sidechaingate=level_in=64"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private switches = new Set<string>();
|
private switches_ = new Set<string>();
|
||||||
|
|
||||||
public toggled(): [string, string][] {
|
public toggled(): [string, string][] {
|
||||||
return Object.entries(Filter.filters)
|
return Object.entries(Filter.filters)
|
||||||
.filter((value) => this.switches.has(value[0]))
|
.filter((value) => this.switches_.has(value[0]))
|
||||||
.map((value) => [value[0], value[1]]);
|
.map((value) => [value[0], value[1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggle(query?: string): void {
|
public toggle(query?: string): boolean {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
this.switches.clear();
|
this.switches_.clear();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.switches.delete(query) && Object.hasOwn(Filter.filters, query)) {
|
if (!this.switches_.delete(query) && Object.hasOwn(Filter.filters, query)) {
|
||||||
this.switches.add(query);
|
this.switches_.add(query);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public size(): number {
|
public size(): number {
|
||||||
return this.switches.size;
|
return this.switches_.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public empty(): boolean {
|
public empty(): boolean {
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
import { AudioResource } from "@discordjs/voice";
|
|
||||||
import { EmbedBuilder } from "discord.js";
|
|
||||||
|
|
||||||
import { loopMoji, secToISO, statusBarGenerator, volumeMoji } from "./misc.js";
|
|
||||||
import Player from "./player.js";
|
|
||||||
import Queue from "./queue.js";
|
|
||||||
import { LoopOption, Track } from "./types.js";
|
|
||||||
|
|
||||||
export default class I18n {
|
|
||||||
public static readonly embeds = {
|
|
||||||
nowPlaying: (track: Track) =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("LuminousVividPink")
|
|
||||||
.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) =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Green")
|
|
||||||
.setTitle(`**♻️ ${amount} tracks have been cleared from the queue.**`)
|
|
||||||
.setTimestamp(new Date()),
|
|
||||||
okConnected: (channel: string) =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Green")
|
|
||||||
.setTitle(`**✅ Successfuly connected to:** _${channel}_.`),
|
|
||||||
notConnected: () =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Red")
|
|
||||||
.setTitle("**🚫 Make yourself connected to a voice channel beforehand.**"),
|
|
||||||
wasConnected: () =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Yellow")
|
|
||||||
.setTitle("**⚠️ Already connected to a voice channel.**"),
|
|
||||||
filtered: (filter?: string) =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Green")
|
|
||||||
.setTitle(
|
|
||||||
filter
|
|
||||||
? `**✅ ${filter} was applied to the player.**`
|
|
||||||
: "**✅ All filters were declined.**"
|
|
||||||
),
|
|
||||||
appended: (track?: Track) =>
|
|
||||||
!track
|
|
||||||
? new EmbedBuilder()
|
|
||||||
.setColor("Red")
|
|
||||||
.setTitle("**🚫 Could not add track to the queue.**")
|
|
||||||
: new EmbedBuilder()
|
|
||||||
.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) =>
|
|
||||||
state
|
|
||||||
? new EmbedBuilder().setColor("Green").setTitle("_**Perfoming jump.**_")
|
|
||||||
: new EmbedBuilder().setColor("Red").setTitle("**❗ Bad _jump_ request.**"),
|
|
||||||
looped: (query: LoopOption) => {
|
|
||||||
switch (query) {
|
|
||||||
case "AUTO": {
|
|
||||||
return new EmbedBuilder()
|
|
||||||
.setColor("LuminousVividPink")
|
|
||||||
.setTitle("**🔀 Shuffle mode** _(does not affect the queue order)_.");
|
|
||||||
}
|
|
||||||
case "LOOP": {
|
|
||||||
return new EmbedBuilder()
|
|
||||||
.setColor("Blue")
|
|
||||||
.setTitle("**🔁 Looping over the queue.**");
|
|
||||||
}
|
|
||||||
case "NONE": {
|
|
||||||
return new EmbedBuilder()
|
|
||||||
.setColor("Red")
|
|
||||||
.setTitle("**⤵️ Player will stop on the queue end.**");
|
|
||||||
}
|
|
||||||
case "SONG": {
|
|
||||||
return new EmbedBuilder()
|
|
||||||
.setColor("Yellow")
|
|
||||||
.setTitle("**🔂 Looping a single track.**");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
skipped: () => new EmbedBuilder().setColor("Green").setTitle("**⏭️ Skipping.**"),
|
|
||||||
paused: (state: boolean) =>
|
|
||||||
state
|
|
||||||
? new EmbedBuilder().setColor("Green").setTitle("_**▶️ Resumed.**_")
|
|
||||||
: new EmbedBuilder().setColor("Yellow").setTitle("_**⏸️ Paused.**_"),
|
|
||||||
removed: (track?: Track) =>
|
|
||||||
track
|
|
||||||
? new EmbedBuilder()
|
|
||||||
.setColor("Green")
|
|
||||||
.setTitle(track.title)
|
|
||||||
.setDescription("**was removed from the queue.**")
|
|
||||||
.setTimestamp(new Date())
|
|
||||||
.setURL(track.url)
|
|
||||||
.setThumbnail(track.thumbnail)
|
|
||||||
: new EmbedBuilder().setColor("Red").setTitle("**❗ Bad _remove_ request.**"),
|
|
||||||
fastForwarded: (amount: number) =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Blue")
|
|
||||||
.setTitle(`**⏩ Fast-forwarding to ${secToISO(amount)}**`),
|
|
||||||
destroyed: () =>
|
|
||||||
new EmbedBuilder().setColor("Red").setTitle("**💀 Destroying the player.**"),
|
|
||||||
volumeSet: (amount: number) =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("Green")
|
|
||||||
.setTitle(`**${volumeMoji(amount)} Volume has been set to ${amount}%**`),
|
|
||||||
queueEmpty: () => new EmbedBuilder().setColor("Yellow").setTitle("**❎ Queue is empty.**"),
|
|
||||||
playerInfo: (player?: Player) => {
|
|
||||||
if (!player) {
|
|
||||||
return new EmbedBuilder()
|
|
||||||
.setColor("Red")
|
|
||||||
.setTitle("**🚫 Player does not exist.**");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player.resource) {
|
|
||||||
return new EmbedBuilder()
|
|
||||||
.setColor("LuminousVividPink")
|
|
||||||
.setTitle(`${player.queue.position + 1}. ${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
|
|
||||||
.toggled()
|
|
||||||
.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 EmbedBuilder()
|
|
||||||
.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
|
|
||||||
.toggled()
|
|
||||||
.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 EmbedBuilder()
|
|
||||||
.setColor("LuminousVividPink")
|
|
||||||
.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) => ({
|
|
||||||
name: `${page * 10 + index + 1}. ${value.title.padEnd(35)}`,
|
|
||||||
value: `**[${secToISO(value.duration)}]**`
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
fullyAlone: () =>
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor("LuminousVividPink")
|
|
||||||
.setTitle("🥬 I have decided to leave to conserve my energy.")
|
|
||||||
.setDescription("_**Since there was nobody in the voice channel.**_"),
|
|
||||||
reshuffle: () =>
|
|
||||||
new EmbedBuilder().setColor("Green").setTitle("**✅ Successfuly reshuffled queue.**")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import { LoopOption } from "./types.js";
|
import { AudioResource } from "@discordjs/voice";
|
||||||
|
|
||||||
const PLAYBACK_LENGTH = 15;
|
import { LoopOption, Track } from "./types.js";
|
||||||
|
|
||||||
export function statusBarGenerator(playback: number, duration: number): string {
|
const STATUSBAR_LENGTH = 15;
|
||||||
const filled = Array<string>(
|
|
||||||
Math.floor((PLAYBACK_LENGTH * playback) / (duration === 0 ? 1 : duration))
|
|
||||||
).fill("🔹");
|
|
||||||
|
|
||||||
const empty = Array<string>(
|
export function statusBarGenerator(resource?: AudioResource<Track>): string {
|
||||||
PLAYBACK_LENGTH - Math.floor((PLAYBACK_LENGTH * playback) / (duration === 0 ? 1 : duration))
|
const duration = resource?.metadata.duration || 1;
|
||||||
).fill("▫️");
|
const playback = Math.min((resource?.playbackDuration || 0) / 1000, duration);
|
||||||
|
|
||||||
return `**[${filled.join("")}${empty.join("")}] ${secToISO(playback)}/${secToISO(duration)}**`;
|
const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹");
|
||||||
|
|
||||||
|
const empty = Array<string>(STATUSBAR_LENGTH - filled.length).fill("▫️");
|
||||||
|
|
||||||
|
return `[${filled.join("")}${empty.join("")}] ${toISOTime(playback)}/${toISOTime(duration)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function secToISO(amount: number): string {
|
export function toISOTime(amount: number): string {
|
||||||
const h = Math.floor(amount / 3600);
|
const h = Math.floor(amount / 3600);
|
||||||
const m = Math.floor((amount % 3600) / 60);
|
const m = Math.floor((amount % 3600) / 60);
|
||||||
const s = Math.floor((amount % 3600) % 60);
|
const s = Math.floor((amount % 3600) % 60);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Collection, Guild, TextChannel } from "discord.js";
|
import { Collection, Guild, GuildTextBasedChannel } from "discord.js";
|
||||||
|
|
||||||
|
import logger from "../../../lib/logger.js";
|
||||||
import Player from "./player.js";
|
import Player from "./player.js";
|
||||||
|
|
||||||
export default class Music {
|
export default class Music {
|
||||||
private players = new Collection<string, Player>();
|
private players_ = new Collection<string, Player>();
|
||||||
|
|
||||||
public get(guildId: string, channel?: TextChannel): Player | undefined {
|
public get(guild: Guild, channel?: GuildTextBasedChannel): Player | undefined {
|
||||||
const player = this.players.get(guildId);
|
const player = this.players_.get(guild.id);
|
||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
if (channel) {
|
if (channel) {
|
||||||
@@ -17,15 +18,19 @@ export default class Music {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(guild: Guild, channel: TextChannel): Player {
|
public set(guild: Guild, channel: GuildTextBasedChannel): Player {
|
||||||
const player = new Player(guild, this, channel);
|
const player = new Player(guild, this, channel);
|
||||||
|
|
||||||
this.players.set(guild.id, player);
|
this.players_.set(guild.id, player);
|
||||||
|
|
||||||
|
logger.info("Player was created.", { player });
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(guildId: string): void {
|
public delete(guildId: string): void {
|
||||||
this.players.delete(guildId);
|
logger.info("Player was deleted.", { guildId });
|
||||||
|
|
||||||
|
this.players_.delete(guildId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+81
-122
@@ -5,60 +5,57 @@ import {
|
|||||||
createAudioPlayer,
|
createAudioPlayer,
|
||||||
createAudioResource,
|
createAudioResource,
|
||||||
DiscordGatewayAdapterCreator,
|
DiscordGatewayAdapterCreator,
|
||||||
entersState,
|
|
||||||
joinVoiceChannel,
|
joinVoiceChannel,
|
||||||
VoiceConnection,
|
VoiceConnection
|
||||||
VoiceConnectionStatus
|
|
||||||
} from "@discordjs/voice";
|
} from "@discordjs/voice";
|
||||||
import { Guild, Message, TextChannel, User, VoiceBasedChannel } from "discord.js";
|
import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js";
|
||||||
import ytdl from "discord-ytdl-core";
|
|
||||||
|
|
||||||
import { fetchStream } from "./fetch.js";
|
import { fetchInfo, ytdlStream } from "./fetch.js";
|
||||||
import Filter from "./filter.js";
|
import Filter from "./filter.js";
|
||||||
import I18n from "./i18n.js";
|
|
||||||
import { largestCommonSequence } from "./misc.js";
|
import { largestCommonSequence } from "./misc.js";
|
||||||
import Music from "./music.js";
|
import Music from "./music.js";
|
||||||
import Queue from "./queue.js";
|
import Queue from "./queue.js";
|
||||||
|
import replies from "./replies.js";
|
||||||
import { Track } from "./types.js";
|
import { Track } from "./types.js";
|
||||||
|
|
||||||
export default class Player {
|
export default class Player {
|
||||||
private music: Music;
|
private music: Music;
|
||||||
private guild: Guild;
|
private guild: Guild;
|
||||||
|
|
||||||
private msgChannel: TextChannel;
|
private channel_: GuildTextBasedChannel;
|
||||||
public get channel(): TextChannel {
|
public get channel(): GuildTextBasedChannel {
|
||||||
return this.msgChannel;
|
return this.channel_;
|
||||||
}
|
}
|
||||||
public set channel(x: TextChannel) {
|
public set channel(x: GuildTextBasedChannel) {
|
||||||
this.msgChannel = x;
|
this.channel_ = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
private message: Message | undefined;
|
private message_?: Message;
|
||||||
|
|
||||||
public internalConnection: VoiceConnection | undefined;
|
private connection_?: VoiceConnection;
|
||||||
public get connection(): VoiceConnection | undefined {
|
public get connection(): VoiceConnection | undefined {
|
||||||
return this.internalConnection;
|
return this.connection_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private internalPlayer: AudioPlayer;
|
private player_: AudioPlayer;
|
||||||
public get player(): AudioPlayer {
|
public get player(): AudioPlayer {
|
||||||
return this.internalPlayer;
|
return this.player_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private playerResource: AudioResource<Track> | undefined;
|
private resource_: AudioResource<Track> | undefined;
|
||||||
public get resource(): AudioResource<Track> | undefined {
|
public get resource(): AudioResource<Track> | undefined {
|
||||||
return this.playerResource;
|
return this.resource_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resourceVolume = 100;
|
private volume_ = 100;
|
||||||
public get volume(): number {
|
public get volume(): number {
|
||||||
return this.resourceVolume;
|
return this.volume_;
|
||||||
}
|
}
|
||||||
public set volume(x: number) {
|
public set volume(x: number) {
|
||||||
this.resourceVolume = Math.max(0, Math.min(150, x));
|
this.volume_ = Math.max(0, Math.min(150, x));
|
||||||
|
|
||||||
if (this.playerResource && this.playerResource.volume) {
|
if (this.resource_ && this.resource_.volume) {
|
||||||
this.playerResource.volume.setVolumeLogarithmic(this.resourceVolume / 100);
|
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,149 +63,109 @@ export default class Player {
|
|||||||
|
|
||||||
public readonly filter = new Filter();
|
public readonly filter = new Filter();
|
||||||
|
|
||||||
public readonly created: Date = new Date();
|
public readonly createdAt: Date = new Date();
|
||||||
|
|
||||||
public constructor(guild: Guild, music: Music, textChannel: TextChannel) {
|
public constructor(guild: Guild, music: Music, channel: GuildTextBasedChannel) {
|
||||||
this.music = music;
|
this.music = music;
|
||||||
this.guild = guild;
|
this.guild = guild;
|
||||||
this.msgChannel = textChannel;
|
this.channel_ = channel;
|
||||||
|
|
||||||
this.internalPlayer = createAudioPlayer();
|
this.player_ = createAudioPlayer();
|
||||||
|
|
||||||
this.internalPlayer.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
|
this.player_.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
|
||||||
}
|
}
|
||||||
|
|
||||||
private resourceGenerator(track: Track, seek?: number) {
|
private resourceGenerator(track: Track, seek?: number) {
|
||||||
const stream = ytdl(track.url, {
|
const stream = ytdlStream(track.url, {
|
||||||
opusEncoded: false,
|
opusEncoded: true,
|
||||||
fmt: "mp3",
|
|
||||||
filter: "audioonly",
|
filter: "audioonly",
|
||||||
highWaterMark: 1 << 25,
|
highWaterMark: 1 << 25,
|
||||||
seek: seek ? seek / 1000 : undefined,
|
seek: seek ? seek / 1000 : undefined,
|
||||||
encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
|
encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
this.playerResource = createAudioResource(stream, {
|
this.resource_ = createAudioResource(stream, {
|
||||||
inlineVolume: true,
|
inlineVolume: true,
|
||||||
metadata: track
|
metadata: track
|
||||||
});
|
});
|
||||||
|
|
||||||
if (seek) {
|
if (seek) {
|
||||||
this.playerResource.playbackDuration = seek;
|
this.resource_.playbackDuration = seek;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.playerResource.volume) {
|
if (this.resource_.volume) {
|
||||||
this.playerResource.volume.setVolumeLogarithmic(this.resourceVolume / 100);
|
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.playerResource;
|
return this.resource_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resourceEndResolvable(): void {
|
private resourceEndResolvable(): void {
|
||||||
if (this.playerResource) {
|
if (this.resource_) {
|
||||||
this.playerResource.encoder?.destroy();
|
this.resource_.encoder?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.queue.next()) {
|
if (this.queue.next()) {
|
||||||
this.play();
|
this.play();
|
||||||
} else {
|
} else {
|
||||||
this.playerResource = undefined;
|
this.player_.stop(true);
|
||||||
this.internalPlayer.stop();
|
this.resource_ = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async messageResolvable(track: Track) {
|
private async messageResolvable(track: Track) {
|
||||||
if (this.message) {
|
if (this.message_?.deletable) {
|
||||||
await this.message.delete();
|
await this.message_.delete().catch();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.message = await this.msgChannel.send({
|
this.message_ = await this.channel_.send(replies.nowPlaying(track));
|
||||||
embeds: [I18n.embeds.nowPlaying(track)]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async search(query: string, requester: User): Promise<Track[] | undefined> {
|
private async search(query: string, requester: User): Promise<Track[] | undefined> {
|
||||||
const response = await fetchStream(query, {
|
const info = await fetchInfo(query);
|
||||||
dumpSingleJson: true,
|
|
||||||
noWarnings: true,
|
|
||||||
noCheckCertificate: true,
|
|
||||||
flatPlaylist: true,
|
|
||||||
skipDownload: true,
|
|
||||||
defaultSearch: "ytsearch"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response) {
|
if (info.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tracks: Track[] = [];
|
return info.map((value) => {
|
||||||
|
return { ...value, requester: requester };
|
||||||
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?: number): Promise<void> {
|
||||||
}
|
|
||||||
|
|
||||||
public play(seek?: number): void {
|
|
||||||
const track = this.queue.current();
|
const track = this.queue.current();
|
||||||
|
|
||||||
if (!this.internalConnection || !track) {
|
if (!this.connection_ || !track) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.internalPlayer.play(this.resourceGenerator(track, seek));
|
this.player_.play(this.resourceGenerator(track, seek));
|
||||||
|
|
||||||
this.internalConnection.subscribe(this.internalPlayer);
|
this.connection_.subscribe(this.player_);
|
||||||
|
|
||||||
this.messageResolvable(track);
|
await this.messageResolvable(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connect(channel: VoiceBasedChannel): void {
|
public connect(channel: VoiceBasedChannel): void {
|
||||||
if (this.internalConnection) {
|
if (
|
||||||
this.internalConnection.destroy();
|
this.connection_?.joinConfig.channelId === channel.id &&
|
||||||
|
this.connection_.joinConfig.guildId === channel.guildId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.internalConnection = joinVoiceChannel({
|
if (this.connection_) {
|
||||||
|
this.connection_.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connection_ = joinVoiceChannel({
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
guildId: channel.guildId,
|
guildId: channel.guildId,
|
||||||
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
|
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
|
||||||
});
|
});
|
||||||
|
|
||||||
this.internalConnection.on(VoiceConnectionStatus.Disconnected, async () => {
|
this.connection_.subscribe(this.player_);
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
entersState(
|
|
||||||
this.internalConnection as VoiceConnection,
|
|
||||||
VoiceConnectionStatus.Signalling,
|
|
||||||
10_000
|
|
||||||
),
|
|
||||||
entersState(
|
|
||||||
this.internalConnection as VoiceConnection,
|
|
||||||
VoiceConnectionStatus.Connecting,
|
|
||||||
10_000
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
|
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
|
||||||
@@ -223,7 +180,7 @@ export default class Player {
|
|||||||
return tracks[0];
|
return tracks[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public jump(query: number | string): boolean {
|
public jump(query: number | string): number | undefined {
|
||||||
let position = typeof query === "number" ? query : -1;
|
let position = typeof query === "number" ? query : -1;
|
||||||
let diff = 0;
|
let diff = 0;
|
||||||
|
|
||||||
@@ -239,10 +196,10 @@ export default class Player {
|
|||||||
|
|
||||||
if (position !== -1 && this.queue.jump(position)) {
|
if (position !== -1 && this.queue.jump(position)) {
|
||||||
this.play();
|
this.play();
|
||||||
return true;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(query?: number | string): Track | undefined {
|
public remove(query?: number | string): Track | undefined {
|
||||||
@@ -279,28 +236,28 @@ export default class Player {
|
|||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.queue.clear();
|
this.queue.clear();
|
||||||
this.playerResource = undefined;
|
this.resource_ = undefined;
|
||||||
|
|
||||||
if (this.internalPlayer) {
|
if (this.player_) {
|
||||||
this.internalPlayer.stop();
|
this.player_.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
if (this.internalConnection) {
|
if (this.connection_) {
|
||||||
this.internalConnection.destroy();
|
this.connection_.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.internalPlayer.stop();
|
this.player_.stop();
|
||||||
this.music.delete(this.guild.id);
|
this.music.delete(this.guild.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): boolean {
|
public pause(): boolean {
|
||||||
if (this.internalPlayer.state.status === AudioPlayerStatus.Paused) {
|
if (this.player_.state.status === AudioPlayerStatus.Paused) {
|
||||||
this.internalPlayer.unpause();
|
this.player_.unpause();
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
this.internalPlayer.pause();
|
this.player_.pause();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,21 +265,23 @@ export default class Player {
|
|||||||
public shuffle(): void {
|
public shuffle(): void {
|
||||||
this.queue.shuffle();
|
this.queue.shuffle();
|
||||||
|
|
||||||
if (this.playerResource) {
|
if (this.resource_) {
|
||||||
this.internalPlayer.stop();
|
this.player_.stop();
|
||||||
this.play();
|
this.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public skip(): void {
|
public skip(): void {
|
||||||
this.internalPlayer.stop();
|
this.player_.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setFilter(filter?: string): void {
|
public setFilter(filter?: string): boolean {
|
||||||
this.filter.toggle(filter);
|
const status = this.filter.toggle(filter);
|
||||||
|
|
||||||
if (this.playerResource) {
|
if (this.resource_) {
|
||||||
this.play(this.playerResource.playbackDuration);
|
this.play(this.resource_.playbackDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,91 +1,93 @@
|
|||||||
import { LoopOption, Track } from "./types.js";
|
import { LoopOption, Track } from "./types.js";
|
||||||
|
|
||||||
export default class Queue {
|
export default class Queue {
|
||||||
private entries: Track[] = [];
|
private tracks_: Track[] = [];
|
||||||
public get tracks(): Track[] {
|
public get tracks(): Track[] {
|
||||||
return this.entries;
|
return this.tracks_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private currentIndex = 0;
|
private position_ = 0;
|
||||||
public get position(): number {
|
public get position(): number {
|
||||||
return this.currentIndex;
|
return this.position_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private loopStatus: LoopOption = "NONE";
|
private loop_: LoopOption = "NONE";
|
||||||
public get loop(): LoopOption {
|
public get loop(): LoopOption {
|
||||||
return this.loopStatus;
|
return this.loop_;
|
||||||
}
|
}
|
||||||
public set loop(x: LoopOption) {
|
public set loop(x: LoopOption) {
|
||||||
this.loopStatus = x;
|
this.loop_ = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
private overallDuration = 0;
|
private duration_ = 0;
|
||||||
public get duration(): number {
|
public get duration(): number {
|
||||||
return this.overallDuration;
|
return this.duration_;
|
||||||
}
|
}
|
||||||
|
|
||||||
public next(): Track | undefined {
|
public next(): Track | undefined {
|
||||||
if (this.loopStatus === "SONG") {
|
if (this.loop_ === "SONG") {
|
||||||
return this.current();
|
return this.current();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.loopStatus === "AUTO") {
|
if (this.loop_ === "AUTO") {
|
||||||
this.currentIndex = Math.floor(Math.random() * this.entries.length);
|
this.position_ = Math.floor(Math.random() * this.tracks_.length);
|
||||||
return this.current();
|
return this.current();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentIndex + 1 < this.entries.length) {
|
if (this.position_ + 1 < this.tracks_.length) {
|
||||||
this.currentIndex++;
|
this.position_++;
|
||||||
return this.current();
|
return this.current();
|
||||||
} else if (this.loopStatus === "LOOP") {
|
} else if (this.loop_ === "LOOP") {
|
||||||
this.currentIndex = 0;
|
this.position_ = 0;
|
||||||
return this.current();
|
return this.current();
|
||||||
} else if (this.loopStatus === "NONE") {
|
} else if (this.loop_ === "NONE") {
|
||||||
this.currentIndex = 0;
|
this.position_ = 0;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public shuffle(): void {
|
public shuffle(): void {
|
||||||
let currentIndex = this.entries.length;
|
let currentIndex = this.tracks_.length;
|
||||||
|
|
||||||
while (currentIndex != 0) {
|
while (currentIndex != 0) {
|
||||||
const randomIndex = Math.floor(Math.random() * currentIndex);
|
const randomIndex = Math.floor(Math.random() * currentIndex);
|
||||||
currentIndex--;
|
currentIndex--;
|
||||||
|
|
||||||
[this.entries[currentIndex], this.entries[randomIndex]] = [
|
[this.tracks_[currentIndex], this.tracks_[randomIndex]] = [
|
||||||
this.entries[randomIndex],
|
this.tracks_[randomIndex],
|
||||||
this.entries[currentIndex]
|
this.tracks_[currentIndex]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentIndex = 0;
|
this.position_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public push(tracks: Track[], index?: number): Track {
|
public push(tracks: Track[], index?: number): Track {
|
||||||
if (!index || index >= this.entries.length) {
|
|
||||||
this.entries = this.entries.concat(tracks);
|
|
||||||
} else {
|
|
||||||
this.entries.splice(Math.max(0, index), 0, ...tracks);
|
|
||||||
if (this.currentIndex >= index) {
|
|
||||||
this.currentIndex += tracks.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
this.overallDuration += track.duration;
|
this.duration_ += track.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.entries[index || this.entries.length - 1];
|
if (index === undefined || index >= this.tracks_.length) {
|
||||||
|
this.tracks_ = this.tracks_.concat(tracks);
|
||||||
|
|
||||||
|
return this.tracks_[this.tracks_.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tracks_.splice(Math.max(0, index), 0, ...tracks);
|
||||||
|
if (this.position_ >= index) {
|
||||||
|
this.position_ += tracks.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tracks_[Math.max(0, index)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public pop(query: number): Track | undefined {
|
public pop(query: number): Track | undefined {
|
||||||
if (query >= 0 && query < this.entries.length) {
|
if (query >= 0 && query < this.tracks_.length) {
|
||||||
const track = this.entries[query];
|
const track = this.tracks_[query];
|
||||||
|
|
||||||
this.entries.splice(query, 1);
|
this.tracks_.splice(query, 1);
|
||||||
this.currentIndex = Math.max(0, this.currentIndex - 1);
|
this.position_ = Math.max(0, this.position_ - 1);
|
||||||
this.overallDuration -= track.duration;
|
this.duration_ -= track.duration;
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +95,9 @@ export default class Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public jump(query: number): Track | undefined {
|
public jump(query: number): Track | undefined {
|
||||||
if (query >= 0 && query < this.entries.length) {
|
if (query >= 0 && query < this.tracks_.length) {
|
||||||
const track = this.entries[query];
|
const track = this.tracks_[query];
|
||||||
this.currentIndex = query;
|
this.position_ = query;
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,15 +105,15 @@ export default class Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public current(): Track {
|
public current(): Track {
|
||||||
return this.entries[this.currentIndex];
|
return this.tracks_[this.position_];
|
||||||
}
|
}
|
||||||
|
|
||||||
public empty(): boolean {
|
public empty(): boolean {
|
||||||
return this.entries.length === 0;
|
return this.tracks_.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.entries = [];
|
this.tracks_ = [];
|
||||||
this.currentIndex = 0;
|
this.position_ = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import { AudioResource } from "@discordjs/voice";
|
||||||
|
import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js";
|
||||||
|
|
||||||
|
import config from "../../../config.js";
|
||||||
|
import { loopMoji, statusBarGenerator, toISOTime, volumeMoji } from "./misc.js";
|
||||||
|
import Player from "./player.js";
|
||||||
|
import Queue from "./queue.js";
|
||||||
|
import { LoopOption, Track } from "./types.js";
|
||||||
|
|
||||||
|
const replies = {
|
||||||
|
notExists: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.warning)
|
||||||
|
.setTitle("⚠️ Command ignored - player does not exist")
|
||||||
|
.setDescription("Use '/play' to create a new one.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
searching: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setTitle("🔍 Searching...")
|
||||||
|
.setDescription("It will take longer if playlist or non-youtube URL was queried.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
notPlaying: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setTitle("💿 Insert the disk...")
|
||||||
|
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||||
|
.setDescription("Use '/play' to start the playback.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
nowPlaying: (track: Track): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setAuthor({
|
||||||
|
iconURL: track.requester.avatarURL() ?? undefined,
|
||||||
|
name: track.requester.username
|
||||||
|
})
|
||||||
|
.setTitle(track.title)
|
||||||
|
.setURL(track.url)
|
||||||
|
.setDescription("playing now.")
|
||||||
|
.setThumbnail(track.thumbnail);
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
okConnected: (channel: string): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(`✅ Successfuly connected to: ${italic(channel)}`)
|
||||||
|
.setDescription("Will leave after some time, if left alone.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
notConnected: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle("❗ Cannot create new player.")
|
||||||
|
.setDescription("Make yourself connected to a voice channel beforehand.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
destroyed: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle("💀 Destroying the player")
|
||||||
|
.setDescription("Everything is lost now. Use '/play' to create a new one.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
alone: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setTitle("🥬 I have decided to leave to conserve my energy")
|
||||||
|
.setDescription("Since there was nobody in the voice channel.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
paused: (state: boolean): BaseMessageOptions => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle("▶️ The playback was resumed");
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.warning)
|
||||||
|
.setTitle("⏸️ The playback was paused")
|
||||||
|
.setDescription("Invoke this command again to resume.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
seeked: (from: number, to: number): BaseMessageOptions => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (from < to) {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.info)
|
||||||
|
.setTitle(`⏩ Fast-forwarding by ${toISOTime(to - from)}`);
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.info)
|
||||||
|
.setTitle(`⏪ Fast-reversing by ${toISOTime(from - to)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.setDescription(
|
||||||
|
`Moving playback from ${italic(toISOTime(from))} to ${italic(toISOTime(to))}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
looped: (option: LoopOption) => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
switch (option) {
|
||||||
|
case "AUTO": {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.default)
|
||||||
|
.setTitle(`${loopMoji(option)} Shuffle mode.`)
|
||||||
|
.setDescription("Does not affect the queue order. Endless playback.");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "LOOP": {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.info)
|
||||||
|
.setTitle(`${loopMoji(option)} Looping over the queue.`)
|
||||||
|
.setDescription("Will start over after final track. Endless playback.");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SONG": {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.warning)
|
||||||
|
.setTitle(`${loopMoji(option)} Looping a single track.`)
|
||||||
|
.setDescription("Endless playback of a single track. Over and over again...");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "NONE": {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle(`${loopMoji(option)} Drop on final.`)
|
||||||
|
.setDescription(
|
||||||
|
"Will end playback after final track. Action will be needed to continue."
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
reshuffle: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle("✅ Successfuly reshuffled queue")
|
||||||
|
.setDescription("Use 'random' mode, if you want shuffle without reshuffle.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
skipped: (): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.info)
|
||||||
|
.setTitle("⏭️ Skipping current track")
|
||||||
|
.setDescription("Behavior depends on the loop option.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
jumped: (previous?: Track, to?: number): BaseMessageOptions => {
|
||||||
|
if (!to) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle("❗ Cannot perfome jump over the queue")
|
||||||
|
.setDescription("Either not found by query, or index is not in range.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(`✅ Jumping over the queue`);
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
embed.setDescription(`Switching from ${italic(previous.title)}.`);
|
||||||
|
} else {
|
||||||
|
embed.setDescription("Playback will start again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
appended: (track?: Track): BaseMessageOptions => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (track) {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setAuthor({
|
||||||
|
iconURL: track.requester.avatarURL() ?? undefined,
|
||||||
|
name: track.requester.username
|
||||||
|
})
|
||||||
|
.setTitle(track.title)
|
||||||
|
.setURL(track.url)
|
||||||
|
.setDescription("has been added to the queue.")
|
||||||
|
.setThumbnail(track.thumbnail)
|
||||||
|
.setTimestamp();
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle("❗ Cannot add track to the queue")
|
||||||
|
.setDescription("Either not found by query, or URL is broken.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
removed: (track?: Track): BaseMessageOptions => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (track) {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(track.title)
|
||||||
|
.setURL(track.url)
|
||||||
|
.setDescription("was removed from the queue.")
|
||||||
|
.setThumbnail(track.thumbnail)
|
||||||
|
.setTimestamp();
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle("❗ Cannot delete track from queue")
|
||||||
|
.setDescription("Either not found by query, or index is not in range.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
cleared: (amount: number): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(`♻️ Queue has been cleared`)
|
||||||
|
.setDescription(
|
||||||
|
`${italic(amount.toString())} track${amount > 1 ? "s were" : " was"} deleted.`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
filtered: (filter?: string, status = true): BaseMessageOptions => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
embed = new EmbedBuilder().setTitle(
|
||||||
|
`✅ ${italic(filter)} was ${status ? "applied to the playback" : "declined"}.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder().setTitle("♻️ All filters were declined");
|
||||||
|
}
|
||||||
|
|
||||||
|
embed
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setDescription("Playback can experience some lags during initial transformation.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
volumeSet: (amount: number): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(`${volumeMoji(amount)} Volume has been set to ${amount}%`)
|
||||||
|
.setDescription("This will affect everyone listening.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
queue: (queue: Queue, position = 0, resource?: AudioResource<Track>): BaseMessageOptions => {
|
||||||
|
if (queue.empty()) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.warning)
|
||||||
|
.setTitle("⚠️ Queue is empty")
|
||||||
|
.setDescription("Use '/play' to add some tracks.");
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (resource) {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setTitle(`${queue.position + 1}. ${resource.metadata.title}`)
|
||||||
|
.setAuthor({
|
||||||
|
iconURL: resource.metadata.requester.avatarURL() ?? undefined,
|
||||||
|
name: resource.metadata.requester.username
|
||||||
|
})
|
||||||
|
.setURL(resource.metadata.url)
|
||||||
|
.setDescription(statusBarGenerator(resource))
|
||||||
|
.setThumbnail(resource.metadata.thumbnail);
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setTitle("💿 Insert the disk...")
|
||||||
|
.setURL("https://www.youtube.com/watch?v=5mGuCdlCcNM&ab_channel=Ra%C3%BAlBlanco")
|
||||||
|
.setDescription(statusBarGenerator())
|
||||||
|
.setThumbnail("https://img.youtube.com/vi/5mGuCdlCcNM/default.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.setColor(config.colors.info).setFields(
|
||||||
|
queue.tracks.slice(position, position + 10).map((value, index) => ({
|
||||||
|
name: `${position + index + 1}. ${value.title.padEnd(35)}`,
|
||||||
|
value: `[${toISOTime(value.duration)}] | ${value.requester.username}`
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
},
|
||||||
|
info: (player: Player): BaseMessageOptions => {
|
||||||
|
let embed: EmbedBuilder;
|
||||||
|
|
||||||
|
if (player.resource) {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`)
|
||||||
|
.setAuthor({
|
||||||
|
iconURL: player.resource.metadata.requester.avatarURL() ?? undefined,
|
||||||
|
name: player.resource.metadata.requester.username
|
||||||
|
})
|
||||||
|
.setURL(player.resource.metadata.url)
|
||||||
|
.setDescription(statusBarGenerator(player.resource))
|
||||||
|
.setThumbnail(player.resource.metadata.thumbnail);
|
||||||
|
} else {
|
||||||
|
embed = new EmbedBuilder()
|
||||||
|
.setTitle("💿 Insert the disk...")
|
||||||
|
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||||
|
.setDescription(statusBarGenerator());
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.setColor(config.colors.info).addFields([
|
||||||
|
{
|
||||||
|
name: "Applied Filters",
|
||||||
|
value:
|
||||||
|
player.filter
|
||||||
|
.toggled()
|
||||||
|
.map((value) => value[0])
|
||||||
|
.join("\n") + "\ndefault",
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue Length",
|
||||||
|
value: `🔢 ${player.queue.tracks.length}`,
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue Duration",
|
||||||
|
value: `💿 ${toISOTime(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: `🕓 ${toISOTime((Date.now() - player.createdAt.getTime()) / 1000)}`,
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default replies;
|
||||||
+27
-245
@@ -1,4 +1,5 @@
|
|||||||
import { User } from "discord.js";
|
import { User } from "discord.js";
|
||||||
|
import { downloadOptions } from "ytdl-core";
|
||||||
|
|
||||||
export interface Track {
|
export interface Track {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -10,252 +11,33 @@ export interface Track {
|
|||||||
|
|
||||||
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
|
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
|
||||||
|
|
||||||
export interface YtdlArgs {
|
export interface YtdlDump {
|
||||||
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;
|
id: string;
|
||||||
}
|
original_url: string;
|
||||||
|
title?: string;
|
||||||
interface YtdlEntry {
|
uploader?: string;
|
||||||
uploader: string;
|
duration?: number;
|
||||||
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;
|
extractor_key: string;
|
||||||
playlist: string;
|
thumbnail?: string;
|
||||||
playlist_index: number;
|
}
|
||||||
|
|
||||||
|
export interface YtdlResult {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
display_id: string;
|
duration: number;
|
||||||
requested_subtitles: unknown;
|
}
|
||||||
asr: number;
|
|
||||||
filesize: number;
|
export interface YtdlStreamOptions extends downloadOptions {
|
||||||
format_id: string;
|
seek?: number;
|
||||||
format_note: string;
|
encoderArgs?: string[];
|
||||||
fps: number;
|
fmt?: string;
|
||||||
height: number;
|
opusEncoded?: boolean;
|
||||||
quality: number;
|
}
|
||||||
tbr: number;
|
|
||||||
url: string;
|
export interface StreamOptions {
|
||||||
width: number;
|
seek?: number;
|
||||||
ext: string;
|
encoderArgs?: string[];
|
||||||
vcodec: string;
|
fmt?: string;
|
||||||
acodec: string;
|
opusEncoded?: boolean;
|
||||||
abr: number;
|
|
||||||
downloader_options: { http_chunk_size: number };
|
|
||||||
container: string;
|
|
||||||
format: string;
|
|
||||||
protocol: string;
|
|
||||||
http_headers: unknown;
|
|
||||||
fulltitle: string;
|
|
||||||
_filename: string;
|
|
||||||
entries: YtdlEntry[];
|
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import Music from "./lib/music.js";
|
|||||||
const name = "music";
|
const name = "music";
|
||||||
const description = "Music module";
|
const description = "Music module";
|
||||||
const author = "unknowableshade";
|
const author = "unknowableshade";
|
||||||
const tag = "2302.1";
|
const tag = "2.0.0";
|
||||||
|
|
||||||
const controller = new Music();
|
const controller = new Music();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
Events,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
SlashCommandBuilder
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
import Client from "../../../lib/client.js";
|
||||||
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
|
|
||||||
|
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
if (!interaction.inCachedGuild()) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const event = interaction.options.getString("event", true);
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case "role": {
|
||||||
|
const role = interaction.options.getRole("role", true);
|
||||||
|
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.db.event.upsert({
|
||||||
|
where: {
|
||||||
|
eventIdentifier: {
|
||||||
|
title: event,
|
||||||
|
action: subcommand,
|
||||||
|
guildId: interaction.guildId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: { metadata: [role.id] },
|
||||||
|
create: {
|
||||||
|
Guild: {
|
||||||
|
connectOrCreate: {
|
||||||
|
where: {
|
||||||
|
id: interaction.guildId
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: interaction.guildId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: event,
|
||||||
|
action: subcommand,
|
||||||
|
metadata: [role.id]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.editReply(replies.auto("add role to a member", event));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error occured while quering db.", { error });
|
||||||
|
} finally {
|
||||||
|
await client.db.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTO_EVENT_CHOICES = [
|
||||||
|
{
|
||||||
|
name: "Member Joins",
|
||||||
|
value: Events.GuildMemberAdd
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("auto")
|
||||||
|
.setDescription("Quetza's automatization engine.")
|
||||||
|
.addSubcommand((subcommand) =>
|
||||||
|
subcommand
|
||||||
|
.setName("role")
|
||||||
|
.setDescription("Auto: give role.")
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("event")
|
||||||
|
.setDescription("Event when perfome an action.")
|
||||||
|
.setChoices(...AUTO_EVENT_CHOICES)
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
.addRoleOption((option) =>
|
||||||
|
option.setName("role").setDescription("Role to grant on event.").setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
|
export { data, execute };
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
import Client from "../../../lib/client.js";
|
||||||
|
import logger from "../../../lib/logger.js";
|
||||||
|
import replies from "../lib/replies.js";
|
||||||
|
|
||||||
|
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
|
||||||
|
logger.warn("Interaction rejected.", { interaction });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case "check": {
|
||||||
|
const user = interaction.options.getUser("member", true);
|
||||||
|
|
||||||
|
const member = await interaction.guild.members.fetch(user).catch(() => undefined);
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
await interaction.reply(replies.roleList(user.username, member.roles.cache));
|
||||||
|
} else {
|
||||||
|
await interaction.reply(replies.notMember("edited", user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "edit": {
|
||||||
|
const user = interaction.options.getUser("member", true);
|
||||||
|
const role = interaction.options.getRole("role", true);
|
||||||
|
const reason = interaction.options.getString("reason") ?? undefined;
|
||||||
|
|
||||||
|
const member = await interaction.guild.members.fetch(user).catch(() => undefined);
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
if (member.roles.cache.some((memberRole) => role.id === memberRole.id)) {
|
||||||
|
await member.roles.remove(role.id, reason);
|
||||||
|
await interaction.reply(
|
||||||
|
replies.roleAction("revoked from", interaction.user, role.name, reason)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await member.roles.add(role.id, reason);
|
||||||
|
await interaction.reply(
|
||||||
|
replies.roleAction("granted to", interaction.user, role.name, reason)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await interaction.reply(replies.notMember("edited", user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "list": {
|
||||||
|
const roles = await interaction.guild.roles.fetch();
|
||||||
|
|
||||||
|
await interaction.reply(replies.roleList(interaction.guild.name, roles));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new SlashCommandBuilder()
|
||||||
|
.setName("role")
|
||||||
|
.setDescription("Quetza's role manager.")
|
||||||
|
.addSubcommand((subcommand) =>
|
||||||
|
subcommand.setName("list").setDescription("Role: list guild's roles.")
|
||||||
|
)
|
||||||
|
.addSubcommand((subcommand) =>
|
||||||
|
subcommand
|
||||||
|
.setName("check")
|
||||||
|
.setDescription("Role: check user roles.")
|
||||||
|
.addUserOption((option) =>
|
||||||
|
option.setName("member").setDescription("Member to check.").setRequired(true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand((subcommand) =>
|
||||||
|
subcommand
|
||||||
|
.setName("edit")
|
||||||
|
.setDescription("Role: edit role for a specific user.")
|
||||||
|
.addUserOption((option) =>
|
||||||
|
option.setName("member").setDescription("Member to edit.").setRequired(true)
|
||||||
|
)
|
||||||
|
.addRoleOption((option) =>
|
||||||
|
option.setName("role").setDescription("Member's role to edit.").setRequired(true)
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option.setName("reason").setDescription("Reason for an action.")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
|
||||||
|
.setDMPermission(false);
|
||||||
|
|
||||||
|
export { data, execute };
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Events, GuildMember } from "discord.js";
|
||||||
|
|
||||||
|
import Client from "../../../lib/client.js";
|
||||||
|
|
||||||
|
async function execute(client: Client, eventee: [GuildMember]): Promise<void> {
|
||||||
|
const [member] = eventee;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = await client.db.event.findFirst({
|
||||||
|
where: { guildId: member.guild.id, title: name }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
await member.roles.add(event.metadata as string[]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await client.db.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = Events.GuildMemberAdd;
|
||||||
|
|
||||||
|
export { execute, name };
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
BaseMessageOptions,
|
||||||
|
bold,
|
||||||
|
Collection,
|
||||||
|
EmbedBuilder,
|
||||||
|
InteractionReplyOptions,
|
||||||
|
italic,
|
||||||
|
Role,
|
||||||
|
underscore,
|
||||||
|
User
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
import config from "../../../config.js";
|
||||||
|
|
||||||
|
const replies = {
|
||||||
|
error: (): InteractionReplyOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.error)
|
||||||
|
.setTitle("Internal error occured while resolving interaction.")
|
||||||
|
.setDescription("Submit an issue (bio). Logs were saved.")
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return { embeds: [embed], ephemeral: true };
|
||||||
|
},
|
||||||
|
notMember: (action: string, user: User): InteractionReplyOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor("Blue")
|
||||||
|
.setTitle(`${user.username} cannot be ${action}`)
|
||||||
|
.setDescription("They're not a guild member.")
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return { embeds: [embed], ephemeral: true };
|
||||||
|
},
|
||||||
|
roleAction: (
|
||||||
|
action: string,
|
||||||
|
user: User,
|
||||||
|
roleName: string,
|
||||||
|
reason?: string
|
||||||
|
): InteractionReplyOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(`Role ${roleName} was ${action} user ${user.username}`)
|
||||||
|
.setDescription(reason ?? "No reason provided.")
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return { embeds: [embed], ephemeral: true };
|
||||||
|
},
|
||||||
|
roleList: (observed: string, roles: Collection<string, Role>): InteractionReplyOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(`${observed} roles:`)
|
||||||
|
.addFields(
|
||||||
|
roles.map((role) => {
|
||||||
|
return {
|
||||||
|
name: role.name,
|
||||||
|
value: italic(role.id)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return { embeds: [embed], ephemeral: true };
|
||||||
|
},
|
||||||
|
auto: (action: string, event: string): BaseMessageOptions => {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(config.colors.success)
|
||||||
|
.setTitle(
|
||||||
|
bold(
|
||||||
|
`From now I will ${underscore(italic(action))} when ${underscore(
|
||||||
|
italic(event)
|
||||||
|
)} event happens`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return { embeds: [embed] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default replies;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const name = "server";
|
||||||
|
const description = "Server utilities module";
|
||||||
|
const author = "unknowableshade";
|
||||||
|
const tag = "0.0.1";
|
||||||
|
|
||||||
|
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export { author, description, name, rootDir, tag };
|
||||||
Reference in new issue
Block a user