feat: use yt-dlp as default download tool with fallback (#3)

* refactor: simplify conditions

* feat: use yt-dlp as default download tool with fallback

* feat: use ytdlp-nodejs instead of direct call
This commit is contained in:
Vaagn Avanesyan 2026-01-11 20:22:27 +03:00 committed by GitHub
parent 638bc70b78
commit 46712450b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 26 deletions

View File

@ -40,6 +40,7 @@
"tough-cookie": "^6.0.0",
"tsup": "^8.5.0",
"typescript": "catalog:",
"ytdlp-nodejs": "^2.3.5",
"zod": "catalog:"
},
"devDependencies": {

View File

@ -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<Context>();
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);
}

View File

@ -0,0 +1,16 @@
import { logger } from './logger';
import { YtDlp } from 'ytdlp-nodejs';
export const getDownloadUrl = async (url: string): Promise<string | undefined> => {
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}`);
}
};

10
pnpm-lock.yaml generated
View File

@ -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: {}