Merge pull request #2 from vchikalkin/feature/youtube-download
Feature/youtube download
This commit is contained in:
commit
e40b8cd704
24
.vscode/launch.json
vendored
Normal file
24
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"name": "Run Script: dev (Next-Downloader-Bot)",
|
||||
"request": "launch",
|
||||
"command": "npm run dev",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -5,11 +5,12 @@ description =
|
||||
🟢 **Currently supported:**
|
||||
- TikTok
|
||||
- Instagram
|
||||
- YouTube
|
||||
|
||||
⚡ Simply send the video link, and the bot will download it for you.
|
||||
|
||||
short-description =
|
||||
Download TikTok, Instagram videos and images.
|
||||
Download TikTok, Instagram and YouTube videos and images.
|
||||
|
||||
For any questions: @v_dev_support
|
||||
|
||||
@ -18,10 +19,10 @@ start =
|
||||
.description = Start the bot
|
||||
|
||||
|
||||
err-invalid-url = ❌ Invalid URL! Please send a valid TikTok or Instagram link (e.g., https://vt.tiktok.com/ or https://www.instagram.com/p/)
|
||||
err-invalid-url = ❌ Invalid URL! Please send a valid TikTok, Instagram or YouTube link (e.g., https://vt.tiktok.com/ or https://www.instagram.com/p/ or https://www.youtube.com/)
|
||||
err-invalid-download-urls = 🔍 Download links not found. The video might be deleted or unavailable
|
||||
err-generic = ⚠️ Something went wrong. Please try again in a few seconds
|
||||
err-limit-exceeded = 🚫 Too many requests! Please wait
|
||||
|
||||
|
||||
msg-welcome = Welcome! I can download TikTok or Instagram videos and images for you without watermark. Just send me the link (for example: https://vt.tiktok.com/ or https://www.instagram.com/p/)
|
||||
msg-welcome = Welcome! I can download TikTok, Instagram or YouTube videos and images for you without watermark. Just send me the link (for example: https://vt.tiktok.com/ or https://www.instagram.com/p/ or https://www.youtube.com/)
|
||||
@ -5,11 +5,12 @@ description =
|
||||
🟢 **Текущая поддержка:**
|
||||
- TikTok
|
||||
- Instagram
|
||||
- YouTube
|
||||
|
||||
⚡ Просто отправьте ссылку на видео и изображения, и бот сразу скачает его для вас!
|
||||
|
||||
short-description =
|
||||
Скачивай видео и изображения из TikTok и Instagram.
|
||||
Скачивай видео и изображения из TikTok, Instagram и YouTube.
|
||||
|
||||
По всем вопросам: @v_dev_support
|
||||
|
||||
@ -18,10 +19,10 @@ start =
|
||||
.description = Запуск бота
|
||||
|
||||
|
||||
err-invalid-url = ❌ Неверная ссылка! Отправьте корректную ссылку TikTok или Instagram (например: https://vt.tiktok.com/ или https://www.instagram.com/p/)
|
||||
err-invalid-url = ❌ Неверная ссылка! Отправьте корректную ссылку TikTok, Instagram или YouTube (например: https://vt.tiktok.com/, https://www.instagram.com/p/ или https://www.youtube.com/watch?v=)
|
||||
err-invalid-download-urls = 🔍 Не удалось найти ссылки для скачивания. Возможно, видео и изображения удалено или недоступно
|
||||
err-generic = ⚠️ Что-то пошло не так. Попробуйте еще раз через несколько секунд
|
||||
err-limit-exceeded = 🚫 Слишком много запросов! Подождите немного
|
||||
|
||||
|
||||
msg-welcome = Добро пожаловать! Я могу скачать для вас видео и изображения из TikTok или Instagram без водяного знака. Для этого просто отправьте мне ссылку (например: https://vt.tiktok.com/ или https://www.instagram.com/p/)
|
||||
msg-welcome = Добро пожаловать! Я могу скачать для вас видео и изображения из TikTok, Instagram или YouTube без водяного знака. Для этого просто отправьте мне ссылку (например: https://vt.tiktok.com/, https://www.instagram.com/p/ или https://www.youtube.com/watch?v=)
|
||||
@ -31,11 +31,13 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "catalog:",
|
||||
"axios": "^1.12.0",
|
||||
"axios-cookiejar-support": "^6.0.5",
|
||||
"grammy": "^1.37.0",
|
||||
"ioredis": "^5.7.0",
|
||||
"pino": "^9.9.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"radashi": "^12.6.2",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "catalog:",
|
||||
"zod": "catalog:"
|
||||
|
||||
@ -5,9 +5,10 @@ import { TTL_URLS } from '@/config/redis';
|
||||
import { getRedisInstance } from '@/utils/redis';
|
||||
import { getTiktokDownloadUrl } from '@/utils/tiktok';
|
||||
import { getInstagramDownloadUrl } from '@/utils/instagram';
|
||||
import { validateTikTokUrl, validateInstagramUrl } from '@/utils/urls';
|
||||
import { validateTikTokUrl, validateInstagramUrl, validateYoutubeUrl } from '@/utils/urls';
|
||||
import { Composer, InputFile } from 'grammy';
|
||||
import { cluster } from 'radashi';
|
||||
import { getYoutubeDownloadUrl } from '@/utils/youtube';
|
||||
|
||||
const composer = new Composer<Context>();
|
||||
const feature = composer.chatType('private');
|
||||
@ -19,8 +20,9 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
|
||||
|
||||
const isTikTok = validateTikTokUrl(url);
|
||||
const isInstagram = validateInstagramUrl(url);
|
||||
const isYoutube = validateYoutubeUrl(url);
|
||||
|
||||
if (!isTikTok && !isInstagram) {
|
||||
if (!isTikTok && !isInstagram && !isYoutube) {
|
||||
return context.reply(context.t('err-invalid-url'));
|
||||
}
|
||||
|
||||
@ -40,6 +42,9 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
|
||||
const result = await getInstagramDownloadUrl(url);
|
||||
imagesUrls = result.images;
|
||||
videoUrl = result.play;
|
||||
} else if (isYoutube) {
|
||||
const result = await getYoutubeDownloadUrl(url);
|
||||
videoUrl = result.play;
|
||||
}
|
||||
|
||||
if (!videoUrl && !imagesUrls?.length) {
|
||||
|
||||
@ -4,3 +4,5 @@ export const TIKTOK_URL_REGEX =
|
||||
|
||||
export const INSTAGRAM_URL_REGEX =
|
||||
/https?:\/\/(www\.)?instagram\.com\/(p|reel|tv|stories)\/([a-zA-Z0-9_-]+)(\/)?(\?utm_source=ig_web_copy_link&igshid=[a-z0-9]+)?/u;
|
||||
|
||||
export const YOUTUBE_URL_REGEX = /(youtu.*be.*)\/(watch\?v=|embed\/|v|shorts|)(.*?((?=[&#?])|$))/u;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { INSTAGRAM_URL_REGEX, TIKTOK_URL_REGEX } from '@/constants/regex';
|
||||
import { INSTAGRAM_URL_REGEX, TIKTOK_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants/regex';
|
||||
|
||||
export function validateTikTokUrl(url: string) {
|
||||
return TIKTOK_URL_REGEX.test(url);
|
||||
@ -7,3 +7,7 @@ export function validateTikTokUrl(url: string) {
|
||||
export function validateInstagramUrl(url: string) {
|
||||
return INSTAGRAM_URL_REGEX.test(url);
|
||||
}
|
||||
|
||||
export function validateYoutubeUrl(url: string) {
|
||||
return YOUTUBE_URL_REGEX.test(url);
|
||||
}
|
||||
|
||||
98
apps/bot/src/utils/youtube.ts
Normal file
98
apps/bot/src/utils/youtube.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import axios from 'axios';
|
||||
import { wrapper } from 'axios-cookiejar-support';
|
||||
import * as tough from 'tough-cookie';
|
||||
|
||||
const jar = new tough.CookieJar();
|
||||
|
||||
const headers = {
|
||||
accept: '*/*',
|
||||
'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'content-type': 'application/json',
|
||||
dnt: '1',
|
||||
priority: 'u=1, i',
|
||||
'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'sec-gpc': '1',
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
const client = wrapper(
|
||||
axios.create({
|
||||
jar,
|
||||
withCredentials: true,
|
||||
headers,
|
||||
}),
|
||||
);
|
||||
|
||||
export interface Media {
|
||||
type: string;
|
||||
quality: string;
|
||||
extension: string;
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
export interface InfoRoot {
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
medias: Media[];
|
||||
}
|
||||
|
||||
export interface DownloadRoot {
|
||||
status: string;
|
||||
url: string;
|
||||
filename: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const qualityOrder = ['144p', '240p', '360p', '480p', '1080p', '720p'].reverse();
|
||||
|
||||
export async function getYoutubeDownloadUrl(url: string) {
|
||||
// get session cookie
|
||||
await client.get('https://downr.org/.netlify/functions/analytics');
|
||||
|
||||
// fetch video info
|
||||
const { data: infoData } = await client.post<InfoRoot>(
|
||||
'https://downr.org/.netlify/functions/video-info',
|
||||
{
|
||||
url,
|
||||
},
|
||||
);
|
||||
|
||||
if (!infoData?.medias.length) throw new Error('Invalid YouTube response');
|
||||
if (infoData.duration > 120) throw new Error('Video duration exceeds limit');
|
||||
|
||||
let quality: string | undefined;
|
||||
|
||||
for (const q of qualityOrder) {
|
||||
const hasQuality = infoData.medias.find(
|
||||
(media) => media.type === 'video' && media.quality === q && media.extension === 'mp4',
|
||||
);
|
||||
|
||||
if (hasQuality) {
|
||||
quality = q;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!quality) throw new Error('No suitable quality found');
|
||||
|
||||
// fetch download link
|
||||
const { data: downloadData } = await client.post<DownloadRoot>(
|
||||
'https://downr.org/.netlify/functions/youtube-download',
|
||||
{
|
||||
downloadMode: 'video',
|
||||
url,
|
||||
videoQuality: quality,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
play: downloadData.url,
|
||||
};
|
||||
}
|
||||
63
pnpm-lock.yaml
generated
63
pnpm-lock.yaml
generated
@ -90,6 +90,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.0
|
||||
axios-cookiejar-support:
|
||||
specifier: ^6.0.5
|
||||
version: 6.0.5(axios@1.12.0)(tough-cookie@6.0.0)
|
||||
grammy:
|
||||
specifier: ^1.37.0
|
||||
version: 1.37.1
|
||||
@ -105,6 +108,9 @@ importers:
|
||||
radashi:
|
||||
specifier: ^12.6.2
|
||||
version: 12.6.2
|
||||
tough-cookie:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
tsup:
|
||||
specifier: ^8.5.0
|
||||
version: 8.5.0(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1)
|
||||
@ -1084,6 +1090,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@ -1193,6 +1203,13 @@ packages:
|
||||
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios-cookiejar-support@6.0.5:
|
||||
resolution: {integrity: sha512-ldPOQCJWB0ipugkTNVB8QRl/5L2UgfmVNVQtS9en1JQJ1wW588PqAmymnwmmgc12HLDzDtsJ28xE2ppj4rD4ng==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
axios: '>=0.20.0'
|
||||
tough-cookie: '>=4.0.0'
|
||||
|
||||
axios@1.12.0:
|
||||
resolution: {integrity: sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==}
|
||||
|
||||
@ -2203,6 +2220,16 @@ packages:
|
||||
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
|
||||
http-cookie-agent@7.0.3:
|
||||
resolution: {integrity: sha512-EeZo7CGhfqPW6R006rJa4QtZZUpBygDa2HZH3DJqsTzTjyRE6foDBVQIv/pjVsxHC8z2GIdbB1Hvn9SRorP3WQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
tough-cookie: ^4.0.0 || ^5.0.0 || ^6.0.0
|
||||
undici: ^7.0.0
|
||||
peerDependenciesMeta:
|
||||
undici:
|
||||
optional: true
|
||||
|
||||
human-signals@5.0.0:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
@ -3352,10 +3379,21 @@ packages:
|
||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tldts-core@7.0.19:
|
||||
resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==}
|
||||
|
||||
tldts@7.0.19:
|
||||
resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==}
|
||||
hasBin: true
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
@ -4628,6 +4666,8 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@ -4746,6 +4786,14 @@ snapshots:
|
||||
|
||||
axe-core@4.10.3: {}
|
||||
|
||||
axios-cookiejar-support@6.0.5(axios@1.12.0)(tough-cookie@6.0.0):
|
||||
dependencies:
|
||||
axios: 1.12.0
|
||||
http-cookie-agent: 7.0.3(tough-cookie@6.0.0)
|
||||
tough-cookie: 6.0.0
|
||||
transitivePeerDependencies:
|
||||
- undici
|
||||
|
||||
axios@1.12.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
@ -5995,6 +6043,11 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
|
||||
http-cookie-agent@7.0.3(tough-cookie@6.0.0):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
tough-cookie: 6.0.0
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
husky@9.1.7: {}
|
||||
@ -7192,10 +7245,20 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tldts-core@7.0.19: {}
|
||||
|
||||
tldts@7.0.19:
|
||||
dependencies:
|
||||
tldts-core: 7.0.19
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
dependencies:
|
||||
tldts: 7.0.19
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@1.0.1:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user