Fix errors on download, migrate method to file upload, add status of upload

This commit is contained in:
FullGreaM 2025-10-22 02:05:40 +03:00
parent 02fa9fa129
commit 5b2911feaf
3 changed files with 130 additions and 35 deletions

View File

@ -1,5 +1,6 @@
const TelegramBot = require("node-telegram-bot-api");
const fs = require("fs");
const path = require("path");
const { DownloadVideo, YtdlUpdater } = require("./download");
const https = require("https");
@ -43,6 +44,10 @@ if (!process.env.BOT_TOKEN) {
config.admins = Object.fromEntries(config.admins.map((x) => [x, true]));
config.whitelist = Object.fromEntries(config.whitelist.map((x) => [x, true]));
try {
fs.rmdirSync(path.join(__dirname, "tmp"), { recursive: true, force: true }, () => {});
} catch (_) {}
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
class TgBotSender {
@ -81,30 +86,50 @@ class TgBotSender {
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}`);
const finallyHandler = (result) => {
if (isStream) {
fs.rm(isStream, { recursive: true, force: true }, () => {});
if (result)
fs.rm(result, { recursive: true, force: true }, () => {});
}
};
video
.download(({ percent, downloaded, fine, msg }) => {
//console.debug({ percent, fine, downloaded });
//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" : msg);
}, true)
.then((downloaded) => {
sent.text(!msg ? "Download complete. Converting.." : msg);
}, isStream)
.then(async (downloaded) => {
console.debug("downloaded:", downloaded);
if (downloaded.format.type === "video") {
this.bot.sendVideo(
await this.bot.sendChatAction(msg.chat.id, 'upload_document');
const uploadFileInterval = setInterval(() => {
this.bot.sendChatAction(msg.chat.id, 'upload_document');
}, 4250);
await new Promise(r => setTimeout(r, 3000));
await this.bot.sendVideo(
msg.chat.id,
downloaded.data,
{ caption: info.title },
{
filename: `downloaded.${downloaded.format.exc}`,
contentType: downloaded.format.mime,
filename,
contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime,
//contentType: downloaded.format.mime,
},
);
clearInterval(uploadFileInterval);
} else if (downloaded.format.type === "audio") {
this.bot.sendAudio(
await this.bot.sendAudio(
msg.chat.id,
downloaded.data,
{ caption: info.title },
@ -114,10 +139,12 @@ class TgBotSender {
},
);
}
finallyHandler(downloaded.data);
})
.catch((err) => {
//console.error(err.stack);
console.error(err.stack);
this.bot.sendMessage(msg.chat.id, "Download failed");
finallyHandler();
});
}
}
@ -150,6 +177,7 @@ class VideosGarbageCollector {
}
tasks.forEach(task => task());
if (tasks.length > 0)
console.info("GC Clean up", tasks.length, "items(s)");
}, 60000);
}

View File

@ -3,6 +3,7 @@ const { Readable } = require("stream");
const EventEmitter = require("events");
const mime = require("mime");
const path = require("path");
const fs = require("fs");
const FFMPEG = require("./ffmpeg");
const formatsCache = new Map();
@ -195,12 +196,15 @@ class DownloadVideo {
if (f.height) {
//flag = `best[height<=${f.height}][ext=${f.ext}]`;
flag = [
`best[height<=${
f.height
}]`,
/*`bestvideo[height<=${
f.height
}]+bestaudio[ext=m4a]/best`,*/
`bestvideo[height<=${
/*`bestvideo[height<=${
f.height
}][vcodec^=avc1]+bestaudio[ext=m4a]/best[ext=${f.ext}]`,
}][vcodec^=avc1]+bestaudio[ext=m4a]/best[ext=${f.ext}]`,*/
/*`bestvideo[height<=${
f.height
}][vcodec^=mov]+bestaudio[ext=m4a]/best`,*/
@ -244,11 +248,12 @@ class DownloadVideo {
this.quality = quality;
return true;
}
return false;
return true;
}
async download(cb, isStream = false) {
const signal = new EventEmitter();
const isPath = typeof isStream === "string";
if (!this.parent.updater.lock(signal)) {
await new Promise((rs => {
this.parent.updater.once("finish", () => {
@ -263,13 +268,13 @@ class DownloadVideo {
[this.quality.flag] : this.quality.flag;
const params = [
"--cookies", "cookies.txt",
"-f", ...flags, "%%",
"-o", "-", this.url,
"-f", ...flags,
"-o", !isPath ? "-" : isStream, this.url,
];
d(params);
d("yt-dlp " + params.join(" "));
const child = spawn("yt-dlp", params);
let downloaded = 0;
let lastPercent = 0;
let percent = 0;
let sizeTg = 0;
let fine = false;
const getDwnld = () => `Real size: ${downloaded}, total: ${sizeTg}`;
@ -279,41 +284,71 @@ class DownloadVideo {
//console.debug(str);
const match = str.match(/(\d+\.\d+)%/);
if (match && cb) {
const percent = parseFloat(match[1]);
if (percent > lastPercent)
lastPercent = percent;
cb({ percent: lastPercent, downloaded: getDwnld(), fine });
percent = parseFloat(match[1]);
cb({ percent, downloaded: !isPath ? getDwnld() : "Processing", fine });
}
});
if (isStream) {
if (isPath) {
child.on("close", (code) => {
signal.emit("finish");
fine = true;
if (code !== 0) {
reject(new Error(`yt-dlp exited with code ${code}`));
return cb({ percent, downloaded, fine, msg: "Download stream failed" });
}
cb({ percent: 100, downloaded, fine: true });
if (this.quality.type !== "video")
return resolve(isStream);
const ffmpeg = new FFMPEG(isStream);
const fileConverted = isStream +
".converted." +
this.quality.exc;
return ffmpeg.convert(FFMPEG.fromFile(
isStream, fileConverted
)).then(readyFile => {
return resolve(readyFile);
}).catch(err => {
reject(err);
});
});
return;
}
else if (isStream) {
const stream = new Readable({ read() {} });
const ffmpeg = new FFMPEG(stream);
child.stdout.on("data", (chunk) => {
stream.push(chunk);
downloaded += chunk.length;
cb({ percent: lastPercent, downloaded: getDwnld(), fine });
cb({ percent, downloaded: getDwnld(), fine });
});
child.on("close", (code) => {
stream.push(null);
signal.emit("finish");
cb({ percent: 100, downloaded, fine: true });
fine = true;
if (code !== 0) reject(new Error(`yt-dlp exited with code ${code}`));
if (code !== 0) {
reject(new Error(`yt-dlp exited with code ${code}`));
return cb({ percent, downloaded, fine, msg: "Download stream failed" });
}
cb({ percent: 100, downloaded, fine: true });
});
child.on("error", reject);
//return resolve(stream);
const resultedStream = await ffmpeg.convert();
//console.debug(this.quality);
const resultedStream = this.quality.type === "video" ?
(await ffmpeg.convert()) : stream;
//const resultedStream = stream;
resultedStream.on("data", chunk => {
sizeTg += chunk.length;
if (sizeTg > 50 * 1024 * 1024) {
fine = true;
cb({ percent: lastPercent, downloaded, fine, msg: "Download failed: 50MB Telegram limit reached" });
cb({ percent, downloaded, fine, msg: "Download failed: 50MB Telegram limit reached" });
resultedStream.push(null);
}
});
@ -324,7 +359,7 @@ class DownloadVideo {
child.stdout.on("data", (chunk) => {
chunks.push(chunk);
downloaded += chunk.length;
cb({ percent: lastPercent, downloaded, fine: false });
cb({ percent, downloaded, fine: false });
});
child.on("close", (code) => {
@ -332,6 +367,9 @@ class DownloadVideo {
signal.emit("finish");
cb({ percent: 100, downloaded, fine });
if (code === 0) {
if (isPath) {
}
resolve(Buffer.concat(chunks));
} else {
reject(new Error(`yt-dlp exited with code ${code}`));

View File

@ -1,15 +1,34 @@
const { Readable, PassThrough } = require("stream");
const { spawn } = require("child_process");
class FromFileFFMPEG {
constructor (source, video) {
this.source = source;
this.video = video;
}
get args () {
return [
"-y", "-i", this.source,
'-q:v', '2', this.video
];
}
}
class FFMPEG {
constructor(data) {
this.data = data; // Can be Readable or Buffer
this.isBuffer = data instanceof Buffer;
}
convert() {
static fromFile (source, video) {
return new FromFileFFMPEG(source, video);
}
convert(fromFile) {
return new Promise((resolve, reject) => {
const ffmpegArgs = [
const isFromFile = fromFile instanceof FromFileFFMPEG;
const ffmpegArgs = !isFromFile ? [
"-i",
"pipe:0",
"-c:v",
@ -29,10 +48,23 @@ class FFMPEG {
"-f",
"mp4",
"pipe:1",
];
] : fromFile.args;
console.debug("ffmpeg", ...ffmpegArgs);
const ff = spawn("ffmpeg", ffmpegArgs);
let errData = "";
ff.stderr.on("data", (chunk) => (errData += chunk.toString()));
if (isFromFile) {
ff.on("close", (code) => {
if (code !== 0) {
return reject(new Error(`FFMPEG exited with code ${code}\n${errData}`));
}
resolve(fromFile.video);
});
return;
}
const inputStream = this.isBuffer ? Readable.from(this.data) : this.data;
inputStream.pipe(ff.stdin);
@ -40,9 +72,6 @@ class FFMPEG {
const output = new PassThrough();
ff.stdout.pipe(output);
let errData = "";
ff.stderr.on("data", (chunk) => (errData += chunk.toString()));
ff.on("close", (code) => {
if (code !== 0) {
reject(new Error(`FFMPEG exited with code ${code}\n${errData}`));