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. 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
|
1.1. Enable 'Message Content Intent' in Discord Developer Portal
|
||||||
2. Node.js 16.11.0 or newer
|
2. Node.js 16.11.0 or newer
|
||||||
|
|||||||
+3
-1
@@ -21,7 +21,9 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
helpEmbed.setTimestamp();
|
helpEmbed.setTimestamp();
|
||||||
|
helpEmbed.setFooter({
|
||||||
|
text: "Made with ❤️ by F04C"
|
||||||
|
});
|
||||||
return interaction.reply({ embeds: [helpEmbed] }).catch(console.error);
|
return interaction.reply({ embeds: [helpEmbed] }).catch(console.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export interface Config {
|
|||||||
STAY_TIME: number;
|
STAY_TIME: number;
|
||||||
DEFAULT_VOLUME: number;
|
DEFAULT_VOLUME: number;
|
||||||
LOCALE: string;
|
LOCALE: string;
|
||||||
|
USE_INVIDIOUS_PROXY: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1
-1
@@ -15,7 +15,6 @@
|
|||||||
"ffmpeg-static": "^4.4.1",
|
"ffmpeg-static": "^4.4.1",
|
||||||
"i18n": "^0.15.1",
|
"i18n": "^0.15.1",
|
||||||
"lyrics-finder": "^21.0.5",
|
"lyrics-finder": "^21.0.5",
|
||||||
"play-dl": "^1.9.7",
|
|
||||||
"soundcloud-downloader": "^0.2.3",
|
"soundcloud-downloader": "^0.2.3",
|
||||||
"string-progressbar": "^1.0.4",
|
"string-progressbar": "^1.0.4",
|
||||||
"youtube-sr": "~4.3.0"
|
"youtube-sr": "~4.3.0"
|
||||||
@@ -2618,6 +2617,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
|
||||||
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
|
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.5.2",
|
"chokidar": "^3.5.2",
|
||||||
"debug": "^3.2.7",
|
"debug": "^3.2.7",
|
||||||
|
|||||||
+184
-18
@@ -1,25 +1,160 @@
|
|||||||
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
|
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
|
||||||
import youtube from "youtube-sr";
|
import youtube from "youtube-sr";
|
||||||
import { i18n } from "../utils/i18n";
|
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 {
|
export interface SongData {
|
||||||
url: string;
|
url: string;
|
||||||
title: string;
|
title: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
adaptiveFormats?: InvidiousResponse["adaptiveFormats"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Song {
|
export class Song {
|
||||||
public readonly url: string;
|
public readonly url: string;
|
||||||
public readonly title: string;
|
public readonly title: string;
|
||||||
public readonly duration: number;
|
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.url = url;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
|
this.adaptiveFormats = adaptiveFormats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async from(url: string = "", search: string = "") {
|
public static async from(url: string = "", search: string = "") {
|
||||||
@@ -28,12 +163,13 @@ export class Song {
|
|||||||
let songInfo;
|
let songInfo;
|
||||||
|
|
||||||
if (isYoutubeUrl) {
|
if (isYoutubeUrl) {
|
||||||
songInfo = await video_basic_info(url);
|
songInfo = await getBasicVideoInfo(url);
|
||||||
|
|
||||||
return new this({
|
return new this({
|
||||||
url: songInfo.video_details.url,
|
url,
|
||||||
title: songInfo.video_details.title,
|
title: songInfo.title,
|
||||||
duration: parseInt(songInfo.video_details.durationInSec)
|
duration: songInfo.lengthSeconds,
|
||||||
|
adaptiveFormats: songInfo.adaptiveFormats
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const result = await youtube.searchOne(search);
|
const result = await youtube.searchOne(search);
|
||||||
@@ -50,31 +186,61 @@ export class Song {
|
|||||||
throw err;
|
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({
|
return new this({
|
||||||
url: songInfo.video_details.url,
|
url: `https://youtube.com/watch?v=${result.id}`,
|
||||||
title: songInfo.video_details.title,
|
title: songInfo.title,
|
||||||
duration: parseInt(songInfo.video_details.durationInSec)
|
duration: songInfo.lengthSeconds,
|
||||||
|
adaptiveFormats: songInfo.adaptiveFormats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async makeResource(): Promise<AudioResource<Song> | void> {
|
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") {
|
let formatUrl = format.url;
|
||||||
playStream = await stream(this.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() {
|
public startMessage() {
|
||||||
return i18n.__mf("play.startedPlaying", { title: this.title, url: this.url });
|
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,
|
PRUNING: process.env.PRUNING === "true" ? true : false,
|
||||||
STAY_TIME: parseInt(process.env.STAY_TIME!) || 30,
|
STAY_TIME: parseInt(process.env.STAY_TIME!) || 30,
|
||||||
DEFAULT_VOLUME: parseInt(process.env.DEFAULT_VOLUME!) || 100,
|
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 mobileScRegex = /^https?:\/\/(soundcloud\.app\.goo\.gl)\/(.*)$/;
|
||||||
export const isURL =
|
export const isURL =
|
||||||
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
/^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