fast video download + add normal download of audio

This commit is contained in:
FullGreaM 2025-10-25 22:28:34 +03:00
parent 9c96e13f71
commit 4805c89cc3
3 changed files with 140 additions and 23 deletions

View File

@ -1,7 +1,8 @@
const TelegramBot = require("node-telegram-bot-api"); const TelegramBot = require("node-telegram-bot-api");
const fs = require("fs"); const fs = require("fs");
const mime = require("mime");
const path = require("path"); const path = require("path");
const { DownloadVideo, YtdlUpdater } = require("./download"); const { DownloadVideo, YtdlUpdater, downloadFile } = require("./download");
const https = require("https"); const https = require("https");
async function getImage(url) { async function getImage(url) {
@ -24,7 +25,7 @@ async function getImage(url) {
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) ?? [],
allFormats: process.env.SHOW_ALL_FORMATS === "1" allFormats: false
}; };
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) ?? [],
@ -160,12 +161,23 @@ class TgBotSender {
} MB)`); } MB)`);
} }
} else if (downloaded.format.type === "audio") { } else if (downloaded.format.type === "audio") {
const videoInfo = await video.getVideoInfo();
await this.bot.sendAudio( await this.bot.sendAudio(
msg.chat.id, msg.chat.id,
downloaded.data, downloaded.data,
{ caption: info.title },
{ {
filename: `downloaded.${downloaded.format.exc}`, 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, contentType: downloaded.format.mime,
}, },
); );
@ -251,7 +263,11 @@ class TgBot {
const sent = await this.send(msg, "Wait.."); const sent = await this.send(msg, "Wait..");
const errHandler = (err) => { const errHandler = (err) => {
console.error(err.stack); console.error(err.stack);
sent.text("Error. Invalid/Hidden video or Forbidden for download"); sent.text(
"Error. Invalid/Hidden video or Forbidden for download\n" +
"But you can create your bot: " +
"https://git.fullgream.tech/fullgream/telegram_ytdlp_node.js/"
);
}; };
try { try {
@ -271,15 +287,15 @@ class TgBot {
for (let quality in qualities) { for (let quality in qualities) {
const qualityItem = qualities[quality]; const qualityItem = qualities[quality];
if (qualityItem.type !== null) { if (qualityItem.type !== null) {
if (config.allFormats || ({ mp4: true, m4a: true })[qualityItem.exc]) { if (config.allFormats || ({ mp4: true })[qualityItem.exc]) {
if (qualityItem.type === "audio") if (qualityItem.type === "audio")
kbChunkM.push({ kbChunkM.push({
text: `🎧 ${qualityItem.exc}/${qualityItem.resolution}`, text: qualityItem.strictName ?? `🎧 ${qualityItem.exc}/${qualityItem.resolution}`,
callback_data: quality + "_" + msg.message_id, callback_data: quality + "_" + msg.message_id,
}); });
else if (qualityItem.type === "video") else if (qualityItem.type === "video")
kbChunk.push({ kbChunk.push({
text: `📹 ${qualityItem.exc}/${qualityItem.resolution}`, text: qualityItem.strictName ?? `📹 ${qualityItem.exc}/${qualityItem.resolution}`,
callback_data: quality + "_" + msg.message_id, callback_data: quality + "_" + msg.message_id,
}); });
if (kbChunk.length >= 3) { if (kbChunk.length >= 3) {

View File

@ -3,6 +3,9 @@ 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 http = require("http");
const https = require("https");
const { URL } = require("url");
const fs = require("fs"); const fs = require("fs");
const FFMPEG = require("./ffmpeg"); const FFMPEG = require("./ffmpeg");
@ -11,6 +14,48 @@ const infoCache = new Map();
const d = r => console.debug("DEBUG:", r) || r; const d = r => console.debug("DEBUG:", r) || r;
async function downloadFile(fileUrl) {
return new Promise((resolve, reject) => {
try {
const url = new URL(fileUrl);
const protocol = url.protocol === "https:" ? https : http;
const extMatch = path.extname(url.pathname).split("?")[0];
const extension = extMatch ? extMatch.replace(".", "") : "tmp";
const filename = `${Date.now()}.${extension}`;
const savePath = path.join(__dirname, "tmp", filename);
fs.mkdirSync(path.dirname(savePath), { recursive: true });
const file = fs.createWriteStream(savePath);
protocol.get(fileUrl, (response) => {
if (response.statusCode !== 200) {
return reject(
new Error(`Failed to get '${fileUrl}' (${response.statusCode})`)
);
}
response.pipe(file);
file.on("finish", () => {
file.close();
setTimeout(() => {
fs.rm(savePath, () => {});
}, 60000);
resolve(savePath);
});
}).on("error", (err) => {
fs.unlinkSync(savePath);
reject(err);
});
} catch (err) {
reject(err);
}
});
}
class YtdlUpdater extends EventEmitter { class YtdlUpdater extends EventEmitter {
constructor () { constructor () {
super(); super();
@ -62,12 +107,13 @@ class YtdlUpdater extends EventEmitter {
class DownloadVideo { class DownloadVideo {
static get qualities () { static get qualities () {
const q = { const q = {
webm_best: { exc: "webm", flag: "best", mime: "video/webm" }, webm_best: { exc: "webm", flag: "best", mime: "video/webm", type: "video" },
//mp4_best: { exc: "mp4", flag: "best[ext=mp4]", mime: "video/mp4" }, //mp4_best: { exc: "mp4", flag: "best[ext=mp4]", mime: "video/mp4" },
mp4_best: { exc: "mp4", flag: [ mp4_best: { exc: "mp4", flag: [
"bestvideo[height<=1080]+bestaudio/best", "bestvideo[height<=1080]+bestaudio/best",
"--merge-output-format", "mp4" "--merge-output-format", "mp4"
], mime: "video/mp4" }, ], mime: "video/mp4", type: "video", strictName: "📹 Best video" },
audio_default: { exc: "mp4", flag: "worst", mime: "audio/mpeg", type: "audio", strictName: "🎧 Audio" },
}; };
q.default = q.webm_best; q.default = q.webm_best;
@ -139,6 +185,7 @@ class DownloadVideo {
info.thumbnails?.[info.thumbnails.length - 1]?.url || info.thumbnails?.[info.thumbnails.length - 1]?.url ||
info.thumbnail, // лучшее превью info.thumbnail, // лучшее превью
url: info.webpage_url, url: info.webpage_url,
thumbnails: info.thumbnails
}); });
} catch (e) { } catch (e) {
reject(e); reject(e);
@ -309,7 +356,7 @@ class DownloadVideo {
if (isPath) { if (isPath) {
child.stdout.on("data", stderrHandler); child.stdout.on("data", stderrHandler);
child.on("close", (code) => { child.on("close", async (code) => {
//console.debug(">>>>>>>>>>>>>FINISHED"); //console.debug(">>>>>>>>>>>>>FINISHED");
signal.emit("finish"); signal.emit("finish");
fine = true; fine = true;
@ -319,14 +366,16 @@ class DownloadVideo {
} }
cb({ percent: 100, downloaded, fine: true }); cb({ percent: 100, downloaded, fine: true });
if (this.quality.type !== "video") if (this.quality.type !== "audio")
return resolve(isStream); return resolve(isStream);
const ffmpeg = new FFMPEG(isStream); const ffmpeg = new FFMPEG(isStream);
const fileConverted = isStream + const fileConverted = isStream +
".converted." + //".converted." +
this.quality.exc; ".mp3";
return ffmpeg.convert(FFMPEG.fromFile( const videoInfo = await this.getVideoInfo();
isStream, fileConverted //console.debug("videoInfo::thumbnails", videoInfo.thumbnails);
return ffmpeg.convertMusic(FFMPEG.fromFile(
isStream, fileConverted, videoInfo.thumbnail
)).then(readyFile => { )).then(readyFile => {
return resolve(readyFile); return resolve(readyFile);
}).catch(err => { }).catch(err => {
@ -337,6 +386,7 @@ class DownloadVideo {
return; return;
} }
else if (isStream) { else if (isStream) {
console.warn("May be unstable with stream");
const stream = new Readable({ read() {} }); const stream = new Readable({ read() {} });
const ffmpeg = new FFMPEG(stream); const ffmpeg = new FFMPEG(stream);
@ -374,6 +424,7 @@ class DownloadVideo {
}); });
return resolve(resultedStream); return resolve(resultedStream);
} }
console.warn("May be unstable with Buffer");
const chunks = []; const chunks = [];
child.stdout.on("data", (chunk) => { child.stdout.on("data", (chunk) => {
@ -405,4 +456,4 @@ class DownloadVideo {
} }
} }
module.exports = { DownloadVideo, YtdlUpdater }; module.exports = { DownloadVideo, YtdlUpdater, downloadFile };

View File

@ -2,9 +2,10 @@ const { Readable, PassThrough } = require("stream");
const { spawn } = require("child_process"); const { spawn } = require("child_process");
class FromFileFFMPEG { class FromFileFFMPEG {
constructor (source, video) { constructor (source, video, preview = null) {
this.source = source; this.source = source;
this.video = video; this.video = video;
this.preview = preview;
} }
get args () { get args () {
@ -13,6 +14,20 @@ class FromFileFFMPEG {
'-q:v', '2', this.video '-q:v', '2', this.video
]; ];
} }
get argsMusic () {
if (!this.preview)
return [
"-i", this.source,
'-q:a', '0', "-map", "a", this.video
];
return [
"-i", this.source, "-i", this.preview,
"-filter:v", "scale='if(gt(iw,ih),720,-1)':'if(gt(ih,iw),720,-1)',pad=720:720:(ow-iw)/2:(oh-ih)/2:black",
"-c:v", "mjpeg", "-q:a", "0",
"-map", "0:a", "-map", "1:v", this.video
];
}
} }
class FFMPEG { class FFMPEG {
@ -21,13 +36,48 @@ class FFMPEG {
this.isBuffer = data instanceof Buffer; this.isBuffer = data instanceof Buffer;
} }
static fromFile (source, video) { static fromFile (source, video, preview = null) {
return new FromFileFFMPEG(source, video); return new FromFileFFMPEG(source, video, preview);
} }
convert(fromFile) { async convertMusic (fromFile) {
return new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const isFromFile = fromFile && (fromFile instanceof FromFileFFMPEG);
if (!isFromFile)
return reject(new Error("Supports only fromFile"));
const ffmpegArgs = !isFromFile ? [] : fromFile.argsMusic;
const ff = spawn("ffmpeg", ffmpegArgs);
console.debug("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;
}
// TODO: Supports only fromFile
});
}
async convert(fromFile) {
return await new Promise((resolve, reject) => {
const isFromFile = fromFile instanceof FromFileFFMPEG; const isFromFile = fromFile instanceof FromFileFFMPEG;
// TODO: Off convert method
console.trace("Deprecated. Off [ffmpeg].convert() on:");
if (isFromFile) {
return resolve(fromFile.source);
}
const returnStream = !this.isBuffer ?
this.data : Readable.from(this.data);
return resolve(returnStream);
const ffmpegArgs = !isFromFile ? [ const ffmpegArgs = !isFromFile ? [
"-i", "-i",
"pipe:0", "pipe:0",
@ -49,7 +99,7 @@ class FFMPEG {
"mp4", "mp4",
"pipe:1", "pipe:1",
] : fromFile.args; ] : fromFile.args;
console.debug("ffmpeg", ...ffmpegArgs); //console.debug("ffmpeg", ...ffmpegArgs);
const ff = spawn("ffmpeg", ffmpegArgs); const ff = spawn("ffmpeg", ffmpegArgs);
let errData = ""; let errData = "";