finish first revision of bot
This commit is contained in:
parent
7a0601bd5e
commit
a865c192bf
271
src/bot.js
271
src/bot.js
@ -1,67 +1,244 @@
|
|||||||
const TelegramBot = require("node-telegram-bot-api");
|
const TelegramBot = require("node-telegram-bot-api");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const DownloadVideo = require("./download");
|
const DownloadVideo = require("./download");
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
async function getImage(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https
|
||||||
|
.get(url, (res) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode} при загрузке изображения`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
res.on("data", (chunk) => chunks.push(chunk));
|
||||||
|
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
|
})
|
||||||
|
.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
token: process.env.BOT_TOKEN,
|
token: process.env.BOT_TOKEN,
|
||||||
admins: process.env.ADMIN_LIST?.split(/\s*,\s*/g).map(x => +x) ?? [],
|
admins: process.env.ADMIN_LIST?.split(/\s*,\s*/g).map((x) => +x) ?? [],
|
||||||
};
|
};
|
||||||
config.whitelist = [].concat(
|
config.whitelist = [].concat(
|
||||||
process.env.WHITELIST?.split(/\s*,\s*/g).map(x => +x) ?? [],
|
process.env.WHITELIST?.split(/\s*,\s*/g).map((x) => +x) ?? [],
|
||||||
config.admins,
|
config.admins,
|
||||||
);
|
);
|
||||||
config.whitelist = Object.keys(
|
config.whitelist = Object.keys(
|
||||||
Object.fromEntries(config.whitelist.map(x => [x, true]))
|
Object.fromEntries(config.whitelist.map((x) => [x, true])),
|
||||||
).map(x => +x);
|
).map((x) => +x);
|
||||||
|
|
||||||
console.info(config);
|
console.info(config);
|
||||||
|
|
||||||
config.admins = Object.fromEntries(config.admins.map(x => [x, true]));
|
config.admins = Object.fromEntries(config.admins.map((x) => [x, true]));
|
||||||
config.whitelist = Object.fromEntries(config.whitelist.map(x => [x, true]));
|
config.whitelist = Object.fromEntries(config.whitelist.map((x) => [x, true]));
|
||||||
|
|
||||||
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
||||||
|
|
||||||
|
class TgBotSender {
|
||||||
|
constructor(parent, bot, msg, memory = {}) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.bot = bot;
|
||||||
|
this.msg = msg;
|
||||||
|
|
||||||
|
this.memory = memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
async textWithMedia(data, reply_markup) {
|
||||||
|
await this.bot.editMessageMedia(data, {
|
||||||
|
chat_id: this.msg.chat.id,
|
||||||
|
message_id: this.msg.message_id,
|
||||||
|
reply_markup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async text(text, reply_markup) {
|
||||||
|
await this.bot.editMessageText(text, {
|
||||||
|
chat_id: this.msg.chat.id,
|
||||||
|
message_id: this.msg.message_id,
|
||||||
|
reply_markup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(video, quality = DownloadVideo.qualities.mp4_best) {
|
||||||
|
const { msg } = this.memory;
|
||||||
|
|
||||||
|
const sent = await this.parent.send(msg, "Start download");
|
||||||
|
const info = await video.getVideoInfo();
|
||||||
|
await video.setupQuality(quality, false);
|
||||||
|
let cooldown = new Date();
|
||||||
|
video
|
||||||
|
.download(({ percent }) => {
|
||||||
|
const now = new Date();
|
||||||
|
if (now - cooldown <= 1500) return;
|
||||||
|
cooldown = new Date();
|
||||||
|
sent.text(`Downloaded (${percent}%)`);
|
||||||
|
})
|
||||||
|
.then((downloaded) => {
|
||||||
|
sent.text("Download complete");
|
||||||
|
if (downloaded.format.type === "video") {
|
||||||
|
this.bot.sendVideo(
|
||||||
|
msg.chat.id,
|
||||||
|
downloaded.data,
|
||||||
|
{ caption: info.title },
|
||||||
|
{
|
||||||
|
filename: `downloaded.${downloaded.format.exc}`,
|
||||||
|
contentType: downloaded.format.mime,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (downloaded.format.type === "audio") {
|
||||||
|
this.bot.sendAudio(
|
||||||
|
msg.chat.id,
|
||||||
|
downloaded.data,
|
||||||
|
{ caption: info.title },
|
||||||
|
{
|
||||||
|
filename: `downloaded.${downloaded.format.exc}`,
|
||||||
|
contentType: downloaded.format.mime,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
//console.error(err.stack);
|
||||||
|
this.bot.sendMessage(msg.chat.id, "Download failed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = new Object();
|
||||||
class TgBot {
|
class TgBot {
|
||||||
constructor () {
|
constructor() {
|
||||||
this.bot = new TelegramBot(config.token, {
|
this.bot = new TelegramBot(config.token, {
|
||||||
polling: true
|
polling: true,
|
||||||
});
|
});
|
||||||
this.bot.on('text', m => this.newMessageHandler(m));
|
this.bot.on("text", (m) => this.newMessageHandler(m));
|
||||||
}
|
this.bot.on("callback_query", (m) => this.cbHandler(m));
|
||||||
|
}
|
||||||
|
|
||||||
async newMessageHandler (msg) {
|
async cbHandler(query) {
|
||||||
console.debug(msg);
|
const key = query.from.id + ":" + query.data;
|
||||||
if (msg.chat.id === msg.from.id) {
|
//console.debug(query, actions[key], key);
|
||||||
if (config.whitelist[msg.from.id]) {
|
actions[key]?.();
|
||||||
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) {
|
async send(msg, txt) {
|
||||||
this.bot.sendMessage(msg.chat.id, "Start download");
|
const result = await this.bot.sendMessage(msg.chat.id, txt);
|
||||||
await video.setupQuality(quality, false);
|
return new TgBotSender(this, this.bot, result, { msg });
|
||||||
video.download()
|
}
|
||||||
.then(downloaded => {
|
|
||||||
if (/^video\//m.test(downloaded.format.mime)) {
|
async newMessageHandler(msg) {
|
||||||
this.bot.sendVideo(msg.chat.id, downloaded.data, { caption: "Download complete" }, {
|
if (msg.chat.id === msg.from.id) {
|
||||||
filename: `downloaded.${downloaded.format.exc}`,
|
if (config.whitelist[msg.from.id]) {
|
||||||
contentType: downloaded.format.mime
|
if (ytLinkRegexp.test(msg.text)) {
|
||||||
});
|
const sent = await this.send(msg, "Wait..");
|
||||||
}
|
const errHandler = (err) => {
|
||||||
})
|
//console.error(err.stack);
|
||||||
.catch(() => {
|
sent.text("Error. Invalid/Hidden video or Forbidden for download");
|
||||||
this.bot.sendMessage(msg.chat.id, "Download failed");
|
};
|
||||||
});
|
|
||||||
}
|
try {
|
||||||
|
const video = new DownloadVideo(msg.text);
|
||||||
|
const info = await video.getVideoInfo();
|
||||||
|
//console.debug(info);
|
||||||
|
// Download only video with best quality
|
||||||
|
const qualities = await video.qualities();
|
||||||
|
//console.debug(qualities);
|
||||||
|
let keyboard = [];
|
||||||
|
let keyboardM = [];
|
||||||
|
let kbChunk = [];
|
||||||
|
let kbChunkM = [];
|
||||||
|
|
||||||
|
for (let quality in qualities) {
|
||||||
|
const qualityItem = qualities[quality];
|
||||||
|
if (qualityItem.type !== null) {
|
||||||
|
if (qualityItem.type === "audio")
|
||||||
|
kbChunkM.push({
|
||||||
|
text: `🎧 ${qualityItem.exc}/${qualityItem.resolution}`,
|
||||||
|
callback_data: quality + "_" + msg.message_id,
|
||||||
|
});
|
||||||
|
else if (qualityItem.type === "video")
|
||||||
|
kbChunk.push({
|
||||||
|
text: `📹 ${qualityItem.exc}/${qualityItem.resolution}`,
|
||||||
|
callback_data: quality + "_" + msg.message_id,
|
||||||
|
});
|
||||||
|
if (kbChunk.length >= 3) {
|
||||||
|
keyboard.push(kbChunk);
|
||||||
|
kbChunk = [];
|
||||||
|
}
|
||||||
|
if (kbChunkM.length >= 3) {
|
||||||
|
keyboardM.push(kbChunkM);
|
||||||
|
kbChunkM = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kbChunk.length > 0) keyboard.push(kbChunk);
|
||||||
|
if (kbChunkM.length > 0) keyboardM.push(kbChunkM);
|
||||||
|
keyboard = keyboard.concat(keyboardM);
|
||||||
|
//keyboard = keyboard.slice(-4);
|
||||||
|
//console.debug(keyboard);
|
||||||
|
//console.debug(info.title, info.thumbnail);
|
||||||
|
const thenHandler = () => {
|
||||||
|
for (let quality in qualities) {
|
||||||
|
const qualityItem = qualities[quality];
|
||||||
|
const actKey =
|
||||||
|
msg.from.id + ":" + quality + "_" + msg.message_id;
|
||||||
|
actions[actKey] = () => {
|
||||||
|
sent
|
||||||
|
.download(video, qualityItem)
|
||||||
|
.then(() => {})
|
||||||
|
.catch(errHandler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
};
|
||||||
|
await sent
|
||||||
|
.textWithMedia(
|
||||||
|
{
|
||||||
|
type: "photo",
|
||||||
|
media: info.thumbnail,
|
||||||
|
//media: { source: await getImage(info.thumbnail) },
|
||||||
|
//media: "https://placekitten.com/500/350",
|
||||||
|
caption: info.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inline_keyboard: keyboard,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(thenHandler)
|
||||||
|
.catch(() => {
|
||||||
|
sent
|
||||||
|
.textWithMedia(
|
||||||
|
{
|
||||||
|
type: "photo",
|
||||||
|
//media: info.thumbnail,
|
||||||
|
media: "https://placekitten.com/500/350",
|
||||||
|
caption: info.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inline_keyboard: keyboard,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(thenHandler)
|
||||||
|
.catch(errHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
//this.download(msg, video);
|
||||||
|
//this.send(msg, "Start download");
|
||||||
|
} catch (e) {
|
||||||
|
errHandler(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.send(msg, "Invalid youtube link");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.send(msg, "Permission denied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bot = new TgBot();
|
const bot = new TgBot();
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
|
const mime = require("mime");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const formatsCache = new Map();
|
const formatsCache = new Map();
|
||||||
|
const infoCache = new Map();
|
||||||
class DownloadVideo {
|
class DownloadVideo {
|
||||||
static get qualities () {
|
static get qualities () {
|
||||||
const q = {
|
const q = {
|
||||||
@ -20,12 +23,55 @@ class DownloadVideo {
|
|||||||
if (!this.quality) this.quality = CLASS.qualities.default;
|
if (!this.quality) this.quality = CLASS.qualities.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getVideoInfo() {
|
||||||
|
const fromCache = infoCache.get(this.url);
|
||||||
|
if (fromCache) return fromCache;
|
||||||
|
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("yt-dlp", ["--cookies", "cookies.txt", "-j", this.url]);
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
|
||||||
|
child.stdout.on("data", chunk => {
|
||||||
|
data += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", err => {
|
||||||
|
console.error("yt-dlp error:", err.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", code => {
|
||||||
|
if (code !== 0) return reject(new Error("yt-dlp failed to get info"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = JSON.parse(data);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
id: info.id,
|
||||||
|
title: info.title,
|
||||||
|
description: info.description,
|
||||||
|
uploader: info.uploader,
|
||||||
|
duration: info.duration, // секунды
|
||||||
|
thumbnail:
|
||||||
|
info.thumbnails?.[info.thumbnails.length - 1]?.url ||
|
||||||
|
info.thumbnail, // лучшее превью
|
||||||
|
url: info.webpage_url,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
infoCache.set(this.url, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async getFormats() {
|
async getFormats() {
|
||||||
const fromCache = formatsCache.get(this.url);
|
const fromCache = formatsCache.get(this.url);
|
||||||
if (fromCache) return fromCache;
|
if (fromCache) return fromCache;
|
||||||
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
const child = spawn("yt-dlp", ["-j", this.url]);
|
const child = spawn("yt-dlp", ["--cookies", "cookies.txt", "-j", this.url]);
|
||||||
let data = "";
|
let data = "";
|
||||||
|
|
||||||
child.stdout.on("data", (chunk) => (data += chunk.toString()));
|
child.stdout.on("data", (chunk) => (data += chunk.toString()));
|
||||||
@ -56,12 +102,17 @@ class DownloadVideo {
|
|||||||
}
|
}
|
||||||
//const flag = f.format_id;
|
//const flag = f.format_id;
|
||||||
|
|
||||||
|
const mimeType = mime.getType(f.ext);
|
||||||
|
const type = (resolution === "audio-only" || /^audio\//m.test(mimeType)) ? "audio" : (
|
||||||
|
/^video\//m.test(mimeType) ? "video" : null
|
||||||
|
);
|
||||||
q[f.ext + "_" + resolution] = {
|
q[f.ext + "_" + resolution] = {
|
||||||
format_id: f.format_id,
|
format_id: f.format_id,
|
||||||
flag, // <- ключевой момент для yt-dlp
|
flag, // <- ключевой момент для yt-dlp
|
||||||
exc: f.ext, // <- расширение
|
exc: f.ext, // <- расширение
|
||||||
resolution,
|
resolution,
|
||||||
filesize: f.filesize || null,
|
filesize: f.filesize || null,
|
||||||
|
mime: mimeType, type: !mimeType ? null : type
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,11 +137,9 @@ class DownloadVideo {
|
|||||||
async download(cb) {
|
async download(cb) {
|
||||||
const data = await new Promise((resolve, reject) => {
|
const data = await new Promise((resolve, reject) => {
|
||||||
const params = [
|
const params = [
|
||||||
"-f",
|
"--cookies", "cookies.txt",
|
||||||
this.quality.flag,
|
"-f", this.quality.flag,
|
||||||
"-o",
|
"-o", "-", this.url,
|
||||||
"-",
|
|
||||||
this.url,
|
|
||||||
];
|
];
|
||||||
const child = spawn("yt-dlp", params);
|
const child = spawn("yt-dlp", params);
|
||||||
|
|
||||||
@ -107,7 +156,7 @@ class DownloadVideo {
|
|||||||
const str = data.toString();
|
const str = data.toString();
|
||||||
const match = str.match(/(\d+\.\d+)%/);
|
const match = str.match(/(\d+\.\d+)%/);
|
||||||
if (match && cb) {
|
if (match && cb) {
|
||||||
cb({ progress: parseFloat(match[1]), downloaded });
|
cb({ percent: parseFloat(match[1]), downloaded });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,10 +169,11 @@ class DownloadVideo {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
format: this.quality,
|
format: this.quality,
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/package-lock.json
generated
20
src/package-lock.json
generated
@ -5,6 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mime": "^3.0.0",
|
||||||
"node-telegram-bot-api": "^0.66.0"
|
"node-telegram-bot-api": "^0.66.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1243,14 +1244,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mime": "cli.js"
|
"mime": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
@ -1296,6 +1297,17 @@
|
|||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-telegram-bot-api/node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/oauth-sign": {
|
"node_modules/oauth-sign": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mime": "^3.0.0",
|
||||||
"node-telegram-bot-api": "^0.66.0"
|
"node-telegram-bot-api": "^0.66.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user