From 6b2b2d4418f0699f5fbae6c72619d135d3f44064 Mon Sep 17 00:00:00 2001 From: F04C Date: Sat, 26 Oct 2024 22:28:01 +0800 Subject: [PATCH] used Invidious --- README.md | 8 +- commands/help.ts | 4 +- interfaces/Config.ts | 1 + package-lock.json | 2 +- structs/Song.ts | 202 +++++++++++++++++++++++++++++++++++++++---- utils/config.ts | 3 +- utils/extractor.ts | 37 ++++++++ utils/patterns.ts | 1 + 8 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 utils/extractor.ts diff --git a/README.md b/README.md index b741fd8..0bf5828 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ -[![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) - - - -## 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