From e59c43650134c37e9835dd7dc8d08673eba6559b Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sat, 31 Jan 2026 19:36:11 +0300 Subject: [PATCH] feat: separate Ollama text/image/think models + add YouTube downloader Support OLLAMA_IMAGE_MODEL and new OLLAMA_THINK_MODEL (both default to OLLAMA_MODEL) Add /ollamathink command; validate thinking capability; disable image-analysis flow in think mode Make /ollama-get-model show Text/Image/Think blocks only when models differ Add /ytdl (/youtube) command + auto-detect YouTube URLs in messages Cache downloaded videos to data/video and schedule daily cleanup Move photo storage from data/temp to data/photo; improve env boolean parsing via ifTrue Update deps (youtubei.js, puppeteer*) and TS config (allowJs, allowSyntheticDefaultImports) --- package.json | 8 ++- src/commands/mistral-chat.ts | 1 + src/commands/ollama-chat.ts | 33 +++++++++--- src/commands/ollama-get-model.ts | 48 +++++++++++++++-- src/commands/youtube-download.ts | 54 +++++++++++++++++++ src/common/environment.ts | 13 +++-- src/index.ts | 37 ++++++++++++- src/model/ollama-request.ts | 1 + src/util/files.ts | 41 ++++++++++++++ src/util/utils.ts | 67 +++++++++++++++++------ src/util/ytdl.ts | 93 ++++++++++++++++++++++++++++++++ tsconfig.json | 4 +- 12 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 src/commands/youtube-download.ts create mode 100644 src/util/files.ts create mode 100644 src/util/ytdl.ts diff --git a/package.json b/package.json index e810573..4ac3193 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,18 @@ "drizzle-orm": "^1.0.0-beta.12-5845444", "emoji-regex": "^10.6.0", "ollama": "^0.6.3", + "puppeteer": "^24.36.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "qrcode": "^1.5.4", "sharp": "^0.34.5", "systeminformation": "^5.30.6", "twemoji": "^14.0.2", - "typescript-telegram-bot-api": "^0.11.0" + "typescript-telegram-bot-api": "^0.11.0", + "youtubei.js": "^16.0.1" }, "devDependencies": { - "@types/bun": "^1.3.7", + "@types/bun": "^1.3.8", "@types/node": "^25.1.0", "@types/qrcode": "^1.5.6", "@typescript-eslint/eslint-plugin": "^8.54.0", diff --git a/src/commands/mistral-chat.ts b/src/commands/mistral-chat.ts index 6566ba7..39acae7 100644 --- a/src/commands/mistral-chat.ts +++ b/src/commands/mistral-chat.ts @@ -81,6 +81,7 @@ export class MistralChat extends ChatCommand { const stream = await mistralAi.chat.stream({ model: Environment.MISTRAL_MODEL, + // eslint-disable-next-line @typescript-eslint/no-explicit-any messages: chatMessages as any }); diff --git a/src/commands/ollama-chat.ts b/src/commands/ollama-chat.ts index b91e447..c2b6915 100644 --- a/src/commands/ollama-chat.ts +++ b/src/commands/ollama-chat.ts @@ -16,7 +16,7 @@ import {OllamaCancel} from "../callback_commands/ollama-cancel"; import {OllamaGetModel} from "./ollama-get-model"; export class OllamaChat extends ChatCommand { - command = "ollama"; + command = ["ollama", "ollamathink"]; argsMode = "required" as const; title = "/ollama"; @@ -24,10 +24,10 @@ export class OllamaChat extends ChatCommand { async execute(msg: Message, match?: RegExpExecArray | null): Promise { console.log("match", match); - return this.executeOllama(msg, match?.[3]); + return this.executeOllama(msg, match?.[3], match?.[1]?.toLowerCase()?.startsWith("ollamathink")); } - async executeOllama(msg: Message, text: string): Promise { + async executeOllama(msg: Message, text: string, think: boolean = false): Promise { if (!text || text.trim().length === 0) return; const chatId = msg.chat.id; @@ -55,7 +55,7 @@ export class OllamaChat extends ChatCommand { return total + (curr.images?.length ?? 0); }, 0); - if (imagesCount) { + if (!think && imagesCount) { try { const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadImageModelInfo(); if (modelInfo) { @@ -73,20 +73,38 @@ export class OllamaChat extends ChatCommand { } } + if (think) { + try { + const modelInfo = await chatCommands.find(c => c instanceof OllamaGetModel).loadThinkModelInfo(); + if (modelInfo) { + const caps = modelInfo.capabilities || []; + if (!caps.includes("thinking")) { + await replyToMessage({ + message: msg, + text: "Моя текущая модель не умеет размышлять 🥹" + }); + return; + } + } + } catch (e) { + logError(e); + } + } + const uuid = crypto.randomUUID(); const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]}; waitMessage = await replyToMessage({ message: msg, - text: imagesCount ? + text: (!think && imagesCount) ? imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText : Environment.waitText }); const stream = await ollama.chat({ - model: imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL, + model: think ? Environment.OLLAMA_THINK_MODEL : imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL, stream: true, - think: false, + think: think, messages: chatMessages, }); @@ -153,6 +171,7 @@ export class OllamaChat extends ChatCommand { message_id: waitMessage.message_id, text: "🤔 Размышляю...", parse_mode: "Markdown", + reply_markup: cancelMarkup }).catch(logError); } diff --git a/src/commands/ollama-get-model.ts b/src/commands/ollama-get-model.ts index 7518ac7..414cbbd 100644 --- a/src/commands/ollama-get-model.ts +++ b/src/commands/ollama-get-model.ts @@ -11,14 +11,48 @@ export class OllamaGetModel extends ChatCommand { async execute(msg: Message): Promise { try { - let modelInfo = await this.loadModelInfo(); - const modelText = "```Text\n" + this.getModelText(Environment.OLLAMA_MODEL, modelInfo) + "```"; - modelInfo = await this.loadImageModelInfo(); - const imageModelText = "```Image\n" + this.getModelText(Environment.OLLAMA_IMAGE_MODEL, modelInfo) + "```"; + const model = Environment.OLLAMA_MODEL; + const imageModel = Environment.OLLAMA_IMAGE_MODEL; + const thinkModel = Environment.OLLAMA_THINK_MODEL; + + const promises: (Promise | null)[] = [this.loadModelInfo()]; + + if (imageModel && imageModel !== model) { + promises.push(this.loadImageModelInfo()); + } else { + promises.push(null); + } + + if (thinkModel && thinkModel !== model) { + promises.push(this.loadThinkModelInfo()); + } else { + promises.push(null); + } + + const infos = await Promise.all(promises); + + let modelInfo = infos[0]; + const modelText = "```Text\n" + this.getModelText(model, modelInfo) + "```"; + + modelInfo = infos[1]; + const imageModelText = modelInfo ? + "```Image\n" + this.getModelText(imageModel, modelInfo) + "```" : null; + + modelInfo = infos[2]; + const thinkModelText = modelInfo ? + "```Think\n" + this.getModelText(thinkModel, modelInfo) + "```" : null; + + const modelInfos = [modelText]; + if (imageModelText) { + modelInfos.push(imageModelText); + } + if (thinkModelText) { + modelInfos.push(thinkModelText); + } await replyToMessage({ message: msg, - text: modelText + "\n\n" + imageModelText, + text: modelInfos.join("\n\n"), parse_mode: "Markdown" }).catch(logError); @@ -44,4 +78,8 @@ export class OllamaGetModel extends ChatCommand { async loadImageModelInfo(): Promise { return ollama.show({model: Environment.OLLAMA_IMAGE_MODEL}); } + + async loadThinkModelInfo(): Promise { + return ollama.show({model: Environment.OLLAMA_THINK_MODEL}); + } } \ No newline at end of file diff --git a/src/commands/youtube-download.ts b/src/commands/youtube-download.ts new file mode 100644 index 0000000..576f070 --- /dev/null +++ b/src/commands/youtube-download.ts @@ -0,0 +1,54 @@ +import {ChatCommand} from "../base/chat-command"; +import {Message} from "typescript-telegram-bot-api"; +import {logError, replyToMessage} from "../util/utils"; +import {bot} from "../index"; +import {downloadVideoFromYouTube} from "../util/ytdl"; + +export class YouTubeDownload extends ChatCommand { + command = ["ytdl", "youtube"]; + argsMode = "required" as const; + + async execute(msg: Message, match?: RegExpExecArray): Promise { + const url = match?.[3]; + return this.downloadYouTubeVideo(msg, url); + } + + async downloadYouTubeVideo(msg: Message, url: string): Promise { + let waitMessage: Message | null = null; + + try { + waitMessage = await replyToMessage({message: msg, text: "⏳ Секунду..."}); + + const {time, exists, buffer} = await downloadVideoFromYouTube(url); + if (buffer) { + const start = Date.now(); + waitMessage = await bot.editMessageMedia({ + chat_id: msg.chat.id, + message_id: waitMessage.message_id, + media: { + type: "video", + media: buffer + } + }) as Message; + + const diff = Date.now() - start; + waitMessage = await bot.editMessageCaption({ + chat_id: msg.chat.id, + message_id: waitMessage.message_id, + caption: `✅ [Видео](${url})` + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс", + parse_mode: "MarkdownV2" + }) as Message; + } + } catch (e) { + logError(e); + + if (waitMessage && "text" in waitMessage) { + await bot.editMessageText({ + chat_id: msg.chat.id, + message_id: waitMessage.message_id, + text: `⚠️ Произошла ошибка.\n${e}`, + }); + } + } + } +} \ No newline at end of file diff --git a/src/common/environment.ts b/src/common/environment.ts index 2f715f3..86e8038 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -1,6 +1,7 @@ import path from "node:path"; import {saveData} from "../db/database"; import {Answers} from "../model/answers"; +import {ifTrue} from "../util/utils"; export class Environment { static BOT_TOKEN: string; @@ -28,6 +29,7 @@ export class Environment { static OLLAMA_ADDRESS?: string; static OLLAMA_MODEL?: string; static OLLAMA_IMAGE_MODEL?: string; + static OLLAMA_THINK_MODEL?: string; static OLLAMA_API_KEY?: string; static GEMINI_API_KEY?: string; @@ -45,17 +47,17 @@ export class Environment { static load() { Environment.BOT_TOKEN = process.env.BOT_TOKEN; - Environment.TEST_ENVIRONMENT = process.env.TEST_ENVIRONMENT === "true"; + Environment.TEST_ENVIRONMENT = ifTrue(process.env.TEST_ENVIRONMENT); Environment.CHAT_IDS_WHITELIST = new Set(process.env.CHAT_IDS_WHITELIST?.split(",")?.map(e => parseInt(e.trim(), 10)) || []); Environment.BOT_PREFIX = process.env.BOT_PREFIX || ""; Environment.CREATOR_ID = parseInt(process.env.CREATOR_ID || ""); - Environment.IS_DOCKER = process.env.IS_DOCKER == "true"; + Environment.IS_DOCKER = ifTrue(process.env.IS_DOCKER); Environment.DATA_PATH = Environment.IS_DOCKER ? "/" + path.join("config", "data") : "data"; Environment.DB_PATH = "file:" + path.join(Environment.DATA_PATH, Environment.DB_FILE_NAME); - Environment.ONLY_FOR_CREATOR_MODE = process.env.ONLY_FOR_CREATOR_MODE == "true"; + Environment.ONLY_FOR_CREATOR_MODE = ifTrue(process.env.ONLY_FOR_CREATOR_MODE); - Environment.USE_NAMES_IN_PROMPT = process.env.USE_NAMES_IN_PROMPT == "true"; + Environment.USE_NAMES_IN_PROMPT = ifTrue(process.env.USE_NAMES_IN_PROMPT); Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280"); @@ -63,7 +65,8 @@ export class Environment { Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS; Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL || "gemma3:4b"; - Environment.OLLAMA_IMAGE_MODEL = process.env.OLLAMA_IMAGE_MODEL || "gemma3:4b"; + Environment.OLLAMA_IMAGE_MODEL = process.env.OLLAMA_IMAGE_MODEL || Environment.OLLAMA_MODEL; + Environment.OLLAMA_THINK_MODEL = process.env.OLLAMA_THINK_MODEL || Environment.OLLAMA_MODEL; Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY; Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY; diff --git a/src/index.ts b/src/index.ts index 4a1f717..ac58b40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,10 @@ import {Environment} from "./common/environment"; import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api"; import {ChatCommand} from "./base/chat-command"; import { + delay, extractTextMessage, findAndExecuteCallbackCommand, + ignore, initSystemSpecs, logError, processNewMessage @@ -66,6 +68,11 @@ import {GeminiGetModel} from "./commands/gemini-get-model"; import {GeminiSetModel} from "./commands/gemini-set-model"; import {Debug} from "./commands/debug"; import {GeminiGenerateImage} from "./commands/gemini-generate-image"; +import {YouTubeDownload} from "./commands/youtube-download"; +import fs from "node:fs"; +import path from "node:path"; +import {setInterval} from "node:timers"; +import {clearUpVideoFolder} from "./util/files"; process.setUncaughtExceptionCaptureCallback(logError); @@ -148,6 +155,8 @@ export const chatCommands: ChatCommand[] = [ new Shutdown(), new Leave(), + + new YouTubeDownload() ]; export const callbackCommands: CallbackCommand[] = [ @@ -187,7 +196,12 @@ if (Environment.MISTRAL_API_KEY) { ); } +export const photoDir = path.join(Environment.DATA_PATH, "photo"); +export const videoDir = path.join(Environment.DATA_PATH, "video"); + async function main() { + const start = Date.now(); + console.log( `TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` + `DATA_PATH: ${Environment.DATA_PATH}\n` + @@ -195,6 +209,25 @@ async function main() { `ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}` ); + fs.mkdir(photoDir, ignore); + fs.mkdir(videoDir, ignore); + + const now = new Date(); + + const midnight = new Date(); + midnight.setHours(0, 0, 0, 0); + midnight.setDate(now.getDate() + 1); + + const diff = midnight.getTime() - now.getTime(); + console.log("Clearing up videos will be started in " + diff + "ms"); + + delay(diff).then(() => { + setInterval(() => { + console.log("Started clearing up videos"); + clearUpVideoFolder(); + }, 1000 * 60 * 60 * 24); + }); + const commands = chatCommands.filter(cmd => { return cmd.title && cmd.title.startsWith("/") && cmd.title.split(" ").length === 1 && cmd.description; }).map(cmd => { @@ -216,7 +249,9 @@ async function main() { await UserStore.put(botUser); await bot.startPolling(); - console.log("Bot started!"); + const end = Date.now(); + const diff = Math.abs(end - start); + console.log(`Bot started in ${diff}ms!`); } catch (error) { logError(error); } diff --git a/src/model/ollama-request.ts b/src/model/ollama-request.ts index 279d920..458aeee 100644 --- a/src/model/ollama-request.ts +++ b/src/model/ollama-request.ts @@ -1,5 +1,6 @@ export type OllamaRequest = { uuid: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any stream: any; done: boolean; fromId: number; diff --git a/src/util/files.ts b/src/util/files.ts new file mode 100644 index 0000000..a52bdc5 --- /dev/null +++ b/src/util/files.ts @@ -0,0 +1,41 @@ +import {logError} from "./utils"; +import fs from "node:fs"; +import {videoDir} from "../index"; +import path from "node:path"; + +export function clearUpVideoFolder() { + fs.readdir(videoDir, (err, files) => { + if (err) { + logError(err); + return; + } + + const filenamesToDelete: string[] = []; + + files.forEach((filename, index) => { + fs.stat(path.join(videoDir, filename), (err, stats) => { + if (err) { + logError(err); + } else { + const then = stats.mtime.getTime() / 1000; + const now = Date.now() / 1000; + const diff = Math.abs(now - then); + const moreThanOneDay = diff >= 60 * 60 * 24; + if (moreThanOneDay) { + filenamesToDelete.push(filename); + } + + if (index === files.length - 1) { + console.log("filenamesToDelete", filenamesToDelete); + if (filenamesToDelete.length) { + filenamesToDelete.forEach((filename) => { + const fullPath = path.join(videoDir, filename); + fs.rm(fullPath, logError); + }); + } + } + } + }); + }); + }); +} \ No newline at end of file diff --git a/src/util/utils.ts b/src/util/utils.ts index bd80731..c462c6b 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -27,6 +27,8 @@ 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} from "./ytdl"; +import {YouTubeDownload} from "../commands/youtube-download"; export const ignore = () => { }; @@ -469,10 +471,14 @@ export function extractTextMessage(msg: Message | StoredMessage | string): strin export function cutPrefixes(msg: Message | StoredMessage | string): string { const prefixes = [ Environment.BOT_PREFIX, + `/ollamathink@${botUser.username}`, + "/ollamathink", + `/ollama@${botUser.username}`, + "/ollama", `/gemini@${botUser.username}`, "/gemini", `/mistral@${botUser.username}`, - "/mistral" + "/mistral", ]; const text = extractTextMessage(msg); @@ -510,7 +516,7 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise< const maxSize = await mapPhotoSizeToMax(getPhotoMaxSize(msg.photo)); if (maxSize) { - const imagePath = path.join(Environment.DATA_PATH, "temp"); + const imagePath = path.join(Environment.DATA_PATH, "photo"); if (!fs.existsSync(imagePath)) { fs.mkdirSync(imagePath); } @@ -539,7 +545,7 @@ export async function loadImagesIfExists(msg: Message | StoredMessage): Promise< export async function loadImagesFromFileIds(sizes: PhotoSize[]): Promise { if (!sizes?.length) return null; - const dataPath = path.join(Environment.DATA_PATH, "temp"); + const dataPath = path.join(Environment.DATA_PATH, "photo"); if (!fs.existsSync(dataPath)) { fs.mkdirSync(dataPath); } @@ -988,20 +994,25 @@ export async function mapPhotoSizeToMax(size: PhotoSize): Promise { - return new Promise((resolve, reject) => { - fs.readFile(filePath, (err, data) => { - if (err) { - return reject(err); - } - const base64Image = Buffer.from(data).toString("base64"); - const dataUrl = `data:image/jpeg;base64,${base64Image}`; - resolve(dataUrl); - }); - }); +export async function imageToBase64(filePath: string, withMimeType: boolean = false): Promise { + if (!fs.existsSync(filePath)) return null; + + try { + const file = fs.readFileSync(filePath); + const base64 = Buffer.from(file).toString("base64"); + if (withMimeType) { + return `data:image/jpeg;base64,${base64}`; + } + + return base64; + } catch (e) { + logError(e); + return null; + } } -export function ifTrue(exp?: never): boolean { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function ifTrue(exp?: any): boolean { if (!exp) return false; return ["true", "t", "y", 1, "1"].includes(exp); @@ -1093,12 +1104,34 @@ export async function processNewMessage(msg: Message) { return; } + const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText; + if (msg.entities) { + const urlEntities = msg.entities.filter(e => e.type === "url"); + if (urlEntities.length) { + for (const e of urlEntities) { + const url = msg.text.substring(e.offset, e.offset + e.length); + // TODO: 31/01/2026, Danil Nikolaev: implement proper checking + try { + getYouTubeVideoId(url); + + const yt = chatCommands.find(e => e instanceof YouTubeDownload); + if (await checkRequirements(yt, msg)) { + await yt.downloadYouTubeVideo(msg, url); + } + return; + } catch (e) { + logError(e); + } + } + } + } + if (!startsWithPrefix && msg.chat.type !== "private") return; if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return; const chat = chatCommands.find(e => e instanceof OllamaChat); if (await checkRequirements(chat, msg)) { - await chat.executeOllama(msg, startsWithPrefix ? messageWithoutPrefix : cmdText); + await chat.executeOllama(msg, textToCheck); } } @@ -1121,5 +1154,5 @@ async function processAlbum(groupId: string): Promise { } export function photoPathByUniqueId(uniqueId: string): string { - return path.join(Environment.DATA_PATH, "temp", uniqueId + ".jpg"); + return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg"); } \ No newline at end of file diff --git a/src/util/ytdl.ts b/src/util/ytdl.ts new file mode 100644 index 0000000..2fb516e --- /dev/null +++ b/src/util/ytdl.ts @@ -0,0 +1,93 @@ +import Innertube, {Platform, Types, Utils} from "youtubei.js"; +import fs, {createWriteStream} from "node:fs"; +import path from "node:path"; +import {Environment} from "../common/environment"; + +export function getYouTubeVideoId(url: string): string { + const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts)\/|.*[?&]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 downloadVideoFromYouTube(url: string, targetQuality: string = "720p"): Promise<{ + time: number, + exists?: boolean, + buffer: Buffer | null +}> { + const start = Date.now(); + let buffer: Buffer | null = null; + + try { + const videoId = getYouTubeVideoId(url); + const videoFolder = path.join(Environment.DATA_PATH, "video"); + if (!fs.existsSync(videoFolder)) { + fs.mkdirSync(videoFolder); + } + + const filePath = path.join(videoFolder, `${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) => { + 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 Innertube.create({ + generate_session_locally: true, + retrieve_player: true + }); + + const info = await yt.getInfo(videoId); + + console.log(`Fetching metadata for: ${videoId}...`); + + const format = info.streaming_data?.formats.find(f => f.quality_label === targetQuality) + || info.streaming_data?.adaptive_formats.find(f => f.quality_label === targetQuality); + + if (!format) { + console.log(`Quality ${targetQuality} not found. Falling back to best available.`); + } + + const stream = await yt.download(videoId, { + type: "video+audio", + quality: "best", + format: "mp4" + }); + + const file = createWriteStream(filePath); + + console.log("Downloading..."); + + for await (const chunk of Utils.streamToIterable(stream)) { + file.write(chunk); + } + + file.end(); + + 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. URL: ${url}\ntook ${diff}ms`); + + return { + time: diff, + buffer: buffer, + }; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 5e4e2fb..4347ee4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,9 @@ "resolveJsonModule": true, "esModuleInterop": true, "emitDecoratorMetadata": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "allowJs": true }, "include": [ "src"