Fix errors on download, migrate method to file upload, add status of upload
This commit is contained in:
parent
02fa9fa129
commit
5b2911feaf
48
src/bot.js
48
src/bot.js
@ -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,7 +177,8 @@ class VideosGarbageCollector {
|
||||
}
|
||||
|
||||
tasks.forEach(task => task());
|
||||
console.info("GC Clean up", tasks.length, "items(s)");
|
||||
if (tasks.length > 0)
|
||||
console.info("GC Clean up", tasks.length, "items(s)");
|
||||
}, 60000);
|
||||
}
|
||||
register (key, act, pinnedVideo) {
|
||||
|
||||
@ -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}`));
|
||||
|
||||
@ -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}`));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user