bump libs

migrate to typescript 6
remove ytdl feature
This commit is contained in:
2026-05-01 07:05:17 +03:00
parent ac51702f00
commit 13b41c3026
56 changed files with 1069 additions and 1857 deletions
+103 -213
View File
@@ -32,8 +32,6 @@ import {MessageStore} from "../common/message-store";
import {SystemInfo} from "../commands/system-info";
import {PrefixResponse} from "../commands/prefix-response";
import {OllamaChat} from "../commands/ollama-chat";
import {getYouTubeVideoId, getYouTubeVideoInfo, isVideoExists} from "./ytdl";
import {YouTubeDownload} from "../commands/youtube-download";
import {ChatCommand} from "../base/chat-command";
import {WebSearchResponse} from "../model/web-search-response";
import {GeminiChat} from "../commands/gemini-chat";
@@ -47,9 +45,6 @@ import {MistralGetModel} from "../commands/mistral-get-model";
import {OpenAIGetModel} from "../commands/openai-get-model";
import {SendOptions} from "../model/send-options";
import {EditOptions} from "../model/edit-options";
import VideoInfo from "youtubei.js/dist/src/parser/youtube/VideoInfo";
import {DownloadYtVideo} from "../callback_commands/download-yt-video";
import {TryAgain} from "../callback_commands/try-again";
import {StoredUser} from "../model/stored-user";
import {performFFmpeg} from "./ffmpeg";
@@ -68,7 +63,7 @@ export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
}
};
export const logError = (e: Error | TelegramError | string) => {
export const logError = (e: Error | TelegramError | string | unknown) => {
console.error(e);
};
@@ -91,7 +86,7 @@ export const isMessageTooLong = (e: Error | TelegramError) => {
export function searchChatCommand(
commands: Command[],
text: string,
botUsername: string = botUser.username
botUsername: string | undefined = botUser.username
): Command | null {
for (const command of commands) {
const match = command.finalRegexp.exec(text);
@@ -129,7 +124,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
let title: string;
if (isChatCommand) {
title = cmd.title;
title = cmd.title || "";
} else if (isCallbackCommand) {
title = cmd.data;
} else {
@@ -138,7 +133,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
const cbId = cb?.id;
const chatId = msg?.chat?.id || cb?.message?.chat?.id || -1;
const messageId = msg?.message_id || (cb && cb.message && "reply_to_message" in cb.message ? cb.message.reply_to_message.message_id : null) || -1;
const messageId = msg?.message_id || (cb && cb.message && "reply_to_message" in cb.message ? cb.message.reply_to_message?.message_id : null) || -1;
const fromId = msg?.from?.id || cb?.from?.id || -1;
const chatType = msg?.chat?.type || cb?.message?.chat?.type || null;
@@ -162,7 +157,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
await replyToMessage({chat_id: chatId, message_id: messageId, text: text});
} else if (cb) {
await bot.answerCallbackQuery({
callback_query_id: cbId,
callback_query_id: cbId || "",
text: text,
cache_time: 0,
show_alert: true
@@ -182,7 +177,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
return false;
}
if (reqs.isRequiresChat() && msg.chat.type === "private") {
if (reqs.isRequiresChat() && msg?.chat?.type === "private") {
console.log(`${title}: chatId is bad`);
await notifyUser("Тут Вам не чат.");
return false;
@@ -215,13 +210,13 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
}
if (reqs.isRequiresSameUser()) {
let originalFromId: number | null;
let originalFromId: number | undefined;
try {
const originalMessage = await MessageStore.get(chatId, messageId);
originalFromId = originalMessage?.fromId;
} catch (e) {
logError(e);
originalFromId = null;
originalFromId = undefined;
}
if (originalFromId && fromId !== originalFromId && fromId !== Environment.CREATOR_ID) {
@@ -239,7 +234,7 @@ export async function executeChatCommand(cmd: Command | null, msg: Message, text
if (!await checkRequirements(cmd, msg)) return false;
await cmd.execute(msg, cmd.regexp.exec(text));
await cmd.execute(msg, cmd.regexp?.exec(text));
return true;
}
@@ -249,7 +244,7 @@ export async function findAndExecuteCallbackCommand(commands: CallbackCommand[],
const cmd = searchCallbackCommand(commands, data);
if (!cmd) return false;
if (!await checkRequirements(cmd, null, query)) return false;
if (!await checkRequirements(cmd, undefined, query)) return false;
await cmd.execute(query);
await cmd.answerCallbackQuery(query);
@@ -281,7 +276,7 @@ export async function editMessageText(options: EditOptions) {
link_preview_options: options.link_preview_options,
});
return Promise.resolve(message);
} catch (e) {
} catch (e: any) {
logError(e);
if (isMarkupFailed(e)) {
@@ -295,6 +290,8 @@ export async function editMessageText(options: EditOptions) {
return Promise.reject(e);
}
}
return Promise.resolve(false);
}
export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
@@ -337,7 +334,7 @@ export async function replyToMessage(options: SendOptions): Promise<Message> {
text: options.text,
parse_mode: options.parse_mode,
reply_parameters: {
message_id: "message" in options ? options.message.message_id : options.message_id
message_id: <number>("message" in options ? options.message.message_id : options.message_id)
},
link_preview_options: options.link_preview_options
});
@@ -1177,12 +1174,13 @@ export function extractTextMessage(msg: Message | StoredMessage | string): strin
if (!msg) return null;
if (typeof msg === "string") return msg;
const text = (isStoredMessage(msg) ? msg.text : msg.text || msg.caption || "").trim();
if (text.length === 0) return null;
const text = (isStoredMessage(msg) ? msg.text : msg.text || msg.caption || "")?.trim();
if (!text || !text?.length) return null;
return text;
}
export function cutPrefixes(msg: Message | StoredMessage | string): string {
export function cutPrefixes(msg: Message | StoredMessage | string | null): string | null {
if (!msg) return null;
const chatCommands = commands.filter(c => c instanceof ChatCommand);
const prefixes = [Environment.BOT_PREFIX];
@@ -1193,10 +1191,12 @@ export function cutPrefixes(msg: Message | StoredMessage | string): string {
chatCommands.forEach((cmd) => {
const command = cmd.command;
if (Array.isArray(command)) {
command.forEach(pushPrefix);
} else {
pushPrefix(command);
if (command) {
if (Array.isArray(command)) {
command.forEach(pushPrefix);
} else {
pushPrefix(command);
}
}
});
@@ -1216,10 +1216,10 @@ export function cutPrefixes(msg: Message | StoredMessage | string): string {
}
export function isStoredMessage(msg: Message | StoredMessage | null): msg is StoredMessage {
return msg && "id" in msg;
return !!msg && "id" in msg;
}
export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<string[] | null> {
export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<string[] | null | undefined> {
if (isStoredMessage(msg)) {
return msg.photoMaxSizeFilePath;
}
@@ -1237,7 +1237,7 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise<
const maxSize = await mapPhotoSizeToMax(getPhotoMaxSize(msg.photo));
if (maxSize) {
let imageFilePath = path.join(photoDir, maxSize.unique_file_id + ".jpg");
let imageFilePath: string | null = path.join(photoDir, maxSize.unique_file_id + ".jpg");
if (!fs.existsSync(imageFilePath)) {
const res = await axios.get<ArrayBuffer>(maxSize.url, {responseType: "arraybuffer"});
const src = Buffer.from(res.data);
@@ -1268,7 +1268,7 @@ export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise<string[
const promises = sizes.filter(s => !fs.existsSync(photoPathByUniqueId(s.file_unique_id)))
.map(s => mapPhotoSizeToMax(s));
const maxSizes = await Promise.all(promises);
const maxSizes = (await Promise.all(promises)).filter(e => !!e);
const imagePromises = maxSizes.map((size) => {
return axios.get<ArrayBuffer>(size.url, {responseType: "arraybuffer"});
@@ -1287,37 +1287,40 @@ export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise<string[
return null;
}
});
const finalPaths = paths.filter(p => p);
finalPaths.unshift(...existing);
const finalPaths = existing.concat(...paths.filter(p => !!p).map(p => <string>p));
return finalPaths;
}
export async function collectReplyChainText(triggerMsg: Message | StoredMessage, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise<MessagePart[]> {
export async function collectReplyChainText(triggerMsg: Message | StoredMessage | null, limit: number = 40, includeTrigger = true, cutPrefix: boolean = true): Promise<MessagePart[]> {
if (!triggerMsg) return [];
const parts: MessagePart[] = [];
const pushPart = async (msg: Message | StoredMessage, textRequired: boolean = false) => {
const rawText = extractTextMessage(msg);
const cleanText = cutPrefix ? cutPrefixes(rawText) : rawText;
const imageNames = await loadImagesIfExists(msg);
const pushPart = async (msg: Message | StoredMessage | undefined | null, textRequired: boolean = false) => {
if (msg) {
const rawText = extractTextMessage(msg);
const cleanText = cutPrefix ? cutPrefixes(rawText) : rawText;
const imageNames = await loadImagesIfExists(msg);
if (!cleanText && textRequired) return;
if (!cleanText && !imageNames?.length) return;
if (!cleanText && textRequired) return;
if (!cleanText && !imageNames?.length) return;
const fromId = isStoredMessage(msg) ? msg.fromId : msg.from.id;
const firstName = isStoredMessage(msg) ?
(await UserStore.get(msg.fromId))?.firstName : msg.from.first_name;
const fromId = isStoredMessage(msg) ? msg.fromId : msg.from?.id;
const firstName = isStoredMessage(msg) ?
(await UserStore.get(msg.fromId))?.firstName : msg.from?.first_name;
const images = imageNames ? imageNames.map(n => {
const filePath = photoPathByUniqueId(n);
return Buffer.from(fs.readFileSync(filePath)).toString("base64");
}) : null;
const images = imageNames ? imageNames.map(n => {
const filePath = photoPathByUniqueId(n);
return Buffer.from(fs.readFileSync(filePath)).toString("base64");
}) : null;
parts.push({
bot: fromId === botUser.id,
content: cleanText ? cleanText : "",
name: firstName,
images: images ? images : []
});
parts.push({
bot: fromId === botUser.id,
content: cleanText ? cleanText : "",
name: firstName,
images: images ? images : []
});
}
};
const chatId = isStoredMessage(triggerMsg) ? triggerMsg.chatId as number : triggerMsg.chat.id;
@@ -1428,7 +1431,8 @@ export async function waveDistortSharp(
.toBuffer();
}
export async function downloadTelegramFile(filePath: string): Promise<Buffer> {
export async function downloadTelegramFile(filePath?: string | null): Promise<Buffer | null> {
if (!filePath) return null;
const url = `https://api.telegram.org/file/bot${Environment.BOT_TOKEN}/${filePath}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to download file: ${res.status} ${res.statusText}`);
@@ -1606,7 +1610,7 @@ export function startIntervalEditor(params: {
try {
await params.editFn(next);
lastSent = next;
} catch (e) {
} catch (e: any) {
if ((e?.description ?? e?.message ?? "").includes("message is not modified")) return;
logError("edit failed: " + e);
}
@@ -1624,7 +1628,7 @@ export function startIntervalEditor(params: {
};
}
export function boolToInt(bool: boolean): number {
export function boolToInt(bool: boolean | undefined): number {
return bool ? 1 : 0;
}
@@ -1645,7 +1649,7 @@ export function buildExcludedSet<
const entries = Object.keys(cols)
.filter((key) => !excludeSet.has(key))
.map((key) => {
const realName = (cols as unknown)[key].name; // actual DB column name
const realName = (cols as any)[key].name; // actual DB column name
return [key, sql.raw(`excluded.${realName}`)] as const;
});
@@ -1674,7 +1678,7 @@ export function getRuntimeInfo(): RuntimeInfo {
export type PhotoMaxSize = { width: number, height: number, url: string; file_id: string; unique_file_id: string; };
export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environment.MAX_PHOTO_SIZE): PhotoSize | null {
export function getPhotoMaxSize(photos: PhotoSize[] | undefined, target: number = Environment.MAX_PHOTO_SIZE): PhotoSize | null {
if (!photos) return null;
photos = photos.filter(p => Math.max(p.width, p.height) <= target);
@@ -1687,12 +1691,11 @@ export function getPhotoMaxSize(photos: PhotoSize[], target: number = Environmen
return photos.reduce((prev, cur) => {
if (!prev) return cur;
return cur.width * cur.height > prev.width * prev.height ? cur : prev;
}, null);
});
}
export async function mapPhotoSizeToMax(size: PhotoSize): Promise<PhotoMaxSize | null> {
export async function mapPhotoSizeToMax(size: PhotoSize | null): Promise<PhotoMaxSize | null> {
if (!size) return null;
return {
width: size.width,
@@ -1733,7 +1736,7 @@ export function boolToEmoji(bool: boolean | undefined): string {
export const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
async function processAlbum(groupId: string): Promise<string[]> {
async function processAlbum(groupId: string): Promise<string[] | undefined | null> {
const entry = albumCache.get(groupId);
if (!entry) return;
@@ -1741,10 +1744,10 @@ async function processAlbum(groupId: string): Promise<string[]> {
.filter(m => m.photo)
.map(m => m.photo);
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)));
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)).filter(s => !!s));
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
console.log(`Received album ${groupId} with ${ids.length} photos.`);
console.log(`Received album ${groupId} with ${ids?.length} photos.`);
console.log("File IDs:", ids);
albumCache.delete(groupId);
@@ -1755,7 +1758,7 @@ export function photoPathByUniqueId(uniqueId: string): string {
return path.join(photoDir, uniqueId + ".jpg");
}
export function getCurrentModel(): string {
export function getCurrentModel(): string | undefined {
switch (Environment.DEFAULT_AI_PROVIDER) {
case AiProvider.OLLAMA:
return Environment.OLLAMA_MODEL;
@@ -1769,7 +1772,7 @@ export function getCurrentModel(): string {
}
export async function getCurrentModelCapabilities(): Promise<AiModelCapabilities | null> {
let promise: Promise<AiModelCapabilities | null> = null;
let promise: Promise<AiModelCapabilities | null> | null | undefined = null;
switch (Environment.DEFAULT_AI_PROVIDER) {
case AiProvider.OLLAMA: {
const ollamaGetModel = commands.find(c => c instanceof OllamaGetModel);
@@ -1779,13 +1782,14 @@ export async function getCurrentModelCapabilities(): Promise<AiModelCapabilities
promise = new Promise(async (resolve, reject) => {
try {
const defaultModelCapabilities = await ollamaGetModel.getModelCapabilities();
const imageModelCapabilities = await ollamaGetModel.loadImageModelInfo();
const result = {
vision: (await ollamaGetModel.loadImageModelInfo()).vision,
ocr: null,
thinking: (await ollamaGetModel.loadThinkModelInfo()).thinking,
tools: defaultModelCapabilities.tools,
audio: defaultModelCapabilities.audio
vision: imageModelCapabilities?.vision,
ocr: imageModelCapabilities?.ocr,
thinking: (await ollamaGetModel.loadThinkModelInfo())?.thinking,
tools: defaultModelCapabilities?.tools,
audio: defaultModelCapabilities?.audio
};
resolve(result);
} catch (e) {
@@ -1824,6 +1828,7 @@ export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
export async function processNewMessage(msg: Message): Promise<void> {
console.log("New Message", msg);
if (!msg.from) return;
const envFile: string = fs.readFileSync(".env").toString();
const env = new Map(
@@ -2002,14 +2007,16 @@ export async function processNewMessage(msg: Message): Promise<void> {
const photos = await processAlbum(groupId);
console.log("processedAlbum", photos);
storedMsg.photoMaxSizeFilePath = photos;
await MessageStore.put(storedMsg).catch(logError);
if (storedMsg) {
storedMsg.photoMaxSizeFilePath = photos;
await MessageStore.put(storedMsg).catch(logError);
}
resolve(true);
}, 1000)
});
} else {
const entry = albumCache.get(groupId);
entry.messages.push(msg);
entry?.messages?.push(msg);
}
});
}
@@ -2044,9 +2051,7 @@ export async function processNewMessage(msg: Message): Promise<void> {
const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText;
if (Environment.PROCESS_LINKS && await processYouTubeLink(msg, getFirstLink(msg))) return;
if (msg.chat.type !== "private" && (!msg.reply_to_message || msg.reply_to_message.from.id !== botUser.id) && !startsWithPrefix && !msg.voice) return;
if (msg.chat.type !== "private" && (!msg.reply_to_message || msg.reply_to_message.from?.id !== botUser.id) && !startsWithPrefix && !msg.voice) return;
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
@@ -2060,167 +2065,52 @@ export async function processNewMessage(msg: Message): Promise<void> {
const input = path.join(Environment.DATA_PATH, "input.ogg");
const output = path.join(Environment.DATA_PATH, "output.wav")
try {
fs.writeFileSync(input, fileBuffer);
await performFFmpeg(() =>
ffmpeg(input)
.toFormat("wav")
.save(output)
.on("progress", (progress) => {
console.log("progress", progress);
})
);
if (fileBuffer) {
try {
fs.writeFileSync(input, fileBuffer);
await performFFmpeg(() =>
ffmpeg(input)
.toFormat("wav")
.save(output)
.on("progress", (progress) => {
console.log("progress", progress);
})
);
fileBuffer = fs.readFileSync(output);
voiceB64 = fileBuffer.toString("base64");
fs.rmSync(input);
fs.rmSync(output);
} catch (e) {
logError(e);
fileBuffer = fs.readFileSync(output);
voiceB64 = fileBuffer.toString("base64");
fs.rmSync(input);
fs.rmSync(output);
} catch (e) {
logError(e);
}
}
}
switch (Environment.DEFAULT_AI_PROVIDER) {
case AiProvider.OLLAMA: {
await commands.find(e => e instanceof OllamaChat).executeOllama(msg, textToCheck, false, voiceB64);
await commands.find(e => e instanceof OllamaChat)?.executeOllama(msg, textToCheck, false, voiceB64);
break;
}
case AiProvider.GEMINI: {
await commands.find(e => e instanceof GeminiChat).executeGemini(msg, textToCheck);
await commands.find(e => e instanceof GeminiChat)?.executeGemini(msg, textToCheck);
break;
}
case AiProvider.MISTRAL: {
await commands.find(e => e instanceof MistralChat).executeMistral(msg, textToCheck);
await commands.find(e => e instanceof MistralChat)?.executeMistral(msg, textToCheck);
break;
}
case AiProvider.OPENAI: {
await commands.find(e => e instanceof OpenAIChat).executeOpenAI(msg, textToCheck);
await commands.find(e => e instanceof OpenAIChat)?.executeOpenAI(msg, textToCheck);
break;
}
}
}
function getFirstLink(msg: Message): string | null {
if (msg.entities) {
const urlEntities = msg.entities.filter(e => e.type === "url");
if (urlEntities.length) {
const e = urlEntities[0];
return msg.text.substring(e.offset, e.offset + e.length);
}
}
return null;
}
export async function processYouTubeLink(msg: Message, url?: string, id?: string): Promise<boolean> {
if (!url && !id) return false;
let waitMessage: Message | null = msg.from.id === botUser.id ? msg : null;
let videoId: string | null = null;
try {
try {
videoId = id || getYouTubeVideoId(url);
} catch (e) {
logError(e);
return false;
}
const yt = commands.find(e => e instanceof YouTubeDownload);
if (await checkRequirements(yt, msg)) {
if (!waitMessage) {
waitMessage = await replyToMessage({
message: msg,
text: "⏳ Ищу информацию о видео..."
});
} else {
await editMessageText({message: msg, text: "⏳ Ищу информацию о видео..."});
}
let videoInfo: VideoInfo | null = null;
let ytError: string = null;
try {
videoInfo = await getYouTubeVideoInfo(videoId);
} catch (e) {
logError(e);
if ("version" in e) {
ytError = e.message;
}
}
console.log("VIDEO_INFO", videoInfo);
let text: string = null;
const inCache = isVideoExists({videoId: videoId});
const duration = videoInfo?.basic_info?.duration || null;
const canDownload = inCache || duration && duration <= 300;
if (videoInfo) {
text = "Видео с YouTube\n\n" +
`Название: ${videoInfo.basic_info?.title}\n` +
`Автор: ${videoInfo.secondary_info?.owner?.author?.name}\n` +
`Длительность: ${duration} сек.`;
if (!canDownload) {
text += `\n\nВидео слишком длинное (${duration} сек. > 300 сек.)`;
}
} else if (!ytError) {
text = "Информация о видео не найдена";
}
const errorButInCache = !videoInfo && ytError && inCache;
if (errorButInCache) {
text = "Я не смог получить информацию о видео, но нашёл его в кэше.";
}
if (!text && ytError) {
await editMessageText({
message: waitMessage,
text: Environment.errorText,
reply_markup: {
inline_keyboard: [[
TryAgain.withData("/ytinfo " + videoId).asButton()
]]
}
});
} else {
await editMessageText({
message: waitMessage,
text: text,
reply_markup: canDownload ? {
inline_keyboard: [[
DownloadYtVideo.withData(inCache, "/ytdl " + videoId).asButton()
]]
} : {inline_keyboard: []}
});
}
}
return true;
} catch (e) {
logError(e);
await editMessageText({
message: waitMessage,
text: Environment.errorText,
reply_markup: {
inline_keyboard: [[
TryAgain.withData("/ytinfo " + videoId).asButton()
]]
}
});
}
return false;
}
export async function processEditedMessage(msg: Message): Promise<void> {
console.log("Edited Message", msg);
if (!msg.from) return;
await UserStore.put(msg.from);
-169
View File
@@ -1,169 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import {videoDir, videoTempDir} from "../index";
import ffmpeg from "fluent-ffmpeg";
import Innertube, {Platform, Types} from "youtubei.js";
import {Readable} from "node:stream";
import {logError} from "./utils";
import {performFFmpeg} from "./ffmpeg";
import VideoInfo from "youtubei.js/dist/src/parser/youtube/VideoInfo";
let innertube: Innertube | null = null;
export async function getYT(): Promise<Innertube> {
if (innertube) {
return innertube;
} else {
innertube = await Innertube.create({
generate_session_locally: true,
retrieve_player: true
});
return innertube;
}
}
export function getYouTubeVideoId(url: string): string {
const regex = /(?:(?:youtube\.com|music\.youtube\.com)\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts|clip)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
const match = url.match(regex);
if (!match || !match[1]) throw new Error("Invalid YouTube or Shorts URL");
return match[1];
}
export async function getYouTubeVideoInfo(videoId: string): Promise<VideoInfo> {
try {
return (await getYT()).getInfo(videoId, {client: "ANDROID"});
} catch (e) {
logError(e);
}
}
export function isVideoExists(options: DownloadOptions): boolean {
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
const filePath = path.join(videoDir, `${videoId}.mp4`);
return fs.existsSync(filePath);
}
export function getVideoFromCache(videoId: string): Buffer | null {
if (!isVideoExists({videoId: videoId})) return null;
const filePath = path.join(videoDir, `${videoId}.mp4`);
return Buffer.from(fs.readFileSync(filePath));
}
export type DownloadOptions = {
url: string
} | {
videoId: string;
}
export async function downloadVideoFromYouTube(options: DownloadOptions): Promise<{
time: number,
exists?: boolean,
buffer: Buffer | null
}> {
const start = Date.now();
let buffer: Buffer | null = null;
try {
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
const filePath = path.join(videoDir, `${videoId}.mp4`);
if (fs.existsSync(filePath)) {
const buffer = Buffer.from(fs.readFileSync(filePath));
return {
time: Date.now() - start,
exists: true,
buffer: buffer
};
}
Platform.shim.eval = async (data: Types.BuildScriptResult, env: Record<string, Types.VMPrimative>) => {
const properties = [];
if (env.n) properties.push(`n: exportedVars.nFunction("${env.n}")`);
if (env.sig) properties.push(`sig: exportedVars.sigFunction("${env.sig}")`);
const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
return new Function(code)();
};
const yt = await getYT();
const videoInfo = await yt.getInfo(videoId, {client: "ANDROID"});
console.log("Video info", videoInfo);
console.log(`Fetching metadata for: ${videoId}...`);
const targetQuality = "360p";
const videoFormat = videoInfo.streaming_data?.formats.find(f => f.quality_label.startsWith(targetQuality))
|| videoInfo.streaming_data?.adaptive_formats.find(f => f.quality_label.startsWith(targetQuality));
const audioFormat = videoInfo.chooseFormat({type: "audio", quality: "best", language: "original"});
console.log("Video format: ", videoFormat);
console.log("Audio Format: ", audioFormat);
if (!videoFormat) {
console.log(`Quality ${targetQuality} not found. Falling back to best available.`);
}
const videoWebStream = await videoInfo.download({
itag: videoFormat.itag,
client: "ANDROID"
});
const audioWebStream = await videoInfo.download({
itag: audioFormat.itag,
client: "ANDROID"
});
const videoStream = Readable.fromWeb(videoWebStream as any);
const audioStream = Readable.fromWeb(audioWebStream as any);
const videoPath = path.join(videoTempDir, `temp_video_${videoId}.mp4`);
const audioPath = path.join(videoTempDir, `temp_audio_${videoId}.mp4`);
const writeStream = (stream: any, path: string) =>
new Promise((resolve, reject) => {
const file = fs.createWriteStream(path);
stream.pipe(file);
file.on("finish", resolve);
file.on("error", reject);
});
await Promise.all([
writeStream(videoStream, videoPath),
writeStream(audioStream, audioPath)
]);
await performFFmpeg(() =>
ffmpeg()
.input(videoPath)
.input(audioPath)
.videoCodec("copy")
.audioCodec("copy")
.save(filePath)
.on("progress", (progress) => {
console.log("progress", progress);
})
).catch(logError);
fs.unlinkSync(videoPath);
fs.unlinkSync(audioPath);
buffer = fs.readFileSync(filePath);
console.log(`✅ Saved to ${videoId}.mp4`);
} catch (error) {
console.error("❌ Download failed:", error instanceof Error ? error.message : error);
throw error;
}
const end = Date.now();
const diff = end - start;
console.log(`Video downloaded.\ntook ${diff}ms`);
return {
time: diff,
buffer: buffer,
};
}