diff --git a/README.md b/README.md
index b741fd8..0bf5828 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,5 @@
-[](http://commitizen.github.io/cz-cli/)
-
-
-
-
-
-## 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
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 7cc50a4..d456818 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,6 @@
"ffmpeg-static": "^4.4.1",
"i18n": "^0.15.1",
"lyrics-finder": "^21.0.5",
- "play-dl": "^1.9.7",
"soundcloud-downloader": "^0.2.3",
"string-progressbar": "^1.0.4",
"youtube-sr": "~4.3.0"
@@ -2618,6 +2617,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",
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