Compare commits
10 Commits
abe724029c
...
a3c27eeda6
| Author | SHA1 | Date | |
|---|---|---|---|
| a3c27eeda6 | |||
| d995b7e1ab | |||
| 2b4da355f8 | |||
| 2c01fe389d | |||
| 3e400cea39 | |||
| 852c0e2803 | |||
| a47c8bb83a | |||
| 13d3607a54 | |||
| 14f4753b27 | |||
| 84fbc475fd |
18
LICENSE
18
LICENSE
@ -686,3 +686,21 @@ Attach the following notice to the start of each source file:
|
|||||||
Make sure to provide a way for users interacting with your program
|
Make sure to provide a way for users interacting with your program
|
||||||
remotely (through a network) to obtain the source code, for example
|
remotely (through a network) to obtain the source code, for example
|
||||||
by providing a link to a repository or downloadable archive.
|
by providing a link to a repository or downloadable archive.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Dual License Notice:
|
||||||
|
|
||||||
|
This software is available under the following two licenses:
|
||||||
|
|
||||||
|
1. GNU Affero General Public License version 3 (AGPLv3 or later)
|
||||||
|
- This license allows you to use, modify, and distribute the software freely
|
||||||
|
under the terms of AGPLv3. You must provide source code to users interacting
|
||||||
|
with your software remotely, as required by AGPLv3.
|
||||||
|
|
||||||
|
2. Commercial License
|
||||||
|
- For commercial use (for example, integrating this software into
|
||||||
|
paid services or monetizing it in any way), you must obtain a separate
|
||||||
|
commercial license from the copyright holder.
|
||||||
|
|
||||||
|
For inquiries regarding commercial licensing, contact: fullgream@i2pmail.org
|
||||||
|
|||||||
17
README.md
17
README.md
@ -1,6 +1,22 @@
|
|||||||
# telegram_ytdlp_node.js
|
# telegram_ytdlp_node.js
|
||||||
|
|
||||||
|
## ⚠️ WARNING!!!
|
||||||
|
|
||||||
|
The **telegram_ytdlp_node.js** bot project will no longer be actively maintained in its open-source version. This means that new commits and bug fixes in the public repository are **unlikely**, although not completely impossible.
|
||||||
|
|
||||||
|
In the future, a link to the running bot and the project roadmap will be shared. **The source code will no longer be publicly available**.
|
||||||
|
|
||||||
|
Thank you to everyone who used and supported the project in its open-source form. Your feedback and contributions have been greatly appreciated.
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
This bot can download music and videos from youtube.
|
This bot can download music and videos from youtube.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This software is available under AGPLv3 for open-source use.
|
||||||
|
For commercial use, please contact fullgream@i2pmail.org for a commercial license.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Step 1
|
### Step 1
|
||||||
@ -24,6 +40,7 @@ API_ID=<If you use MTProto API. Type here API_ID from Telegram API Development T
|
|||||||
API_HASH=<If you use MTProto API. Type here API_HASH from Telegram API Development Tools>
|
API_HASH=<If you use MTProto API. Type here API_HASH from Telegram API Development Tools>
|
||||||
API_SESSION=<If you use MTProto API. Type here API_SESSION from Session get script (Step 2)>
|
API_SESSION=<If you use MTProto API. Type here API_SESSION from Session get script (Step 2)>
|
||||||
CHAT_ID=<If you use MTProto API. ID of chat. You can send new message and chat and use command: curl -s "https://api.telegram.org/bot$BOT_TOKEN/getUpdates" (REPLACE '$BOT_TOKEN' to Your Bot token!!!)>
|
CHAT_ID=<If you use MTProto API. ID of chat. You can send new message and chat and use command: curl -s "https://api.telegram.org/bot$BOT_TOKEN/getUpdates" (REPLACE '$BOT_TOKEN' to Your Bot token!!!)>
|
||||||
|
EMPTY_IMAGE_URL=<URL to empty preview image (for default using: https://placekitten.com/500/350)>
|
||||||
```
|
```
|
||||||
4) Build docker container: `docker build -t ytdlp_node:release .`
|
4) Build docker container: `docker build -t ytdlp_node:release .`
|
||||||
5) Up docker container: `docker run -d --name ytdlp_node --restart unless-stopped --env-file .env -v "$(pwd)/cookies.txt:/app/cookies.txt" ytdlp_node:release`
|
5) Up docker container: `docker run -d --name ytdlp_node --restart unless-stopped --env-file .env -v "$(pwd)/cookies.txt:/app/cookies.txt" ytdlp_node:release`
|
||||||
|
|||||||
15
src/bot.js
15
src/bot.js
@ -31,7 +31,7 @@ function removeTemp () {
|
|||||||
}
|
}
|
||||||
removeTemp();
|
removeTemp();
|
||||||
|
|
||||||
const ytLinkRegexp = /^https:\/\/www\.youtube\.com/m;
|
const ytLinkRegexp = /^https:\/\/(www\.|m\.|)youtu(be\.com|\.be)/m;
|
||||||
|
|
||||||
async function getFileSize(pathname) {
|
async function getFileSize(pathname) {
|
||||||
const stream = fs.createReadStream(pathname);
|
const stream = fs.createReadStream(pathname);
|
||||||
@ -78,7 +78,9 @@ class TgBotSender {
|
|||||||
async sendVideo (chatId, data, settings = {}, apiOptions = {}) {
|
async sendVideo (chatId, data, settings = {}, apiOptions = {}) {
|
||||||
if (this.parent.mtproto !== null) {
|
if (this.parent.mtproto !== null) {
|
||||||
const mtprotoMID = await this.parent.mtproto.saveMedia(
|
const mtprotoMID = await this.parent.mtproto.saveMedia(
|
||||||
data, settings.caption ?? ""
|
data, settings.caption ?? "", {
|
||||||
|
thumb: settings.thumb,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return await this.bot.copyMessage(
|
return await this.bot.copyMessage(
|
||||||
@ -157,6 +159,7 @@ class TgBotSender {
|
|||||||
.then(async (downloaded) => {
|
.then(async (downloaded) => {
|
||||||
//console.debug("downloaded:", downloaded);
|
//console.debug("downloaded:", downloaded);
|
||||||
try {
|
try {
|
||||||
|
const videoInfo = await video.getVideoInfo();
|
||||||
if (downloaded.format.type === "video") {
|
if (downloaded.format.type === "video") {
|
||||||
const filesize = (typeof isStream !== "string" || this.parent.mtproto !== null) ?
|
const filesize = (typeof isStream !== "string" || this.parent.mtproto !== null) ?
|
||||||
0 : await getFileSize(downloaded.data);
|
0 : await getFileSize(downloaded.data);
|
||||||
@ -170,7 +173,10 @@ class TgBotSender {
|
|||||||
await this.sendVideo(
|
await this.sendVideo(
|
||||||
msg.chat.id,
|
msg.chat.id,
|
||||||
downloaded.data,
|
downloaded.data,
|
||||||
{ caption: info.title },
|
{
|
||||||
|
caption: info.title,
|
||||||
|
thumb: await downloadFile(videoInfo.thumbnail),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
filename,
|
filename,
|
||||||
contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime,
|
contentType: isStream === true ? "application/octet-stream" : downloaded.format.mime,
|
||||||
@ -193,7 +199,6 @@ class TgBotSender {
|
|||||||
} MB)`);
|
} MB)`);
|
||||||
}
|
}
|
||||||
} else if (downloaded.format.type === "audio") {
|
} else if (downloaded.format.type === "audio") {
|
||||||
const videoInfo = await video.getVideoInfo();
|
|
||||||
await this.sendAudio(
|
await this.sendAudio(
|
||||||
msg.chat.id,
|
msg.chat.id,
|
||||||
downloaded.data,
|
downloaded.data,
|
||||||
@ -420,7 +425,7 @@ class TgBot {
|
|||||||
{
|
{
|
||||||
type: "photo",
|
type: "photo",
|
||||||
//media: info.thumbnail,
|
//media: info.thumbnail,
|
||||||
media: "https://placekitten.com/500/350",
|
media: config.emptyImage,
|
||||||
caption: info.title,
|
caption: info.title,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const config = {
|
|||||||
session: process.env.API_SESSION,
|
session: process.env.API_SESSION,
|
||||||
chatId: +process.env.CHAT_ID,
|
chatId: +process.env.CHAT_ID,
|
||||||
},
|
},
|
||||||
|
emptyImage: process.env.EMPTY_IMAGE_URL ?? "https://placekitten.com/500/350",
|
||||||
};
|
};
|
||||||
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) ?? [],
|
||||||
|
|||||||
101
src/download.js
101
src/download.js
@ -3,59 +3,16 @@ 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 { URL } = require("url");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const FFMPEG = require("./ffmpeg");
|
const FFMPEG = require("./ffmpeg");
|
||||||
|
const config = require("./config");
|
||||||
|
const { downloadFile, verifyThumbUrl } = require("./utils");
|
||||||
|
|
||||||
const formatsCache = new Map();
|
const formatsCache = new Map();
|
||||||
const infoCache = new Map();
|
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();
|
||||||
@ -108,9 +65,8 @@ class DownloadVideo {
|
|||||||
static get qualities () {
|
static get qualities () {
|
||||||
const q = {
|
const q = {
|
||||||
webm_best: { exc: "webm", flag: "best", mime: "video/webm", type: "video" },
|
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: [
|
mp4_best: { exc: "mp4", flag: [
|
||||||
"bestvideo[height<=1080]+bestaudio/best",
|
"best[ext=mp4]",
|
||||||
"--merge-output-format", "mp4"
|
"--merge-output-format", "mp4"
|
||||||
], mime: "video/mp4", type: "video", strictName: "📹 Best video" },
|
], mime: "video/mp4", type: "video", strictName: "📹 Best video" },
|
||||||
audio_default: { exc: "mp4", flag: "worst", mime: "audio/mpeg", type: "audio", strictName: "🎧 Audio" },
|
audio_default: { exc: "mp4", flag: "worst", mime: "audio/mpeg", type: "audio", strictName: "🎧 Audio" },
|
||||||
@ -173,23 +129,30 @@ class DownloadVideo {
|
|||||||
console.error("yt-dlp error:", err.toString());
|
console.error("yt-dlp error:", err.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", code => {
|
child.on("close", async code => {
|
||||||
if (code !== 0) return reject(new Error("yt-dlp failed to get info"));
|
if (code !== 0) return reject(new Error("yt-dlp failed to get info"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = JSON.parse(data);
|
const info = JSON.parse(data);
|
||||||
|
|
||||||
|
const thumbnails =
|
||||||
|
(await Promise.all(info.thumbnails.map(async thumbnail => {
|
||||||
|
const verified = await verifyThumbUrl(thumbnail.url);
|
||||||
|
if (!verified) return null;
|
||||||
|
return thumbnail;
|
||||||
|
}))).filter(x => x !== null);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
id: info.id,
|
id: info.id,
|
||||||
title: info.title,
|
title: info.title,
|
||||||
description: info.description,
|
description: info.description,
|
||||||
uploader: info.uploader,
|
uploader: info.uploader,
|
||||||
duration: info.duration, // секунды
|
duration: info.duration,
|
||||||
thumbnail:
|
thumbnail:
|
||||||
info.thumbnails?.[info.thumbnails.length - 1]?.url ||
|
(thumbnails?.[thumbnails.length - 1]?.url ||
|
||||||
info.thumbnail, // лучшее превью
|
info.thumbnail) || config.emptyImage,
|
||||||
url: info.webpage_url,
|
url: info.webpage_url,
|
||||||
thumbnails: info.thumbnails
|
thumbnails
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
@ -240,33 +203,19 @@ class DownloadVideo {
|
|||||||
const formats = await this.getFormats();
|
const formats = await this.getFormats();
|
||||||
const q = {};
|
const q = {};
|
||||||
|
|
||||||
// Перебираем форматы и группируем по расширению
|
|
||||||
formats.forEach(f => {
|
formats.forEach(f => {
|
||||||
if (!f.ext || !f.format_id) return;
|
if (!f.ext || !f.format_id) return;
|
||||||
//if (!q[f.ext]) q[f.ext] = [];
|
|
||||||
|
|
||||||
const resolution = f.height ? `${f.height}p` : 'audio-only';
|
const resolution = f.height ? `${f.height}p` : 'audio-only';
|
||||||
|
|
||||||
let flag = `bestaudio[ext=${f.ext}]`;
|
let flag = `bestaudio[ext=${f.ext}]`;
|
||||||
if (f.height) {
|
if (f.height) {
|
||||||
//flag = `best[height<=${f.height}][ext=${f.ext}]`;
|
|
||||||
flag = [
|
flag = [
|
||||||
`best[height<=${
|
`best[height<=${
|
||||||
f.height
|
f.height
|
||||||
}]`,
|
}]`,
|
||||||
/*`bestvideo[height<=${
|
|
||||||
f.height
|
|
||||||
}]+bestaudio[ext=m4a]/best`,*/
|
|
||||||
/*`bestvideo[height<=${
|
|
||||||
f.height
|
|
||||||
}][vcodec^=avc1]+bestaudio[ext=m4a]/best[ext=${f.ext}]`,*/
|
|
||||||
/*`bestvideo[height<=${
|
|
||||||
f.height
|
|
||||||
}][vcodec^=mov]+bestaudio[ext=m4a]/best`,*/
|
|
||||||
"--merge-output-format", f.ext
|
"--merge-output-format", f.ext
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
//const flag = f.format_id;
|
|
||||||
|
|
||||||
const mimeType = mime.getType(f.ext);
|
const mimeType = mime.getType(f.ext);
|
||||||
const type = (resolution === "audio-only" || /^audio\//m.test(mimeType)) ? "audio" : (
|
const type = (resolution === "audio-only" || /^audio\//m.test(mimeType)) ? "audio" : (
|
||||||
@ -274,8 +223,8 @@ class DownloadVideo {
|
|||||||
);
|
);
|
||||||
q[f.ext + "_" + resolution] = {
|
q[f.ext + "_" + resolution] = {
|
||||||
format_id: f.format_id,
|
format_id: f.format_id,
|
||||||
flag, // <- ключевой момент для yt-dlp
|
flag,
|
||||||
exc: f.ext, // <- расширение
|
exc: f.ext,
|
||||||
resolution,
|
resolution,
|
||||||
filesize: f.filesize || null,
|
filesize: f.filesize || null,
|
||||||
mime: mimeType, type: !mimeType ? null : type,
|
mime: mimeType, type: !mimeType ? null : type,
|
||||||
@ -325,22 +274,16 @@ class DownloadVideo {
|
|||||||
const params = [
|
const params = [
|
||||||
"--cookies", "cookies.txt",
|
"--cookies", "cookies.txt",
|
||||||
"--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
"--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||||
//"--extractor-args", "youtube:player-client=default,mweb;po_token=mweb.gvs+XXX",
|
|
||||||
"--extractor-args", "youtube:player_client=default,mweb",
|
"--extractor-args", "youtube:player_client=default,mweb",
|
||||||
"-f", ...flags,
|
"-f", ...flags,
|
||||||
"-o", !isPath ? "-" : isStream, this.url,
|
"-o", !isPath ? "-" : isStream, this.url,
|
||||||
];
|
];
|
||||||
d("yt-dlp " + params.join(" "));
|
|
||||||
//console.debug({ HOME: process.env.HOME });
|
|
||||||
const child = spawn("yt-dlp", params, {
|
const child = spawn("yt-dlp", params, {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
//HOME: process.env.HOME,
|
|
||||||
LANG: "en_US.UTF-8",
|
LANG: "en_US.UTF-8",
|
||||||
},
|
},
|
||||||
stdio: ["pipe","pipe","pipe"],
|
stdio: ["pipe","pipe","pipe"],
|
||||||
//stdio: ["pipe","inherit","pipe"],
|
|
||||||
//stdio: ["inherit", "inherit", "inherit"],
|
|
||||||
});
|
});
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let percent = 0;
|
let percent = 0;
|
||||||
@ -350,8 +293,6 @@ class DownloadVideo {
|
|||||||
|
|
||||||
const stderrHandler = (data) => {
|
const stderrHandler = (data) => {
|
||||||
const str = data.toString();
|
const str = data.toString();
|
||||||
//console.debug("DEBUG >>>>>>", str);
|
|
||||||
//console.debug(str);
|
|
||||||
const match = str.match(/(\d+\.\d+)%/);
|
const match = str.match(/(\d+\.\d+)%/);
|
||||||
if (match && cb) {
|
if (match && cb) {
|
||||||
percent = parseFloat(match[1]);
|
percent = parseFloat(match[1]);
|
||||||
@ -365,7 +306,6 @@ class DownloadVideo {
|
|||||||
child.stdout.on("data", stderrHandler);
|
child.stdout.on("data", stderrHandler);
|
||||||
|
|
||||||
child.on("close", async (code) => {
|
child.on("close", async (code) => {
|
||||||
//console.debug(">>>>>>>>>>>>>FINISHED");
|
|
||||||
signal.emit("finish");
|
signal.emit("finish");
|
||||||
fine = true;
|
fine = true;
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
@ -378,10 +318,8 @@ class DownloadVideo {
|
|||||||
return resolve(isStream);
|
return resolve(isStream);
|
||||||
const ffmpeg = new FFMPEG(isStream);
|
const ffmpeg = new FFMPEG(isStream);
|
||||||
const fileConverted = isStream +
|
const fileConverted = isStream +
|
||||||
//".converted." +
|
|
||||||
".mp3";
|
".mp3";
|
||||||
const videoInfo = await this.getVideoInfo();
|
const videoInfo = await this.getVideoInfo();
|
||||||
//console.debug("videoInfo::thumbnails", videoInfo.thumbnails);
|
|
||||||
return ffmpeg.convertMusic(FFMPEG.fromFile(
|
return ffmpeg.convertMusic(FFMPEG.fromFile(
|
||||||
isStream, fileConverted, videoInfo.thumbnail
|
isStream, fileConverted, videoInfo.thumbnail
|
||||||
)).then(readyFile => {
|
)).then(readyFile => {
|
||||||
@ -416,11 +354,8 @@ class DownloadVideo {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", reject);
|
child.on("error", reject);
|
||||||
//return resolve(stream);
|
|
||||||
//console.debug(this.quality);
|
|
||||||
const resultedStream = this.quality.type === "video" ?
|
const resultedStream = this.quality.type === "video" ?
|
||||||
(await ffmpeg.convert()) : stream;
|
(await ffmpeg.convert()) : stream;
|
||||||
//const resultedStream = stream;
|
|
||||||
|
|
||||||
resultedStream.on("data", chunk => {
|
resultedStream.on("data", chunk => {
|
||||||
sizeTg += chunk.length;
|
sizeTg += chunk.length;
|
||||||
|
|||||||
@ -65,74 +65,6 @@ class FFMPEG {
|
|||||||
// TODO: Supports only fromFile
|
// 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",
|
|
||||||
"-c:v",
|
|
||||||
"libx264",
|
|
||||||
"-profile:v",
|
|
||||||
"high",
|
|
||||||
"-level",
|
|
||||||
"4.1",
|
|
||||||
"-c:a",
|
|
||||||
"aac",
|
|
||||||
"-crf",
|
|
||||||
"26",
|
|
||||||
"-preset",
|
|
||||||
"medium",
|
|
||||||
"-movflags",
|
|
||||||
"+frag_keyframe+empty_moov+default_base_moof",
|
|
||||||
"-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);
|
|
||||||
|
|
||||||
const output = new PassThrough();
|
|
||||||
ff.stdout.pipe(output);
|
|
||||||
|
|
||||||
ff.on("close", (code) => {
|
|
||||||
if (code !== 0) {
|
|
||||||
reject(new Error(`FFMPEG exited with code ${code}\n${errData}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ff.on("error", reject);
|
|
||||||
|
|
||||||
resolve(output);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FFMPEG;
|
module.exports = FFMPEG;
|
||||||
|
|||||||
@ -32,7 +32,6 @@ class MTProtoUser {
|
|||||||
writeStream.on("error", reject);
|
writeStream.on("error", reject);
|
||||||
writeStream.on("finish", resolve);
|
writeStream.on("finish", resolve);
|
||||||
});
|
});
|
||||||
//fs.writeFileSync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = await this.client.sendFile(config.mtproto.chatId, {
|
const msg = await this.client.sendFile(config.mtproto.chatId, {
|
||||||
@ -41,6 +40,14 @@ class MTProtoUser {
|
|||||||
thumb: options.thumb,
|
thumb: options.thumb,
|
||||||
//filename: options.filename,
|
//filename: options.filename,
|
||||||
supportsStreaming: true,
|
supportsStreaming: true,
|
||||||
|
attributes: [
|
||||||
|
new Api.DocumentAttributeVideo({
|
||||||
|
duration: options.duration || 0,
|
||||||
|
w: options.width || 1280,
|
||||||
|
h: options.height || 720,
|
||||||
|
supportsStreaming: true
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
return msg.id;
|
return msg.id;
|
||||||
|
|||||||
@ -24,5 +24,5 @@
|
|||||||
"bot"
|
"bot"
|
||||||
],
|
],
|
||||||
"author": "FullGreaM",
|
"author": "FullGreaM",
|
||||||
"license": "AGPL-3.0-or-later"
|
"license": "AGPL-3.0-or-later OR Commercial"
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/utils.js
Normal file
55
src/utils.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const http = require("http");
|
||||||
|
const https = require("https");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
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();
|
||||||
|
resolve(savePath);
|
||||||
|
});
|
||||||
|
}).on("error", (err) => {
|
||||||
|
fs.unlinkSync(savePath);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyThumbUrl (fileUrl) {
|
||||||
|
try {
|
||||||
|
if (/\.webp$/m.test(fileUrl)) return false;
|
||||||
|
await downloadFile(fileUrl);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { downloadFile, verifyThumbUrl };
|
||||||
Loading…
Reference in New Issue
Block a user