finish first revision of bot

This commit is contained in:
fullgream 2025-10-15 07:05:50 +03:00
parent 7a0601bd5e
commit a865c192bf
4 changed files with 301 additions and 61 deletions

View File

@ -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) {
console.debug(msg); async cbHandler(query) {
if (msg.chat.id === msg.from.id) { const key = query.from.id + ":" + query.data;
if (config.whitelist[msg.from.id]) { //console.debug(query, actions[key], key);
if (ytLinkRegexp.test(msg.text)) { actions[key]?.();
const video = new DownloadVideo(msg.text); }
// Download only video with best quality
this.download(msg, video); async send(msg, txt) {
} else { const result = await this.bot.sendMessage(msg.chat.id, txt);
this.bot.sendMessage(msg.chat.id, "Invalid youtube link"); return new TgBotSender(this, this.bot, result, { msg });
} }
}
else { async newMessageHandler(msg) {
this.bot.sendMessage(msg.chat.id, "Permission denied"); if (msg.chat.id === msg.from.id) {
} if (config.whitelist[msg.from.id]) {
} if (ytLinkRegexp.test(msg.text)) {
} const sent = await this.send(msg, "Wait..");
const errHandler = (err) => {
async download (msg, video, quality=DownloadVideo.qualities.mp4_best) { //console.error(err.stack);
this.bot.sendMessage(msg.chat.id, "Start download"); sent.text("Error. Invalid/Hidden video or Forbidden for download");
await video.setupQuality(quality, false); };
video.download()
.then(downloaded => { try {
if (/^video\//m.test(downloaded.format.mime)) { const video = new DownloadVideo(msg.text);
this.bot.sendVideo(msg.chat.id, downloaded.data, { caption: "Download complete" }, { const info = await video.getVideoInfo();
filename: `downloaded.${downloaded.format.exc}`, //console.debug(info);
contentType: downloaded.format.mime // Download only video with best quality
}); const qualities = await video.qualities();
} //console.debug(qualities);
}) let keyboard = [];
.catch(() => { let keyboardM = [];
this.bot.sendMessage(msg.chat.id, "Download failed"); 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();

View File

@ -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 = {
@ -19,13 +22,56 @@ class DownloadVideo {
this.quality = quality; this.quality = quality;
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
View File

@ -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",

View File

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"mime": "^3.0.0",
"node-telegram-bot-api": "^0.66.0" "node-telegram-bot-api": "^0.66.0"
} }
} }