used Invidious
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
[](http://commitizen.github.io/cz-cli/)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 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
@@ -21,7 +21,9 @@ export default {
|
||||
});
|
||||
|
||||
helpEmbed.setTimestamp();
|
||||
|
||||
helpEmbed.setFooter({
|
||||
text: "Made with ❤️ by F04C"
|
||||
});
|
||||
return interaction.reply({ embeds: [helpEmbed] }).catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,4 +5,5 @@ export interface Config {
|
||||
STAY_TIME: number;
|
||||
DEFAULT_VOLUME: number;
|
||||
LOCALE: string;
|
||||
USE_INVIDIOUS_PROXY: boolean;
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
+184
-18
@@ -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,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<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() {
|
||||
return i18n.__mf("play.startedPlaying", { title: this.title, url: this.url });
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}$/;
|
||||
Reference in New Issue
Block a user