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 TelegramBot = require("node-telegram-bot-api");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const { DownloadVideo, YtdlUpdater } = require("./download");
|
const { DownloadVideo, YtdlUpdater } = require("./download");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
|
|
||||||
@ -43,6 +44,10 @@ if (!process.env.BOT_TOKEN) {
|
|||||||
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]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(path.join(__dirname, "tmp"), { recursive: true, force: true }, () => {});
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
||||||
|
|
||||||
class TgBotSender {
|
class TgBotSender {
|
||||||
@ -81,30 +86,50 @@ class TgBotSender {
|
|||||||
const info = await video.getVideoInfo();
|
const info = await video.getVideoInfo();
|
||||||
await video.setupQuality(quality, false);
|
await video.setupQuality(quality, false);
|
||||||
let cooldown = new Date();
|
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
|
video
|
||||||
.download(({ percent, downloaded, fine, msg }) => {
|
.download(({ percent, downloaded, fine, msg }) => {
|
||||||
//console.debug({ percent, fine, downloaded });
|
//console.debug({ percent, fine, downloaded, msg });
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
if (!fine && now - cooldown <= 3500) return;
|
if (!fine && now - cooldown <= 3500) return;
|
||||||
cooldown = new Date();
|
cooldown = new Date();
|
||||||
if (!fine)
|
if (!fine)
|
||||||
sent.text(`Downloaded (${percent}%), ${downloaded} bytes`);
|
sent.text(`Downloaded (${percent}%), ${downloaded} bytes`);
|
||||||
else
|
else
|
||||||
sent.text(!msg ? "Download complete" : msg);
|
sent.text(!msg ? "Download complete. Converting.." : msg);
|
||||||
}, true)
|
}, isStream)
|
||||||
.then((downloaded) => {
|
.then(async (downloaded) => {
|
||||||
|
console.debug("downloaded:", downloaded);
|
||||||
if (downloaded.format.type === "video") {
|
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,
|
msg.chat.id,
|
||||||
downloaded.data,
|
downloaded.data,
|
||||||
{ caption: info.title },
|
{ caption: info.title },
|
||||||
{
|
{
|
||||||
filename: `downloaded.${downloaded.format.exc}`,
|
filename,
|
||||||
contentType: downloaded.format.mime,
|
contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime,
|
||||||
|
//contentType: downloaded.format.mime,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
clearInterval(uploadFileInterval);
|
||||||
} else if (downloaded.format.type === "audio") {
|
} else if (downloaded.format.type === "audio") {
|
||||||
this.bot.sendAudio(
|
await this.bot.sendAudio(
|
||||||
msg.chat.id,
|
msg.chat.id,
|
||||||
downloaded.data,
|
downloaded.data,
|
||||||
{ caption: info.title },
|
{ caption: info.title },
|
||||||
@ -114,10 +139,12 @@ class TgBotSender {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
finallyHandler(downloaded.data);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
//console.error(err.stack);
|
console.error(err.stack);
|
||||||
this.bot.sendMessage(msg.chat.id, "Download failed");
|
this.bot.sendMessage(msg.chat.id, "Download failed");
|
||||||
|
finallyHandler();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,7 +177,8 @@ class VideosGarbageCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.forEach(task => task());
|
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);
|
}, 60000);
|
||||||
}
|
}
|
||||||
register (key, act, pinnedVideo) {
|
register (key, act, pinnedVideo) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const { Readable } = require("stream");
|
|||||||
const EventEmitter = require("events");
|
const EventEmitter = require("events");
|
||||||
const mime = require("mime");
|
const mime = require("mime");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
const FFMPEG = require("./ffmpeg");
|
const FFMPEG = require("./ffmpeg");
|
||||||
|
|
||||||
const formatsCache = new Map();
|
const formatsCache = new Map();
|
||||||
@ -195,12 +196,15 @@ class DownloadVideo {
|
|||||||
if (f.height) {
|
if (f.height) {
|
||||||
//flag = `best[height<=${f.height}][ext=${f.ext}]`;
|
//flag = `best[height<=${f.height}][ext=${f.ext}]`;
|
||||||
flag = [
|
flag = [
|
||||||
|
`best[height<=${
|
||||||
|
f.height
|
||||||
|
}]`,
|
||||||
/*`bestvideo[height<=${
|
/*`bestvideo[height<=${
|
||||||
f.height
|
f.height
|
||||||
}]+bestaudio[ext=m4a]/best`,*/
|
}]+bestaudio[ext=m4a]/best`,*/
|
||||||
`bestvideo[height<=${
|
/*`bestvideo[height<=${
|
||||||
f.height
|
f.height
|
||||||
}][vcodec^=avc1]+bestaudio[ext=m4a]/best[ext=${f.ext}]`,
|
}][vcodec^=avc1]+bestaudio[ext=m4a]/best[ext=${f.ext}]`,*/
|
||||||
/*`bestvideo[height<=${
|
/*`bestvideo[height<=${
|
||||||
f.height
|
f.height
|
||||||
}][vcodec^=mov]+bestaudio[ext=m4a]/best`,*/
|
}][vcodec^=mov]+bestaudio[ext=m4a]/best`,*/
|
||||||
@ -244,11 +248,12 @@ class DownloadVideo {
|
|||||||
this.quality = quality;
|
this.quality = quality;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async download(cb, isStream = false) {
|
async download(cb, isStream = false) {
|
||||||
const signal = new EventEmitter();
|
const signal = new EventEmitter();
|
||||||
|
const isPath = typeof isStream === "string";
|
||||||
if (!this.parent.updater.lock(signal)) {
|
if (!this.parent.updater.lock(signal)) {
|
||||||
await new Promise((rs => {
|
await new Promise((rs => {
|
||||||
this.parent.updater.once("finish", () => {
|
this.parent.updater.once("finish", () => {
|
||||||
@ -263,13 +268,13 @@ class DownloadVideo {
|
|||||||
[this.quality.flag] : this.quality.flag;
|
[this.quality.flag] : this.quality.flag;
|
||||||
const params = [
|
const params = [
|
||||||
"--cookies", "cookies.txt",
|
"--cookies", "cookies.txt",
|
||||||
"-f", ...flags, "%%",
|
"-f", ...flags,
|
||||||
"-o", "-", this.url,
|
"-o", !isPath ? "-" : isStream, this.url,
|
||||||
];
|
];
|
||||||
d(params);
|
d("yt-dlp " + params.join(" "));
|
||||||
const child = spawn("yt-dlp", params);
|
const child = spawn("yt-dlp", params);
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let lastPercent = 0;
|
let percent = 0;
|
||||||
let sizeTg = 0;
|
let sizeTg = 0;
|
||||||
let fine = false;
|
let fine = false;
|
||||||
const getDwnld = () => `Real size: ${downloaded}, total: ${sizeTg}`;
|
const getDwnld = () => `Real size: ${downloaded}, total: ${sizeTg}`;
|
||||||
@ -279,41 +284,71 @@ class DownloadVideo {
|
|||||||
//console.debug(str);
|
//console.debug(str);
|
||||||
const match = str.match(/(\d+\.\d+)%/);
|
const match = str.match(/(\d+\.\d+)%/);
|
||||||
if (match && cb) {
|
if (match && cb) {
|
||||||
const percent = parseFloat(match[1]);
|
percent = parseFloat(match[1]);
|
||||||
if (percent > lastPercent)
|
cb({ percent, downloaded: !isPath ? getDwnld() : "Processing", fine });
|
||||||
lastPercent = percent;
|
|
||||||
cb({ percent: lastPercent, downloaded: getDwnld(), 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 stream = new Readable({ read() {} });
|
||||||
const ffmpeg = new FFMPEG(stream);
|
const ffmpeg = new FFMPEG(stream);
|
||||||
|
|
||||||
child.stdout.on("data", (chunk) => {
|
child.stdout.on("data", (chunk) => {
|
||||||
stream.push(chunk);
|
stream.push(chunk);
|
||||||
downloaded += chunk.length;
|
downloaded += chunk.length;
|
||||||
cb({ percent: lastPercent, downloaded: getDwnld(), fine });
|
cb({ percent, downloaded: getDwnld(), fine });
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
stream.push(null);
|
stream.push(null);
|
||||||
signal.emit("finish");
|
signal.emit("finish");
|
||||||
cb({ percent: 100, downloaded, fine: true });
|
|
||||||
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);
|
child.on("error", reject);
|
||||||
//return resolve(stream);
|
//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;
|
//const resultedStream = stream;
|
||||||
|
|
||||||
resultedStream.on("data", chunk => {
|
resultedStream.on("data", chunk => {
|
||||||
sizeTg += chunk.length;
|
sizeTg += chunk.length;
|
||||||
if (sizeTg > 50 * 1024 * 1024) {
|
if (sizeTg > 50 * 1024 * 1024) {
|
||||||
fine = true;
|
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);
|
resultedStream.push(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -324,7 +359,7 @@ class DownloadVideo {
|
|||||||
child.stdout.on("data", (chunk) => {
|
child.stdout.on("data", (chunk) => {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
downloaded += chunk.length;
|
downloaded += chunk.length;
|
||||||
cb({ percent: lastPercent, downloaded, fine: false });
|
cb({ percent, downloaded, fine: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
@ -332,6 +367,9 @@ class DownloadVideo {
|
|||||||
signal.emit("finish");
|
signal.emit("finish");
|
||||||
cb({ percent: 100, downloaded, fine });
|
cb({ percent: 100, downloaded, fine });
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
if (isPath) {
|
||||||
|
|
||||||
|
}
|
||||||
resolve(Buffer.concat(chunks));
|
resolve(Buffer.concat(chunks));
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`yt-dlp exited with code ${code}`));
|
reject(new Error(`yt-dlp exited with code ${code}`));
|
||||||
|
|||||||
@ -1,15 +1,34 @@
|
|||||||
const { Readable, PassThrough } = require("stream");
|
const { Readable, PassThrough } = require("stream");
|
||||||
const { spawn } = require("child_process");
|
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 {
|
class FFMPEG {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
this.data = data; // Can be Readable or Buffer
|
this.data = data; // Can be Readable or Buffer
|
||||||
this.isBuffer = data instanceof Buffer;
|
this.isBuffer = data instanceof Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
convert() {
|
static fromFile (source, video) {
|
||||||
|
return new FromFileFFMPEG(source, video);
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(fromFile) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ffmpegArgs = [
|
const isFromFile = fromFile instanceof FromFileFFMPEG;
|
||||||
|
const ffmpegArgs = !isFromFile ? [
|
||||||
"-i",
|
"-i",
|
||||||
"pipe:0",
|
"pipe:0",
|
||||||
"-c:v",
|
"-c:v",
|
||||||
@ -29,10 +48,23 @@ class FFMPEG {
|
|||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
"mp4",
|
||||||
"pipe:1",
|
"pipe:1",
|
||||||
];
|
] : fromFile.args;
|
||||||
|
console.debug("ffmpeg", ...ffmpegArgs);
|
||||||
const ff = spawn("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;
|
const inputStream = this.isBuffer ? Readable.from(this.data) : this.data;
|
||||||
|
|
||||||
inputStream.pipe(ff.stdin);
|
inputStream.pipe(ff.stdin);
|
||||||
@ -40,9 +72,6 @@ class FFMPEG {
|
|||||||
const output = new PassThrough();
|
const output = new PassThrough();
|
||||||
ff.stdout.pipe(output);
|
ff.stdout.pipe(output);
|
||||||
|
|
||||||
let errData = "";
|
|
||||||
ff.stderr.on("data", (chunk) => (errData += chunk.toString()));
|
|
||||||
|
|
||||||
ff.on("close", (code) => {
|
ff.on("close", (code) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
reject(new Error(`FFMPEG exited with code ${code}\n${errData}`));
|
reject(new Error(`FFMPEG exited with code ${code}\n${errData}`));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user