diff --git a/apps/bot/package.json b/apps/bot/package.json index 09855b2..9689ced 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -40,6 +40,7 @@ "tough-cookie": "^6.0.0", "tsup": "^8.5.0", "typescript": "catalog:", + "ytdlp-nodejs": "^2.3.5", "zod": "catalog:" }, "devDependencies": { diff --git a/apps/bot/src/bot/features/download.ts b/apps/bot/src/bot/features/download.ts index fb1be43..15ecc7c 100644 --- a/apps/bot/src/bot/features/download.ts +++ b/apps/bot/src/bot/features/download.ts @@ -9,6 +9,8 @@ import { validateTikTokUrl, validateInstagramUrl, validateYoutubeUrl } from '@/u import { Composer, InputFile } from 'grammy'; import { cluster } from 'radashi'; import { getYoutubeDownloadUrl } from '@/utils/youtube'; +import { getDownloadUrl } from '@/utils/yt-dlp'; +import { logger } from '@/utils/logger'; const composer = new Composer(); const feature = composer.chatType('private'); @@ -22,7 +24,9 @@ feature.on('message:text', logHandle('download-message'), async (context) => { const isInstagram = validateInstagramUrl(url); const isYoutube = validateYoutubeUrl(url); - if (!isTikTok && !isInstagram && !isYoutube) { + const isServiceSupported = isTikTok || isInstagram || isYoutube; + + if (!isServiceSupported) { return context.reply(context.t('err-invalid-url')); } @@ -32,28 +36,31 @@ feature.on('message:text', logHandle('download-message'), async (context) => { } let imagesUrls: string[] | undefined; - let videoUrl: string | undefined; + let videoUrl = await getDownloadUrl(url); - try { - if (isTikTok) { - const result = await getTiktokDownloadUrl(url); - imagesUrls = result.images; - videoUrl = result.play; - } else if (isInstagram) { - const result = await getInstagramDownloadUrl(url); - imagesUrls = result.images; - videoUrl = result.play; - } else if (isYoutube) { - const result = await getYoutubeDownloadUrl(url); - videoUrl = result.play; - } - } catch (err: any) { - const message = err?.message ?? String(err); - if (typeof message === 'string' && message.startsWith('err-')) { - return context.reply(context.t(message)); - } + if (!videoUrl) { + logger.info(`Failed to get download URL for ${url}, using fallback`); + try { + if (isTikTok) { + const result = await getTiktokDownloadUrl(url); + imagesUrls = result.images; + videoUrl = result.play; + } else if (isInstagram) { + const result = await getInstagramDownloadUrl(url); + imagesUrls = result.images; + videoUrl = result.play; + } else if (isYoutube) { + const result = await getYoutubeDownloadUrl(url); + videoUrl = result.play; + } + } catch (err: any) { + const message = err?.message ?? String(err); + if (typeof message === 'string' && message.startsWith('err-')) { + return context.reply(context.t(message)); + } - return context.reply(context.t('err-generic')); + return context.reply(context.t('err-generic')); + } } if (!videoUrl && !imagesUrls?.length) { @@ -67,11 +74,7 @@ feature.on('message:text', logHandle('download-message'), async (context) => { chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })), ); } - - return; - } - - if (videoUrl) { + } else { const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl })); await redis.set(url, video.file_id, 'EX', TTL_URLS); } diff --git a/apps/bot/src/utils/yt-dlp.ts b/apps/bot/src/utils/yt-dlp.ts new file mode 100644 index 0000000..a2bb6a0 --- /dev/null +++ b/apps/bot/src/utils/yt-dlp.ts @@ -0,0 +1,16 @@ +import { logger } from './logger'; +import { YtDlp } from 'ytdlp-nodejs'; + +export const getDownloadUrl = async (url: string): Promise => { + try { + const ytdlp = new YtDlp(); + const [downloadUrl] = await ytdlp.getUrlsAsync(url, { format: 'b' }); + return downloadUrl; + } catch (error: any) { + if (error.code === 'ENOENT') { + throw new Error('yt-dlp not found. Please install yt-dlp.'); + } + + logger.error(`Failed to get download URL for ${url}: ${error.message}`); + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e0390c..15cf1a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: typescript: specifier: 'catalog:' version: 5.9.2 + ytdlp-nodejs: + specifier: ^2.3.5 + version: 2.3.5 zod: specifier: 'catalog:' version: 3.25.76 @@ -3682,6 +3685,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + ytdlp-nodejs@2.3.5: + resolution: {integrity: sha512-7V08DRv8C1K0HxJFvRoaoLYFS/reJ9VJBlaMVhEvdi2IsYK/9Hae1Mah65Y+bhk3RAmx7G9eTfpOhkj3bp0Zbw==} + engines: {node: '>=16.0.0'} + hasBin: true + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -7596,4 +7604,6 @@ snapshots: yocto-queue@0.1.0: {} + ytdlp-nodejs@2.3.5: {} + zod@3.25.76: {}