fast video download + add normal download of audio
This commit is contained in:
parent
9c96e13f71
commit
4805c89cc3
32
src/bot.js
32
src/bot.js
@ -1,7 +1,8 @@
|
||||
const TelegramBot = require("node-telegram-bot-api");
|
||||
const fs = require("fs");
|
||||
const mime = require("mime");
|
||||
const path = require("path");
|
||||
const { DownloadVideo, YtdlUpdater } = require("./download");
|
||||
const { DownloadVideo, YtdlUpdater, downloadFile } = require("./download");
|
||||
const https = require("https");
|
||||
|
||||
async function getImage(url) {
|
||||
@ -24,7 +25,7 @@ async function getImage(url) {
|
||||
const config = {
|
||||
token: process.env.BOT_TOKEN,
|
||||
admins: process.env.ADMIN_LIST?.split(/\s*,\s*/g).map((x) => +x) ?? [],
|
||||
allFormats: process.env.SHOW_ALL_FORMATS === "1"
|
||||
allFormats: false
|
||||
};
|
||||
config.whitelist = [].concat(
|
||||
process.env.WHITELIST?.split(/\s*,\s*/g).map((x) => +x) ?? [],
|
||||
@ -160,12 +161,23 @@ class TgBotSender {
|
||||
} MB)`);
|
||||
}
|
||||
} else if (downloaded.format.type === "audio") {
|
||||
const videoInfo = await video.getVideoInfo();
|
||||
await this.bot.sendAudio(
|
||||
msg.chat.id,
|
||||
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,
|
||||
},
|
||||
);
|
||||
@ -251,7 +263,11 @@ class TgBot {
|
||||
const sent = await this.send(msg, "Wait..");
|
||||
const errHandler = (err) => {
|
||||
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 {
|
||||
@ -271,15 +287,15 @@ class TgBot {
|
||||
for (let quality in qualities) {
|
||||
const qualityItem = qualities[quality];
|
||||
if (qualityItem.type !== null) {
|
||||
if (config.allFormats || ({ mp4: true, m4a: true })[qualityItem.exc]) {
|
||||
if (config.allFormats || ({ mp4: true })[qualityItem.exc]) {
|
||||
if (qualityItem.type === "audio")
|
||||
kbChunkM.push({
|
||||
text: `🎧 ${qualityItem.exc}/${qualityItem.resolution}`,
|
||||
text: qualityItem.strictName ?? `🎧 ${qualityItem.exc}/${qualityItem.resolution}`,
|
||||
callback_data: quality + "_" + msg.message_id,
|
||||
});
|
||||
else if (qualityItem.type === "video")
|
||||
kbChunk.push({
|
||||
text: `📹 ${qualityItem.exc}/${qualityItem.resolution}`,
|
||||
text: qualityItem.strictName ?? `📹 ${qualityItem.exc}/${qualityItem.resolution}`,
|
||||
callback_data: quality + "_" + msg.message_id,
|
||||
});
|
||||
if (kbChunk.length >= 3) {
|
||||
|
||||
@ -3,6 +3,9 @@ const { Readable } = require("stream");
|
||||
const EventEmitter = require("events");
|
||||
const mime = require("mime");
|
||||
const path = require("path");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const { URL } = require("url");
|
||||
const fs = require("fs");
|
||||
const FFMPEG = require("./ffmpeg");
|
||||
|
||||
@ -11,6 +14,48 @@ const infoCache = new Map();
|
||||
|
||||
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 {
|
||||
constructor () {
|
||||
super();
|
||||
@ -62,12 +107,13 @@ class YtdlUpdater extends EventEmitter {
|
||||
class DownloadVideo {
|
||||
static get qualities () {
|
||||
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: [
|
||||
"bestvideo[height<=1080]+bestaudio/best",
|
||||
"--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;
|
||||
|
||||
@ -139,6 +185,7 @@ class DownloadVideo {
|
||||
info.thumbnails?.[info.thumbnails.length - 1]?.url ||
|
||||
info.thumbnail, // лучшее превью
|
||||
url: info.webpage_url,
|
||||
thumbnails: info.thumbnails
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
@ -309,7 +356,7 @@ class DownloadVideo {
|
||||
if (isPath) {
|
||||
child.stdout.on("data", stderrHandler);
|
||||
|
||||
child.on("close", (code) => {
|
||||
child.on("close", async (code) => {
|
||||
//console.debug(">>>>>>>>>>>>>FINISHED");
|
||||
signal.emit("finish");
|
||||
fine = true;
|
||||
@ -319,14 +366,16 @@ class DownloadVideo {
|
||||
}
|
||||
cb({ percent: 100, downloaded, fine: true });
|
||||
|
||||
if (this.quality.type !== "video")
|
||||
if (this.quality.type !== "audio")
|
||||
return resolve(isStream);
|
||||
const ffmpeg = new FFMPEG(isStream);
|
||||
const fileConverted = isStream +
|
||||
".converted." +
|
||||
this.quality.exc;
|
||||
return ffmpeg.convert(FFMPEG.fromFile(
|
||||
isStream, fileConverted
|
||||
//".converted." +
|
||||
".mp3";
|
||||
const videoInfo = await this.getVideoInfo();
|
||||
//console.debug("videoInfo::thumbnails", videoInfo.thumbnails);
|
||||
return ffmpeg.convertMusic(FFMPEG.fromFile(
|
||||
isStream, fileConverted, videoInfo.thumbnail
|
||||
)).then(readyFile => {
|
||||
return resolve(readyFile);
|
||||
}).catch(err => {
|
||||
@ -337,6 +386,7 @@ class DownloadVideo {
|
||||
return;
|
||||
}
|
||||
else if (isStream) {
|
||||
console.warn("May be unstable with stream");
|
||||
const stream = new Readable({ read() {} });
|
||||
const ffmpeg = new FFMPEG(stream);
|
||||
|
||||
@ -374,6 +424,7 @@ class DownloadVideo {
|
||||
});
|
||||
return resolve(resultedStream);
|
||||
}
|
||||
console.warn("May be unstable with Buffer");
|
||||
|
||||
const chunks = [];
|
||||
child.stdout.on("data", (chunk) => {
|
||||
@ -405,4 +456,4 @@ class DownloadVideo {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DownloadVideo, YtdlUpdater };
|
||||
module.exports = { DownloadVideo, YtdlUpdater, downloadFile };
|
||||
|
||||
@ -2,9 +2,10 @@ const { Readable, PassThrough } = require("stream");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
class FromFileFFMPEG {
|
||||
constructor (source, video) {
|
||||
constructor (source, video, preview = null) {
|
||||
this.source = source;
|
||||
this.video = video;
|
||||
this.preview = preview;
|
||||
}
|
||||
|
||||
get args () {
|
||||
@ -13,6 +14,20 @@ class FromFileFFMPEG {
|
||||
'-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 {
|
||||
@ -21,13 +36,48 @@ class FFMPEG {
|
||||
this.isBuffer = data instanceof Buffer;
|
||||
}
|
||||
|
||||
static fromFile (source, video) {
|
||||
return new FromFileFFMPEG(source, video);
|
||||
static fromFile (source, video, preview = null) {
|
||||
return new FromFileFFMPEG(source, video, preview);
|
||||
}
|
||||
|
||||
convert(fromFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
async convertMusic (fromFile) {
|
||||
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;
|
||||
// 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 ? [
|
||||
"-i",
|
||||
"pipe:0",
|
||||
@ -49,7 +99,7 @@ class FFMPEG {
|
||||
"mp4",
|
||||
"pipe:1",
|
||||
] : fromFile.args;
|
||||
console.debug("ffmpeg", ...ffmpegArgs);
|
||||
//console.debug("ffmpeg", ...ffmpegArgs);
|
||||
const ff = spawn("ffmpeg", ffmpegArgs);
|
||||
|
||||
let errData = "";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user