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:
parent
638bc70b78
commit
46712450b2
@ -40,6 +40,7 @@
|
|||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.0",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
"ytdlp-nodejs": "^2.3.5",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { validateTikTokUrl, validateInstagramUrl, validateYoutubeUrl } from '@/u
|
|||||||
import { Composer, InputFile } from 'grammy';
|
import { Composer, InputFile } from 'grammy';
|
||||||
import { cluster } from 'radashi';
|
import { cluster } from 'radashi';
|
||||||
import { getYoutubeDownloadUrl } from '@/utils/youtube';
|
import { getYoutubeDownloadUrl } from '@/utils/youtube';
|
||||||
|
import { getDownloadUrl } from '@/utils/yt-dlp';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
|
|
||||||
const composer = new Composer<Context>();
|
const composer = new Composer<Context>();
|
||||||
const feature = composer.chatType('private');
|
const feature = composer.chatType('private');
|
||||||
@ -22,7 +24,9 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
|
|||||||
const isInstagram = validateInstagramUrl(url);
|
const isInstagram = validateInstagramUrl(url);
|
||||||
const isYoutube = validateYoutubeUrl(url);
|
const isYoutube = validateYoutubeUrl(url);
|
||||||
|
|
||||||
if (!isTikTok && !isInstagram && !isYoutube) {
|
const isServiceSupported = isTikTok || isInstagram || isYoutube;
|
||||||
|
|
||||||
|
if (!isServiceSupported) {
|
||||||
return context.reply(context.t('err-invalid-url'));
|
return context.reply(context.t('err-invalid-url'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +36,10 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let imagesUrls: string[] | undefined;
|
let imagesUrls: string[] | undefined;
|
||||||
let videoUrl: string | undefined;
|
let videoUrl = await getDownloadUrl(url);
|
||||||
|
|
||||||
|
if (!videoUrl) {
|
||||||
|
logger.info(`Failed to get download URL for ${url}, using fallback`);
|
||||||
try {
|
try {
|
||||||
if (isTikTok) {
|
if (isTikTok) {
|
||||||
const result = await getTiktokDownloadUrl(url);
|
const result = await getTiktokDownloadUrl(url);
|
||||||
@ -55,6 +61,7 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
|
|||||||
|
|
||||||
return context.reply(context.t('err-generic'));
|
return context.reply(context.t('err-generic'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!videoUrl && !imagesUrls?.length) {
|
if (!videoUrl && !imagesUrls?.length) {
|
||||||
return context.reply(context.t('err-invalid-download-urls'));
|
return context.reply(context.t('err-invalid-download-urls'));
|
||||||
@ -67,11 +74,7 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
|
|||||||
chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })),
|
chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoUrl) {
|
|
||||||
const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl }));
|
const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl }));
|
||||||
await redis.set(url, video.file_id, 'EX', TTL_URLS);
|
await redis.set(url, video.file_id, 'EX', TTL_URLS);
|
||||||
}
|
}
|
||||||
|
|||||||
16
apps/bot/src/utils/yt-dlp.ts
Normal file
16
apps/bot/src/utils/yt-dlp.ts
Normal 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
10
pnpm-lock.yaml
generated
@ -117,6 +117,9 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
|
ytdlp-nodejs:
|
||||||
|
specifier: ^2.3.5
|
||||||
|
version: 2.3.5
|
||||||
zod:
|
zod:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@ -3682,6 +3685,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
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:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
@ -7596,4 +7604,6 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
ytdlp-nodejs@2.3.5: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user