used Invidious

This commit is contained in:
2024-10-26 22:28:01 +08:00
parent 11241875b0
commit 6b2b2d4418
8 changed files with 230 additions and 28 deletions
+1 -7
View File
@@ -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
<h1> Hello </h1>
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
+3 -1
View File
@@ -21,7 +21,9 @@ export default {
});
helpEmbed.setTimestamp();
helpEmbed.setFooter({
text: "Made with ❤️ by F04C"
});
return interaction.reply({ embeds: [helpEmbed] }).catch(console.error);
}
};
+1
View File
@@ -5,4 +5,5 @@ export interface Config {
STAY_TIME: number;
DEFAULT_VOLUME: number;
LOCALE: string;
USE_INVIDIOUS_PROXY: boolean;
}
+1 -1
View File
@@ -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",
+183 -17
View File
@@ -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<string>;
viewCount: number;
likeCount: number;
dislikeCount: number;
paid: boolean;
premium: boolean;
isFamilyFriendly: boolean;
allowedRegions: Array<string>;
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,28 +186,58 @@ 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<AudioResource<Song> | 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() {
+2 -1
View File
@@ -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
};
}
+37
View File
@@ -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;
}
+1
View File
@@ -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}$/;