458 lines
13 KiB
JavaScript
458 lines
13 KiB
JavaScript
const TelegramBot = require("node-telegram-bot-api");
|
|
const fs = require("fs");
|
|
const mime = require("mime");
|
|
const path = require("path");
|
|
const https = require("https");
|
|
const config = require("./config");
|
|
const { DownloadVideo, YtdlUpdater, downloadFile } = require("./download");
|
|
const { MTProtoUser } = require("./mtproto");
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
function removeTemp () {
|
|
try {
|
|
fs.rmdirSync(path.join(__dirname, "tmp"), { recursive: true, force: true }, () => {});
|
|
} catch (_) {}
|
|
}
|
|
removeTemp();
|
|
|
|
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
|
|
|
async function getFileSize(pathname) {
|
|
const stream = fs.createReadStream(pathname);
|
|
let size = 0;
|
|
return await new Promise((resolve, reject) => {
|
|
stream.on("data", data => size += data.length);
|
|
stream.on("end", () => resolve(size));
|
|
stream.on("error", reject);
|
|
});
|
|
}
|
|
|
|
// Check if download process
|
|
const tgChatsDownloaded = new Object();
|
|
|
|
class TgBotSender {
|
|
constructor(parent, bot, msg, memory = {}) {
|
|
this.parent = parent;
|
|
this.bot = bot;
|
|
this.msg = msg;
|
|
|
|
this.curText = null;
|
|
|
|
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) {
|
|
if (text === this.curText) return;
|
|
await this.bot.editMessageText(text, {
|
|
chat_id: this.msg.chat.id,
|
|
message_id: this.msg.message_id,
|
|
reply_markup,
|
|
});
|
|
this.curText = text;
|
|
}
|
|
|
|
async sendVideo (chatId, data, settings = {}, apiOptions = {}) {
|
|
if (this.parent.mtproto !== null) {
|
|
const mtprotoMID = await this.parent.mtproto.saveMedia(
|
|
data, settings.caption ?? "", {
|
|
thumb: settings.thumb,
|
|
}
|
|
);
|
|
|
|
return await this.bot.copyMessage(
|
|
chatId,
|
|
config.mtproto.chatId,
|
|
mtprotoMID,
|
|
{
|
|
caption: settings.caption ?? "",
|
|
}
|
|
);
|
|
}
|
|
return await this.bot.sendVideo(chatId, data, settings, apiOptions);
|
|
}
|
|
|
|
async sendAudio (chatId, data, settings = {}, apiOptions = {}) {
|
|
if (this.parent.mtproto !== null) {
|
|
const mtprotoMID = await this.parent.mtproto.saveMedia(
|
|
data, settings.caption ?? "", {
|
|
thumb: settings.thumb,
|
|
filename: apiOptions.filename,
|
|
}
|
|
);
|
|
|
|
return await this.bot.copyMessage(
|
|
chatId,
|
|
config.mtproto.chatId,
|
|
mtprotoMID,
|
|
{
|
|
caption: settings.caption ?? "",
|
|
}
|
|
);
|
|
}
|
|
return await this.bot.sendAudio(chatId, data, settings, apiOptions);
|
|
}
|
|
|
|
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();
|
|
//const isStream = true;
|
|
const filename = `${(new Date()).getTime()}.${quality.exc}`;
|
|
const isStream = path.join(__dirname, `tmp/${filename}`);
|
|
|
|
let uploadFileInterval = null;
|
|
const finallyHandler = (result) => {
|
|
clearInterval(uploadFileInterval);
|
|
//return;
|
|
if (isStream && isStream !== true) {
|
|
fs.rm(isStream, { recursive: true, force: true }, () => {});
|
|
if (result)
|
|
fs.rm(result, { recursive: true, force: true }, () => {});
|
|
}
|
|
};
|
|
|
|
const errHandler = (err) => {
|
|
console.error(err.stack);
|
|
//this.bot.sendMessage(msg.chat.id, "Download failed");
|
|
sent.text("Download failed");
|
|
finallyHandler();
|
|
}
|
|
|
|
await video
|
|
.download(({ percent, downloaded, fine, msg }) => {
|
|
//console.debug({ percent, fine, downloaded, msg });
|
|
const now = new Date();
|
|
if (!fine && now - cooldown <= 3500) return;
|
|
cooldown = new Date();
|
|
if (!fine)
|
|
sent.text(`Downloaded (${percent}%), ${downloaded} bytes`);
|
|
else
|
|
sent.text(!msg ? "Download complete. Converting.." : msg);
|
|
}, isStream)
|
|
.then(async (downloaded) => {
|
|
//console.debug("downloaded:", downloaded);
|
|
try {
|
|
const videoInfo = await video.getVideoInfo();
|
|
if (downloaded.format.type === "video") {
|
|
const filesize = (typeof isStream !== "string" || this.parent.mtproto !== null) ?
|
|
0 : await getFileSize(downloaded.data);
|
|
//console.debug("filesize:", filesize);
|
|
if (filesize < 50 * 1024 * 1024) {
|
|
await this.bot.sendChatAction(msg.chat.id, 'upload_document');
|
|
uploadFileInterval = setInterval(() => {
|
|
this.bot.sendChatAction(msg.chat.id, 'upload_document');
|
|
}, 4250);
|
|
await new Promise(r => setTimeout(r, 3000));
|
|
await this.sendVideo(
|
|
msg.chat.id,
|
|
downloaded.data,
|
|
{
|
|
caption: info.title,
|
|
thumb: await downloadFile(videoInfo.thumbnail),
|
|
},
|
|
{
|
|
filename,
|
|
contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime,
|
|
//contentType: downloaded.format.mime,
|
|
},
|
|
);
|
|
/*await this.bot.sendVideo(
|
|
msg.chat.id,
|
|
downloaded.data,
|
|
{ caption: info.title },
|
|
{
|
|
filename,
|
|
contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime,
|
|
//contentType: downloaded.format.mime,
|
|
},
|
|
);*/
|
|
} else {
|
|
return await sent.text(`Maximum filesize reached. Try any types (Current file: ${
|
|
((filesize / (1024 * 1024))+"").split(".")[0]
|
|
} MB)`);
|
|
}
|
|
} else if (downloaded.format.type === "audio") {
|
|
await this.sendAudio(
|
|
msg.chat.id,
|
|
downloaded.data,
|
|
{
|
|
caption: info.title,
|
|
thumb: await downloadFile(videoInfo.thumbnail),
|
|
/*thumbnail: {
|
|
type: mime.getType(videoInfo.thumbnail),
|
|
media: videoInfo.thumbnail,
|
|
}*/
|
|
},
|
|
{
|
|
//filename: `downloaded.${downloaded.format.exc}`,
|
|
filename: `${
|
|
videoInfo.title
|
|
}.mp3`,
|
|
contentType: downloaded.format.mime,
|
|
},
|
|
);
|
|
}
|
|
} catch (err) {
|
|
return errHandler(err);
|
|
}
|
|
finallyHandler(downloaded.data);
|
|
}).catch(errHandler);
|
|
}
|
|
}
|
|
|
|
class VideosGarbageCollector {
|
|
constructor () {
|
|
this.actions = new Object();
|
|
this.pinnedVideos = new Object();
|
|
this.removeTable = new Object();
|
|
|
|
setInterval(() => {
|
|
const tasks = [];
|
|
|
|
for (let registerDate in this.removeTable) {
|
|
const key = this.removeTable[registerDate];
|
|
const savedTo = new Date(registerDate);
|
|
savedTo.setHours(savedTo.getHours() + 3);
|
|
|
|
if ((new Date()) >= savedTo) {
|
|
const video = this.pinnedVideos[key];
|
|
tasks.push(() => {
|
|
// Call destructor
|
|
video.destructor();
|
|
// Delete..
|
|
delete this.actions[key];
|
|
delete this.pinnedVideos[key];
|
|
delete this.removeTable[registerDate];
|
|
});
|
|
}
|
|
}
|
|
|
|
tasks.forEach(task => task());
|
|
if (tasks.length > 0)
|
|
console.info("GC Clean up", tasks.length, "items(s)");
|
|
}, 60000);
|
|
}
|
|
|
|
register (key, act, pinnedVideo) {
|
|
this.actions[key] = act;
|
|
this.pinnedVideos[key] = pinnedVideo;
|
|
this.removeTable[new Date()] = key;
|
|
}
|
|
|
|
removeTemp () {
|
|
if (Object.keys(tgChatsDownloaded).length === 0)
|
|
removeTemp();
|
|
}
|
|
}
|
|
|
|
class TgBot {
|
|
constructor() {
|
|
this.bot = new TelegramBot(config.token, {
|
|
polling: true,
|
|
});
|
|
this.bot.on("text", (m) => this.newMessageHandler(m));
|
|
this.bot.on("callback_query", (m) => this.cbHandler(m));
|
|
this.videosGC = new VideosGarbageCollector();
|
|
this.updater = new YtdlUpdater();
|
|
this.mtproto = null;
|
|
if (config.mtproto !== null) {
|
|
this.mtproto = new MTProtoUser(
|
|
config.mtproto.apiId,
|
|
config.mtproto.apiHash,
|
|
config.mtproto.session,
|
|
);
|
|
}
|
|
}
|
|
|
|
async init () {
|
|
if (this.mtproto === null)
|
|
console.warn("API_ID or API_HASH not defined. MTProto API not used");
|
|
else
|
|
await this.mtproto.init();
|
|
}
|
|
|
|
async cbHandler(query) {
|
|
const key = query.from.id + ":" + query.data;
|
|
//console.debug(query, this.videosGC.actions[key], key);
|
|
if (!this.videosGC.actions[key])
|
|
return this.bot.answerCallbackQuery(query.id, {
|
|
text: "Resend me video link, please",
|
|
show_alert: true
|
|
});
|
|
this.videosGC.actions[key](query.id);
|
|
}
|
|
|
|
async send(msg, txt) {
|
|
const result = await this.bot.sendMessage(msg.chat.id, txt);
|
|
return new TgBotSender(this, this.bot, result, { msg });
|
|
}
|
|
|
|
async newMessageHandler(msg) {
|
|
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) => {
|
|
console.error(err.stack);
|
|
sent.text(
|
|
"Error. Invalid/Hidden video or Forbidden for download"
|
|
);
|
|
};
|
|
|
|
try {
|
|
const video = new DownloadVideo(msg.text);
|
|
video.setupParent(this);
|
|
const info = await video.getVideoInfo();
|
|
sent.text("Info getted, getting formats...");
|
|
//console.debug(info);
|
|
// Download only video with best quality
|
|
const qualities = await video.qualities(true);
|
|
//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 (config.allFormats || ({ mp4: true })[qualityItem.exc]) {
|
|
if (qualityItem.type === "audio")
|
|
kbChunkM.push({
|
|
text: qualityItem.strictName ?? `🎧 ${qualityItem.exc}/${qualityItem.resolution}`,
|
|
callback_data: quality + "_" + msg.message_id,
|
|
});
|
|
else if (qualityItem.type === "video")
|
|
kbChunk.push({
|
|
text: qualityItem.strictName ?? `📹 ${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;
|
|
this.videosGC.register(actKey, async (queryId) => {
|
|
//console.debug("ACT btn:", actKey);
|
|
if (tgChatsDownloaded[msg.from.id])
|
|
return this.bot.answerCallbackQuery(queryId, {
|
|
text: 'Wait until download was not finished',
|
|
show_alert: true
|
|
});
|
|
tgChatsDownloaded[msg.from.id] = true;
|
|
sent
|
|
.download(video, qualityItem)
|
|
.then(() => {
|
|
delete tgChatsDownloaded[msg.from.id];
|
|
this.videosGC.removeTemp();
|
|
})
|
|
.catch((err) => {
|
|
errHandler(err);
|
|
delete tgChatsDownloaded[msg.from.id];
|
|
this.videosGC.removeTemp();
|
|
});
|
|
}, video);
|
|
/*this.videosGC.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: config.emptyImage,
|
|
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\n" +
|
|
"But you can create your bot: " +
|
|
"https://git.fullgream.tech/fullgream/telegram_ytdlp_node.js/");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const bot = new TgBot();
|
|
bot.init();
|