diff --git a/apps/bot/Dockerfile b/apps/bot/Dockerfile index bf7bb2a..cfad2aa 100644 --- a/apps/bot/Dockerfile +++ b/apps/bot/Dockerfile @@ -64,7 +64,8 @@ RUN apk add --no-cache \ pixman \ pangomm \ libjpeg-turbo \ - freetype + freetype \ + yt-dlp RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 botuser diff --git a/apps/bot/src/config/env.ts b/apps/bot/src/config/env.ts index 1f6033f..ef13c3d 100644 --- a/apps/bot/src/config/env.ts +++ b/apps/bot/src/config/env.ts @@ -18,6 +18,7 @@ export const envSchema = z.object({ .transform((value) => Number.parseInt(value, 10)) .default('6379'), TELEGRAM_API_ROOT: z.string(), + YTDLP_PATH: z.string().optional(), }); export const env = envSchema.parse(process.env); diff --git a/apps/bot/src/utils/youtube.ts b/apps/bot/src/utils/youtube.ts deleted file mode 100644 index 27d0cfc..0000000 --- a/apps/bot/src/utils/youtube.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { getClient } from './client'; -import { MAX_VIDEO_DURATION_SECONDS } from '@/constants/limits'; - -export type DownloadRoot = { - duration: number; - filename: string; - status: string; - url: string; -}; - -export type InfoRoot = { - duration: number; - medias: Media[]; - thumbnail: string; - title: string; -}; - -export type Media = { - extension: string; - fileSize: number; - quality: string; - type: string; -}; - -const qualityOrder = ['144p', '240p', '360p', '480p', '1080p', '720p'].reverse(); - -export async function getYoutubeDownloadUrl(url: string) { - const client = await getClient(); - // fetch info - const { data: infoData } = await client.post( - 'https://downr.org/.netlify/functions/video-info', - { - url, - }, - ); - - if (!infoData?.medias.length) throw new Error('err-invalid-youtube-response'); - if (infoData.duration > MAX_VIDEO_DURATION_SECONDS) - throw new Error('err-youtube-duration-exceeded'); - - 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('err-youtube-no-quality'); - - // fetch download link - const { data: downloadData } = await client.post( - 'https://downr.org/.netlify/functions/youtube-download', - { - downloadMode: 'video', - url, - videoQuality: quality, - }, - ); - - return { - play: downloadData.url, - }; -} diff --git a/apps/bot/src/utils/youtube/api.ts b/apps/bot/src/utils/youtube/api.ts new file mode 100644 index 0000000..771673f --- /dev/null +++ b/apps/bot/src/utils/youtube/api.ts @@ -0,0 +1,40 @@ +import axios from 'axios'; + +type DlsrvResponse = { + duration: number; + filename: string; + status: string; + url: string; +}; + +export async function getYoutubeDownload(url: string) { + const videoId = getYouTubeVideoId(url); + + const { data } = await axios.post('https://embed.dlsrv.online/api/download/mp4', { + format: 'mp4', + quality: '720', + videoId, + }); + + return data.url; +} + +export function getYouTubeVideoId(link: string) { + const url = new URL(link); + + // 1. shorts + if (url.pathname.startsWith('/shorts/')) { + return url.pathname.split('/')[2] || null; + } + + // 2. обычное видео: watch?v= + const vParam = url.searchParams.get('v'); + if (vParam) return vParam; + + // 3. короткая ссылка youtu.be + if (url.hostname.includes('youtu.be')) { + return url.pathname.slice(1) || null; + } + + return null; +} diff --git a/apps/bot/src/utils/youtube/index.ts b/apps/bot/src/utils/youtube/index.ts new file mode 100644 index 0000000..a8e2819 --- /dev/null +++ b/apps/bot/src/utils/youtube/index.ts @@ -0,0 +1,21 @@ +import { getInfo, ytDlpGetUrl } from '../yt-dlp'; +import { getYoutubeDownload } from './api'; +import { MAX_VIDEO_DURATION_SECONDS } from '@/constants/limits'; + +export async function getYoutubeDownloadUrl(url: string) { + const infoData = await getInfo(url); + + if (!infoData) throw new Error('err-invalid-youtube-response'); + + if (infoData.duration > MAX_VIDEO_DURATION_SECONDS) + throw new Error('err-youtube-duration-exceeded'); + + let play: string | undefined; + try { + play = await getYoutubeDownload(url); + } catch { + play = await ytDlpGetUrl(url); + } + + return { play, title: infoData.title }; +} diff --git a/apps/bot/src/utils/yt-dlp/get-download-url.ts b/apps/bot/src/utils/yt-dlp/get-download-url.ts new file mode 100644 index 0000000..2431fa0 --- /dev/null +++ b/apps/bot/src/utils/yt-dlp/get-download-url.ts @@ -0,0 +1,22 @@ +import { runYtDlp } from './yt-dlp'; + +export async function ytDlpGetUrl(url: string): Promise { + const output = await runYtDlp([ + '-f', + // '298+ba/136+ba/22+ba/247+251/best[height<=720]+ba/bv+ba', + 'best[height<=720]+ba/bv+ba', + '-g', + '--no-playlist', + '--no-warnings', + url, + ]); + + // yt-dlp может вернуть несколько строк — берём первую + const directUrl = output.split('\n')[0]; + + if (!directUrl?.startsWith('http')) { + throw new Error('Failed to get direct video URL'); + } + + return directUrl; +} diff --git a/apps/bot/src/utils/yt-dlp/get-info.ts b/apps/bot/src/utils/yt-dlp/get-info.ts new file mode 100644 index 0000000..74248bc --- /dev/null +++ b/apps/bot/src/utils/yt-dlp/get-info.ts @@ -0,0 +1,1710 @@ +import { runYtDlp } from './yt-dlp'; + +export type A = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Aa = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ab = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Af = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ak = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Am = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ar = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type AutomaticCaptions = { + aa: Aa[]; + ab: Ab[]; + af: Af[]; + ak: Ak[]; + am: Am[]; + ar: Ar[]; + as: A[]; + ay: Ay[]; + az: Az[]; + ba: Ba[]; + be: Be[]; + bg: Bg[]; + bho: Bho[]; + bn: Bn[]; + bo: Bo[]; + br: Br[]; + bs: B[]; + ca: Ca[]; + ceb: Ceb[]; + co: Co[]; + crs: Cr[]; + cs: C[]; + cy: Cy[]; + da: Da[]; + de: De[]; + dv: Dv[]; + dz: Dz[]; + ee: Ee[]; + el: El[]; + en: En[]; + eo: Eo[]; + es: E[]; + et: Et[]; + eu: Eu[]; + fa: Fa[]; + fi: Fi[]; + fil: Fil[]; + fj: Fj[]; + fo: Fo[]; + fr: Fr[]; + fy: Fy[]; + ga: Ga[]; + gaa: Gaa[]; + gd: Gd[]; + gl: Gl[]; + gn: Gn[]; + gu: Gu[]; + gv: Gv[]; + ha: Ha[]; + haw: Haw[]; + hi: Hi[]; + hmn: Hmn[]; + hr: Hr[]; + ht: Ht[]; + hu: Hu[]; + hy: Hy[]; + id: Id[]; + ig: Ig[]; + is: I[]; + it: It[]; + iu: Iu[]; + iw: Iw[]; + ja: Ja[]; + jv: Jv[]; + ka: Ka[]; + kha: Kha[]; + kk: Kk[]; + kl: Kl[]; + km: Km[]; + kn: Kn[]; + ko: Ko[]; + kri: Kri[]; + ku: Ku[]; + ky: Ky[]; + la: La[]; + lb: Lb[]; + lg: Lg[]; + ln: Ln[]; + lo: Lo[]; + lt: Lt[]; + lua: Lua[]; + luo: Luo[]; + lv: Lv[]; + mfe: Mfe[]; + mg: Mg[]; + mi: Mi[]; + mk: Mk[]; + ml: Ml[]; + mn: Mn[]; + mr: Mr[]; + ms: M[]; + mt: Mt[]; + my: My[]; + ne: Ne[]; + new: New[]; + nl: Nl[]; + no: No[]; + nso: Nso[]; + ny: Ny[]; + oc: Oc[]; + om: Om[]; + or: Or[]; + os: O[]; + pa: Pa[]; + pam: Pam[]; + pl: Pl[]; + ps: P[]; + pt: Pt[]; + 'pt-PT': PtPt[]; + qu: Qu[]; + rn: Rn[]; + ro: Ro[]; + ru: Ru[]; + 'ru-orig': RuOrig[]; + rw: Rw[]; + sa: Sa[]; + sd: Sd[]; + sg: Sg[]; + si: Si[]; + sk: Sk[]; + sl: Sl[]; + sm: Sm[]; + sn: Sn[]; + so: So[]; + sq: Sq[]; + sr: Sr[]; + ss: Ss[]; + st: St[]; + su: Su[]; + sv: Sv[]; + sw: Sw[]; + ta: Um[]; + te: Te[]; + tg: Tg[]; + th: Th[]; + ti: Ti[]; + tk: Tk[]; + tn: Tn[]; + to: To[]; + tr: Tr[]; + ts: T[]; + tt: Tt[]; + tum: Tum[]; + ug: Ug[]; + uk: Uk[]; + ur: Ur[]; + uz: Uz[]; + ve: Ve[]; + vi: Vi[]; + war: War[]; + wo: Wo[]; + xh: Xh[]; + yi: Yi[]; + yo: Yo[]; + 'zh-Hans': Han[]; + 'zh-Hant': ZhHant[]; + zu: Zu[]; +}; + +export type Ay = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Az = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type B = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ba = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Be = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Bg = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Bho = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Bn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Bo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Br = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type C = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ca = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ceb = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Co = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Cr = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Cy = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Da = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type De = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type DownloaderOptions = { + http_chunk_size: number; +}; + +export type DownloaderOptions2 = { + http_chunk_size: number; +}; + +export type DownloaderOptions3 = { + http_chunk_size: number; +}; + +export type Dv = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Dz = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type E = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ee = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type El = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type En = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Eo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Et = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Eu = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Fa = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Fi = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Fil = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Fj = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Fo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Format = { + __needs_testing?: boolean; + abr?: number; + acodec?: string; + aspect_ratio?: number; + asr?: number; + audio_channels?: number; + audio_ext: string; + available_at?: number; + columns?: number; + container?: string; + downloader_options?: DownloaderOptions; + dynamic_range?: string; + ext: string; + filesize?: number; + filesize_approx?: number; + format: string; + format_id: string; + format_index: unknown; + format_note?: string; + fps?: number; + fragments?: Fragment[]; + has_drm?: boolean; + height?: number; + http_headers: HttpHeaders; + language?: string; + language_preference?: number; + manifest_url?: string; + preference: unknown; + protocol: string; + quality?: number; + resolution: string; + rows?: number; + source_preference?: number; + tbr?: number; + url: string; + vbr?: number; + vcodec: string; + video_ext: string; + width?: number; +}; + +export type Fr = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Fragment = { + duration: number; + url: string; +}; + +export type Fy = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ga = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Gaa = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Gd = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Gl = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Gn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Gu = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Gv = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ha = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Han = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Haw = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Heatmap = { + end_time: number; + start_time: number; + value: number; +}; + +export type Hi = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Hmn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Hr = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ht = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type HttpHeaders = { + Accept: string; + 'Accept-Language': string; + 'Sec-Fetch-Mode': string; + 'User-Agent': string; +}; + +export type HttpHeaders2 = { + Accept: string; + 'Accept-Language': string; + 'Sec-Fetch-Mode': string; + 'User-Agent': string; +}; + +export type HttpHeaders3 = { + Accept: string; + 'Accept-Language': string; + 'Sec-Fetch-Mode': string; + 'User-Agent': string; +}; + +export type Hu = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Hy = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type I = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Id = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ig = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type InfoRoot = { + _format_sort_fields: string[]; + _has_drm: unknown; + _type: string; + _version: Version; + abr: number; + acodec: string; + age_limit: number; + aspect_ratio: number; + asr: number; + audio_channels: number; + automatic_captions: AutomaticCaptions; + availability: string; + average_rating: unknown; + categories: string[]; + channel: string; + channel_follower_count: number; + channel_id: string; + channel_is_verified: boolean; + channel_url: string; + chapters: unknown; + comment_count: number; + creators: unknown; + description: string; + display_id: string; + duration: number; + duration_string: string; + dynamic_range: string; + epoch: number; + ext: string; + extractor: string; + extractor_key: string; + filesize_approx: number; + format: string; + format_id: string; + format_note: string; + formats: Format[]; + fps: number; + fulltitle: string; + heatmap: Heatmap[]; + height: number; + id: string; + is_live: boolean; + language: string; + like_count: number; + live_status: string; + media_type: string; + original_url: string; + playable_in_embed: boolean; + playlist: unknown; + playlist_index: unknown; + protocol: string; + release_timestamp: unknown; + release_year: unknown; + requested_downloads: RequestedDownload[]; + requested_formats: RequestedFormat2[]; + requested_subtitles: unknown; + resolution: string; + stretched_ratio: unknown; + subtitles: Subtitles; + tags: unknown[]; + tbr: number; + thumbnail: string; + thumbnails: Thumbnail[]; + timestamp: number; + title: string; + upload_date: string; + uploader: string; + uploader_id: string; + uploader_url: string; + vbr: number; + vcodec: string; + view_count: number; + was_live: boolean; + webpage_url: string; + webpage_url_basename: string; + webpage_url_domain: string; + width: number; +}; + +export type It = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Iu = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Iw = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ja = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Jv = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ka = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Kha = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Kk = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Kl = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Km = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Kn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ko = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Kri = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ku = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ky = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type La = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Lb = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Lg = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ln = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Lo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Lt = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Lua = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Luo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Lv = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type M = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mfe = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mg = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mi = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mk = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ml = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mr = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Mt = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type My = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ne = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type New = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Nl = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type No = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Nso = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ny = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type O = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Oc = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Om = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Or = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type P = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Pa = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Pam = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Pl = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Pt = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type PtPt = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Qu = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type RequestedDownload = { + __write_download_archive: boolean; + _filename: string; + abr: number; + acodec: string; + aspect_ratio: number; + asr: number; + audio_channels: number; + dynamic_range: string; + ext: string; + filename: string; + filesize_approx: number; + format: string; + format_id: string; + format_note: string; + fps: number; + height: number; + language: string; + protocol: string; + requested_formats: RequestedFormat[]; + resolution: string; + tbr: number; + vbr: number; + vcodec: string; + width: number; +}; + +export type RequestedFormat = { + abr: number; + acodec: string; + aspect_ratio?: number; + asr?: number; + audio_channels?: number; + audio_ext: string; + available_at: number; + container: string; + downloader_options: DownloaderOptions2; + dynamic_range?: string; + ext: string; + filesize: number; + filesize_approx: number; + format: string; + format_id: string; + format_note: string; + fps?: number; + has_drm: boolean; + height?: number; + http_headers: HttpHeaders2; + language?: string; + language_preference: number; + preference: unknown; + protocol: string; + quality: number; + resolution: string; + source_preference: number; + tbr: number; + url: string; + vbr: number; + vcodec: string; + video_ext: string; + width?: number; +}; + +export type RequestedFormat2 = { + abr: number; + acodec: string; + aspect_ratio?: number; + asr?: number; + audio_channels?: number; + audio_ext: string; + available_at: number; + container: string; + downloader_options: DownloaderOptions3; + dynamic_range?: string; + ext: string; + filesize: number; + filesize_approx: number; + format: string; + format_id: string; + format_note: string; + fps?: number; + has_drm: boolean; + height?: number; + http_headers: HttpHeaders3; + language?: string; + language_preference: number; + preference: unknown; + protocol: string; + quality: number; + resolution: string; + source_preference: number; + tbr: number; + url: string; + vbr: number; + vcodec: string; + video_ext: string; + width?: number; +}; + +export type Rn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ro = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ru = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type RuOrig = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Rw = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sa = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sd = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sg = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Si = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sk = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sl = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sm = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type So = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sq = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sr = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ss = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type St = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Su = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Subtitles = {}; + +export type Sv = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Sw = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type T = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Te = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Tg = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Th = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Thumbnail = { + height?: number; + id: string; + preference: number; + resolution?: string; + url: string; + width?: number; +}; + +export type Ti = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Tk = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Tn = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type To = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Tr = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Tt = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Tum = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ug = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Uk = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Um = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ur = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Uz = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Ve = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Version = { + current_git_head: unknown; + release_git_head: string; + repository: string; + version: string; +}; + +export type Vi = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type War = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Wo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Xh = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Yi = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Yo = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type ZhHant = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export type Zu = { + __yt_dlp_client: string; + ext: string; + impersonate: boolean; + name: string; + url: string; +}; + +export async function getInfo(url: string) { + const output = await runYtDlp([ + '-J', // JSON + '--no-playlist', + url, + ]); + + return JSON.parse(output) as InfoRoot; +} diff --git a/apps/bot/src/utils/yt-dlp/index.ts b/apps/bot/src/utils/yt-dlp/index.ts new file mode 100644 index 0000000..3137a20 --- /dev/null +++ b/apps/bot/src/utils/yt-dlp/index.ts @@ -0,0 +1,2 @@ +export { ytDlpGetUrl } from './get-download-url'; +export { getInfo } from './get-info'; diff --git a/apps/bot/src/utils/yt-dlp/yt-dlp.ts b/apps/bot/src/utils/yt-dlp/yt-dlp.ts new file mode 100644 index 0000000..4280a03 --- /dev/null +++ b/apps/bot/src/utils/yt-dlp/yt-dlp.ts @@ -0,0 +1,32 @@ +import { env } from '@/config/env'; +import { spawn } from 'node:child_process'; + +export function runYtDlp(args: string[]): Promise { + return new Promise((resolve, reject) => { + const ytDlpPath = getYtDlpPath(); + const process = spawn(ytDlpPath, args); + + let output = ''; + let error = ''; + + process.stdout.on('data', (data) => { + output += data.toString(); + }); + + process.stderr.on('data', (data) => { + error += data.toString(); + }); + + process.on('close', (code) => { + if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(`yt-dlp error: ${error}`)); + } + }); + }); +} + +function getYtDlpPath(): string { + return env.YTDLP_PATH || 'yt-dlp'; +}