add files
This commit is contained in:
parent
3bf02993f6
commit
ca759ef134
1
Dockerfile
Normal file
1
Dockerfile
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
0
.gitignore → src/.gitignore
vendored
0
.gitignore → src/.gitignore
vendored
67
src/bot.js
Normal file
67
src/bot.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const TelegramBot = require("node-telegram-bot-api");
|
||||||
|
const fs = require("fs");
|
||||||
|
const DownloadVideo = require("./download");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
token: process.env.BOT_TOKEN,
|
||||||
|
admins: process.env.ADMIN_LIST?.split(/\s*,\s*/g).map(x => +x) ?? [],
|
||||||
|
};
|
||||||
|
config.whitelist = [].concat(
|
||||||
|
process.env.WHITELIST?.split(/\s*,\s*/g).map(x => +x) ?? [],
|
||||||
|
config.admins,
|
||||||
|
);
|
||||||
|
config.whitelist = Object.keys(
|
||||||
|
Object.fromEntries(config.whitelist.map(x => [x, true]))
|
||||||
|
).map(x => +x);
|
||||||
|
|
||||||
|
console.info(config);
|
||||||
|
|
||||||
|
config.admins = Object.fromEntries(config.admins.map(x => [x, true]));
|
||||||
|
config.whitelist = Object.fromEntries(config.whitelist.map(x => [x, true]));
|
||||||
|
|
||||||
|
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
||||||
|
class TgBot {
|
||||||
|
constructor () {
|
||||||
|
this.bot = new TelegramBot(config.token, {
|
||||||
|
polling: true
|
||||||
|
});
|
||||||
|
this.bot.on('text', m => this.newMessageHandler(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
async newMessageHandler (msg) {
|
||||||
|
console.debug(msg);
|
||||||
|
if (msg.chat.id === msg.from.id) {
|
||||||
|
if (config.whitelist[msg.from.id]) {
|
||||||
|
if (ytLinkRegexp.test(msg.text)) {
|
||||||
|
const video = new DownloadVideo(msg.text);
|
||||||
|
// Download only video with best quality
|
||||||
|
this.download(msg, video);
|
||||||
|
} else {
|
||||||
|
this.bot.sendMessage(msg.chat.id, "Invalid youtube link");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.bot.sendMessage(msg.chat.id, "Permission denied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async download (msg, video, quality=DownloadVideo.qualities.mp4_best) {
|
||||||
|
this.bot.sendMessage(msg.chat.id, "Start download");
|
||||||
|
await video.setupQuality(quality, false);
|
||||||
|
video.download()
|
||||||
|
.then(downloaded => {
|
||||||
|
if (/^video\//m.test(downloaded.format.mime)) {
|
||||||
|
this.bot.sendVideo(msg.chat.id, downloaded.data, { caption: "Download complete" }, {
|
||||||
|
filename: `downloaded.${downloaded.format.exc}`,
|
||||||
|
contentType: downloaded.format.mime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.bot.sendMessage(msg.chat.id, "Download failed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = new TgBot();
|
||||||
130
src/download.js
Normal file
130
src/download.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
const { spawn } = require("child_process");
|
||||||
|
|
||||||
|
const formatsCache = new Map();
|
||||||
|
class DownloadVideo {
|
||||||
|
static get qualities () {
|
||||||
|
const q = {
|
||||||
|
webm_best: { exc: "webm", flag: "best", mime: "video/webm" },
|
||||||
|
mp4_best: { exc: "mp4", flag: "best[ext=mp4]", mime: "video/mp4" },
|
||||||
|
};
|
||||||
|
q.default = q.webm_best;
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(url, quality) {
|
||||||
|
const CLASS = this.__proto__.constructor;
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
|
this.quality = quality;
|
||||||
|
if (!this.quality) this.quality = CLASS.qualities.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFormats() {
|
||||||
|
const fromCache = formatsCache.get(this.url);
|
||||||
|
if (fromCache) return fromCache;
|
||||||
|
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("yt-dlp", ["-j", this.url]);
|
||||||
|
let data = "";
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk) => (data += chunk.toString()));
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code !== 0) return reject(new Error("yt-dlp failed"));
|
||||||
|
const info = JSON.parse(data);
|
||||||
|
resolve(info.formats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
formatsCache.set(this.url, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async qualities() {
|
||||||
|
const formats = await this.getFormats();
|
||||||
|
const q = {};
|
||||||
|
|
||||||
|
// Перебираем форматы и группируем по расширению
|
||||||
|
formats.forEach(f => {
|
||||||
|
if (!f.ext || !f.format_id) return;
|
||||||
|
//if (!q[f.ext]) q[f.ext] = [];
|
||||||
|
|
||||||
|
const resolution = f.height ? `${f.height}p` : 'audio-only';
|
||||||
|
|
||||||
|
let flag = `bestaudio[ext=${f.ext}]`;
|
||||||
|
if (f.height) {
|
||||||
|
flag = `best[height<=${f.height}][ext=${f.ext}]`;
|
||||||
|
}
|
||||||
|
//const flag = f.format_id;
|
||||||
|
|
||||||
|
q[f.ext + "_" + resolution] = {
|
||||||
|
format_id: f.format_id,
|
||||||
|
flag, // <- ключевой момент для yt-dlp
|
||||||
|
exc: f.ext, // <- расширение
|
||||||
|
resolution,
|
||||||
|
filesize: f.filesize || null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupQuality (quality, check=true) {
|
||||||
|
if (!check) {
|
||||||
|
this.quality = quality;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const CLASS = this.__proto__.constructor;
|
||||||
|
|
||||||
|
const all = Object.assign({}, (await this.qualities()), CLASS.qualities);
|
||||||
|
if (all[quality.exc + "_" + quality.resolution]) {
|
||||||
|
this.quality = quality;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(cb) {
|
||||||
|
const data = await new Promise((resolve, reject) => {
|
||||||
|
const params = [
|
||||||
|
"-f",
|
||||||
|
this.quality.flag,
|
||||||
|
"-o",
|
||||||
|
"-",
|
||||||
|
this.url,
|
||||||
|
];
|
||||||
|
const child = spawn("yt-dlp", params);
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
let downloaded = 0;
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
chunks.push(chunk);
|
||||||
|
downloaded += chunk.length;
|
||||||
|
//if (cb) cb({ downloaded });
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", (data) => {
|
||||||
|
const str = data.toString();
|
||||||
|
const match = str.match(/(\d+\.\d+)%/);
|
||||||
|
if (match && cb) {
|
||||||
|
cb({ progress: parseFloat(match[1]), downloaded });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(Buffer.concat(chunks));
|
||||||
|
} else {
|
||||||
|
reject(new Error(`yt-dlp exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: this.quality,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DownloadVideo;
|
||||||
2204
src/package-lock.json
generated
Normal file
2204
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
src/package.json
Normal file
5
src/package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"node-telegram-bot-api": "^0.66.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user