diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index b5ae78f..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -contact_links: - - name: ❔ Questions / Help - url: https://github.com/eritislami/evobot/discussions - about: Please use GitHub Discussions for help and questions, do not start an issue. - - name: 📃 Discord.js Guide - url: https://discordjs.guide/ - about: The official guide for discord.js diff --git a/Dockerfile b/Dockerfile index 852834c..5e30c2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG NODE_VERSION=18.18.2-slim FROM node:${NODE_VERSION} as base -ENV USER=evobot +ENV USER=Apollo RUN apt-get update && \ apt-get install -y --no-install-recommends python3 build-essential && \ @@ -9,10 +9,10 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* RUN groupadd -r ${USER} && \ - useradd --create-home --home /home/evobot -r -g ${USER} ${USER} + useradd --create-home --home /home/apollo -r -g ${USER} ${USER} USER ${USER} -WORKDIR /home/evobot +WORKDIR /home/apollo FROM base as build @@ -26,7 +26,7 @@ RUN rm -rf node_modules && \ FROM node:${NODE_VERSION} as prod COPY --chown=${USER}:${USER} package*.json ./ -COPY --from=build --chown=${USER}:${USER} /home/evobot/node_modules ./node_modules -COPY --from=build --chown=${USER}:${USER} /home/evobot/dist ./dist +COPY --from=build --chown=${USER}:${USER} /home/apollo/node_modules ./node_modules +COPY --from=build --chown=${USER}:${USER} /home/apollo/dist ./dist CMD [ "node", "./dist/index.js" ] \ No newline at end of file diff --git a/README.md b/README.md index 4432c34..0bf5828 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,5 @@ -![Node build](https://github.com/eritislami/evobot/actions/workflows/node.yml/badge.svg) -![Docker build](https://github.com/eritislami/evobot/actions/workflows/docker.yml/badge.svg) -[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) - -![logo](https://repository-images.githubusercontent.com/186841818/8aa95700-7730-11e9-84be-e80f28520325) - -# 🤖 EvoBot (Discord Music Bot) - -> EvoBot is a Discord Music Bot built with TypeScript, discord.js & uses Command Handler from [discordjs.guide](https://discordjs.guide) - -## Requirements +

Hello

1. Discord Bot Token **[Guide](https://discordjs.guide/preparations/setting-up-a-bot-application.html#creating-your-bot)** 1.1. Enable 'Message Content Intent' in Discord Developer Portal 2. Node.js 16.11.0 or newer @@ -17,8 +7,6 @@ ## 🚀 Getting Started ```sh -git clone https://github.com/eritislami/evobot.git -cd evobot npm install ``` @@ -43,10 +31,9 @@ Copy or Rename `config.json.example` to `config.json` and fill out the values: ## 🐬 Docker Configuration -For those who would prefer to use our [Docker container](https://hub.docker.com/repository/docker/eritislami/evobot), you may provide values from `config.json` as environment variables. ```shell -docker run -e "TOKEN=" eritislami/evobot +docker run -e "TOKEN=" f04c/apollo ``` ## 📝 Features & Commands @@ -125,13 +112,3 @@ Currently available locales are: - Vietnamese (vi) - Check [Contributing](#-contributing) if you wish to help add more languages! - For languages please use [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) two letter format - -## 🤝 Contributing - -1. [Fork the repository](https://github.com/eritislami/evobot/fork) -2. Clone your fork: `git clone https://github.com/your-username/evobot.git` -3. Create your feature branch: `git checkout -b my-new-feature` -4. Stage changes `git add .` -5. Commit your changes: `cz` OR `npm run commit` do not use `git commit` -6. Push to the branch: `git push origin my-new-feature` -7. Submit a pull request diff --git a/app.json b/app.json index f95c5d6..e9c6beb 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,5 @@ { - "name": "EvoBot", - "description": "🤖 Discord Music Bot built with discord.js", - "repository": "https://github.com/eritislami/evobot", - "logo": "https://i.imgur.com/JFxgbWH.png", + "name": "Apollo", + "description": "Discord Music Bot built with discord.js", "keywords": ["discord", "discordjs", "typescript", "music", "bot"] } diff --git a/commands/help.ts b/commands/help.ts index 159e407..e21d75c 100644 --- a/commands/help.ts +++ b/commands/help.ts @@ -21,7 +21,9 @@ export default { }); helpEmbed.setTimestamp(); - + helpEmbed.setFooter({ + text: "Made with ❤️ by F04C" + }); return interaction.reply({ embeds: [helpEmbed] }).catch(console.error); } }; diff --git a/interfaces/Config.ts b/interfaces/Config.ts index 7d5ad2e..41fad77 100644 --- a/interfaces/Config.ts +++ b/interfaces/Config.ts @@ -5,4 +5,5 @@ export interface Config { STAY_TIME: number; DEFAULT_VOLUME: number; LOCALE: string; + USE_INVIDIOUS_PROXY: boolean; } diff --git a/package-lock.json b/package-lock.json index dffa782..bb23555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { - "name": "evobot", + "name": "apollo", "version": "2.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "evobot", + "name": "apollo", "version": "2.9.0", "dependencies": { - "@discordjs/voice": "^0.17.0", - "array-move": "^4.0.0", + + "@discordjs/voice": "^0.18.0", + "array-move": "^3.0.1", + "discord.js": "^14.15.3", "dotenv": "^16.4.5", "ffmpeg-static": "^4.4.1", @@ -476,23 +478,28 @@ } }, "node_modules/@discordjs/voice": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.17.0.tgz", - "integrity": "sha512-hArn9FF5ZYi1IkxdJEVnJi+OxlwLV0NJYWpKXsmNOojtGtAZHxmsELA+MZlu2KW1F/K1/nt7lFOfcMXNYweq9w==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz", + "integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==", "dependencies": { - "@types/ws": "^8.5.10", - "discord-api-types": "0.37.83", + "@types/ws": "^8.5.12", + "discord-api-types": "^0.37.103", "prism-media": "^1.3.5", - "tslib": "^2.6.2", - "ws": "^8.16.0" + "tslib": "^2.6.3", + "ws": "^8.18.0" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "funding": { "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@discordjs/voice/node_modules/discord-api-types": { + "version": "0.37.107", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.107.tgz", + "integrity": "sha512-XOxmxnhtYIRH55kLTrc/JS3nJV1l3wfBtTptFiRGdGDOe2qdCT4DltpxSgskasfDrKfw71Z5quG4tYqTxyPJ7g==" + }, "node_modules/@discordjs/voice/node_modules/opusscript": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz", @@ -525,6 +532,11 @@ } } }, + "node_modules/@discordjs/voice/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/@discordjs/ws": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.1.1.tgz", @@ -691,9 +703,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dependencies": { "@types/node": "*" } @@ -2618,6 +2630,7 @@ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^3.2.7", @@ -3606,9 +3619,9 @@ "devOptional": true }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 4d67333..77833a0 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,10 @@ { - "name": "evobot", + "name": "apollo", "version": "2.9.0", "description": "Discord music bot built with discord.js", "main": "index.ts", "author": "Erit Islami ", "private": true, - "homepage": "https://github.com/eritislami/evobot", - "repository": "github:eritislami/evobot", - "bugs": "https://github.com/eritislami/evobot/issues", "engines": { "node": ">=16.11.0" }, @@ -22,6 +19,7 @@ "dependencies": { "@discordjs/voice": "^0.17.0", "array-move": "^4.0.0", + "discord.js": "^14.15.3", "dotenv": "^16.4.5", "ffmpeg-static": "^4.4.1", diff --git a/structs/Song.ts b/structs/Song.ts index 9e5dd93..579c620 100644 --- a/structs/Song.ts +++ b/structs/Song.ts @@ -1,25 +1,160 @@ import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice"; import youtube from "youtube-sr"; import { i18n } from "../utils/i18n"; -import { videoPattern, isURL } from "../utils/patterns"; +import { isURL, videoPattern } from "../utils/patterns"; -const { stream, video_basic_info } = require("play-dl"); +import { Readable } from "stream"; +import { extractID } from "../utils/extractor"; +import { config } from "../utils/config"; + +const INVIDIOUS_BASE_URL = "https://inv.nadeko.net"; + +type InvidiousResponse = { + type: string; + title: string; + videoId: string; + videoThumbnails: Array<{ + quality: string; + url: string; + width: number; + height: number; + }>; + storyboards: Array<{ + url: string; + templateUrl: string; + width: number; + height: number; + count: number; + interval: number; + storyboardWidth: number; + storyboardHeight: number; + storyboardCount: number; + }>; + description: string; + descriptionHtml: string; + published: number; + publishedText: string; + keywords: Array; + viewCount: number; + likeCount: number; + dislikeCount: number; + paid: boolean; + premium: boolean; + isFamilyFriendly: boolean; + allowedRegions: Array; + genre: string; + genreUrl: any; + author: string; + authorId: string; + authorUrl: string; + authorVerified: boolean; + authorThumbnails: Array<{ + url: string; + width: number; + height: number; + }>; + subCountText: string; + lengthSeconds: number; + allowRatings: boolean; + rating: number; + isListed: boolean; + liveNow: boolean; + isPostLiveDvr: boolean; + isUpcoming: boolean; + dashUrl: string; + adaptiveFormats: Array<{ + init: string; + index: string; + bitrate: string; + url: string; + itag: string; + type: string; + clen: string; + lmt: string; + projectionType: string; + container: string; + encoding: string; + audioQuality?: string; + audioSampleRate?: number; + audioChannels?: number; + fps?: number; + size?: string; + resolution?: string; + qualityLabel?: string; + colorInfo?: { + primaries: string; + transferCharacteristics: string; + matrixCoefficients: string; + }; + }>; + formatStreams: Array<{ + url: string; + itag: string; + type: string; + quality: string; + bitrate: string; + fps: number; + size: string; + resolution: string; + qualityLabel: string; + container: string; + encoding: string; + }>; + captions: Array<{ + label: string; + language_code: string; + url: string; + }>; + recommendedVideos: Array<{ + videoId: string; + title: string; + videoThumbnails: Array<{ + quality: string; + url: string; + width: number; + height: number; + }>; + author: string; + authorUrl: string; + authorId: string; + authorVerified: boolean; + lengthSeconds: number; + viewCountText: string; + viewCount: number; + }>; +}; + +const getBasicVideoInfo = async (url: string) => { + const videoId = extractID(url); + + if (!videoId) { + throw new Error("Invalid YouTube URL"); + } + + const response = await fetch(`https://inv.nadeko.net/api/v1/videos/${videoId}`); + const data = (await response.json()) as InvidiousResponse; + + return data; +}; export interface SongData { url: string; title: string; duration: number; + adaptiveFormats?: InvidiousResponse["adaptiveFormats"]; } export class Song { public readonly url: string; public readonly title: string; public readonly duration: number; + public readonly adaptiveFormats?: InvidiousResponse["adaptiveFormats"]; - public constructor({ url, title, duration }: SongData) { + public constructor({ url, title, duration, adaptiveFormats = [] }: SongData) { this.url = url; this.title = title; this.duration = duration; + this.adaptiveFormats = adaptiveFormats; } public static async from(url: string = "", search: string = "") { @@ -28,12 +163,13 @@ export class Song { let songInfo; if (isYoutubeUrl) { - songInfo = await video_basic_info(url); + songInfo = await getBasicVideoInfo(url); return new this({ - url: songInfo.video_details.url, - title: songInfo.video_details.title, - duration: parseInt(songInfo.video_details.durationInSec) + url, + title: songInfo.title, + duration: songInfo.lengthSeconds, + adaptiveFormats: songInfo.adaptiveFormats }); } else { const result = await youtube.searchOne(search); @@ -50,31 +186,61 @@ export class Song { throw err; } - songInfo = await video_basic_info(`https://youtube.com/watch?v=${result.id}`); + songInfo = await getBasicVideoInfo(`https://youtube.com/watch?v=${result.id}`); return new this({ - url: songInfo.video_details.url, - title: songInfo.video_details.title, - duration: parseInt(songInfo.video_details.durationInSec) + url: `https://youtube.com/watch?v=${result.id}`, + title: songInfo.title, + duration: songInfo.lengthSeconds, + adaptiveFormats: songInfo.adaptiveFormats }); } } public async makeResource(): Promise | void> { - let playStream; + const format = this.adaptiveFormats + ?.sort((a, b) => { + return parseInt(b.bitrate!) - parseInt(a.bitrate!); + }) + .find((format) => format.type.startsWith("audio/")); - const source = this.url.includes("youtube") ? "youtube" : "soundcloud"; + if (!format) return; - if (source === "youtube") { - playStream = await stream(this.url); + let formatUrl = format.url; + + if (config.USE_INVIDIOUS_PROXY) { + const invidiousUrl = new URL(INVIDIOUS_BASE_URL); + const formatUrlObject = new URL(format.url); + + formatUrlObject.hostname = invidiousUrl.hostname; + + formatUrl = formatUrlObject.toString(); } - if (!stream) return; + const response = await fetch(formatUrl); - return createAudioResource(playStream.stream, { metadata: this, inputType: playStream.type, inlineVolume: true }); + const arrayBuffer = await response.arrayBuffer(); + + const stream = new Readable({ + read() { + this.push(Buffer.from(arrayBuffer)); + this.push(null); + } + }); + + const codec = format.encoding; + + const type: StreamType = + codec === "opus" && format.container === "webm" ? StreamType.WebmOpus : StreamType.Arbitrary; + + return createAudioResource(stream, { + metadata: this, + inputType: type, + inlineVolume: true + }); } public startMessage() { return i18n.__mf("play.startedPlaying", { title: this.title, url: this.url }); } -} +} \ No newline at end of file diff --git a/utils/config.ts b/utils/config.ts index cc22ecf..a564666 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -12,7 +12,8 @@ try { PRUNING: process.env.PRUNING === "true" ? true : false, STAY_TIME: parseInt(process.env.STAY_TIME!) || 30, DEFAULT_VOLUME: parseInt(process.env.DEFAULT_VOLUME!) || 100, - LOCALE: process.env.LOCALE || "en" + LOCALE: process.env.LOCALE || "en", + USE_INVIDIOUS_PROXY: process.env.USE_INVIDIOUS_PROXY === "true" ? true : false }; } diff --git a/utils/extractor.ts b/utils/extractor.ts new file mode 100644 index 0000000..ceff889 --- /dev/null +++ b/utils/extractor.ts @@ -0,0 +1,37 @@ +import { videoIdPattern, videoPattern } from "../utils/patterns"; + +export const extractID = (url: string) => { + const url_ = url.trim(); + if (url_.startsWith("https")) { + if (url_.indexOf("list=") === -1) { + const video_id = extractVideoId(url_); + if (!video_id) throw new Error("This is not a YouTube url or videoId or PlaylistID"); + return video_id; + } else { + return url_.split("list=")[1].split("&")[0]; + } + } else return url_; +}; + +function extractVideoId(urlOrId: string): string | false { + if (urlOrId.startsWith("https://") && urlOrId.match(videoPattern)) { + let id: string; + if (urlOrId.includes("youtu.be/")) { + id = urlOrId.split("youtu.be/")[1].split(/(\?|\/|&)/)[0]; + } else if (urlOrId.includes("youtube.com/embed/")) { + id = urlOrId.split("youtube.com/embed/")[1].split(/(\?|\/|&)/)[0]; + } else if (urlOrId.includes("youtube.com/shorts/")) { + id = urlOrId.split("youtube.com/shorts/")[1].split(/(\?|\/|&)/)[0]; + } else if (urlOrId.includes("youtube.com/live/")) { + id = urlOrId.split("youtube.com/live/")[1].split(/(\?|\/|&)/)[0]; + } else { + id = (urlOrId.split("watch?v=")[1] ?? urlOrId.split("&v=")[1]).split(/(\?|\/|&)/)[0]; + } + + if (id.match(videoIdPattern)) return id; + } else if (urlOrId.match(videoIdPattern)) { + return urlOrId; + } + + return false; +} \ No newline at end of file diff --git a/utils/patterns.ts b/utils/patterns.ts index 2b97103..7d985eb 100644 --- a/utils/patterns.ts +++ b/utils/patterns.ts @@ -4,3 +4,4 @@ export const scRegex = /^https?:\/\/(soundcloud\.com)\/(.*)$/; export const mobileScRegex = /^https?:\/\/(soundcloud\.app\.goo\.gl)\/(.*)$/; export const isURL = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; +export const videoIdPattern = /^[a-zA-Z\d_-]{11,12}$/; \ No newline at end of file