From 3248d096e4d4501e80bdb4a648a98a730b07d849 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 25 May 2026 16:35:11 +0900 Subject: [PATCH] server-youtube: add POSIX zipapp fallback when native bundled binary won't run If the native yt-dlp_linux/yt-dlp_macos binary fails to execute (glibc mismatch, musl libc, wrong arch) AND no system yt-dlp is on PATH, fall back to downloading the universal Python zipapp ('yt-dlp', ~3MB) and running it via shebang. Requires python3 on PATH, which is standard on modern Linux servers. Also: rewrite stale Windows-flavored install-path comments to reflect actual cross-platform behavior. Co-Authored-By: Claude Opus 4.7 --- "\001ΐ\034@@Pφ" | 0 src/server/youtube.ts | 43 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 "\001ΐ\034@@Pφ" diff --git "a/\001ΐ\034@@Pφ" "b/\001ΐ\034@@Pφ" new file mode 100644 index 0000000..e69de29 diff --git a/src/server/youtube.ts b/src/server/youtube.ts index b7b49fa..8b64286 100644 --- a/src/server/youtube.ts +++ b/src/server/youtube.ts @@ -32,21 +32,35 @@ function getYtDlpAssetName(): string { return 'yt-dlp' // κ·Έ μ™Έ OS: 순수 파이썬 zipapp. python3 κ°€ PATH 에 μžˆμ–΄μ•Ό λ™μž‘ } -/** 둜컬 μ„€μΉ˜ 경둜: %appdata%/.mc_custom/ */ +/** + * 둜컬 μ„€μΉ˜ 경둜: OS별 μ‚¬μš©μž 데이터 디렉터리 μ•ˆμ˜ .mc_custom/. + * - Windows: %APPDATA%/.mc_custom/yt-dlp.exe + * - macOS : ~/Library/Application Support/.mc_custom/yt-dlp_macos + * - Linux λ“±: $XDG_CONFIG_HOME λ˜λŠ” ~/.config/.mc_custom/yt-dlp_linux (arch 따라 닀름) + */ export function getYtDlpInstallPath(): string { return path.join(getMcCustomDir(), getYtDlpAssetName()) } +/** 순수 파이썬 zipapp(`yt-dlp`) 의 둜컬 μ„€μΉ˜ 경둜. python3 κ°€ PATH 에 μžˆμ–΄μ•Ό λ™μž‘. */ +function getYtDlpZipappPath(): string { + return path.join(getMcCustomDir(), 'yt-dlp_zipapp') +} + /** ν•œ λ²ˆμ— ν•œ λ‹€μš΄λ‘œλ“œλ§Œ μ§„ν–‰ν•˜λ„λ‘ 락 (μ„œλ²„ λ™μ‹œ μš”μ²­ 보호). */ let installPromise: Promise | null = null type ProbeResult = { ok: true } | { ok: false; detail: string } /** - * %appdata%/.mc_custom/ 에 yt-dlp κ°€ μ€€λΉ„λλŠ”μ§€ ν™•μΈν•˜κ³ , μ—†μœΌλ©΄ GitHub Releases μ—μ„œ - * ν˜„μž¬ OS/μ•„ν‚€ν…μ²˜μš© λ°”μ΄λ„ˆλ¦¬λ₯Ό μžλ™μœΌλ‘œ λ°›μ•„ μ„€μΉ˜ν•œλ‹€. 성곡 μ‹œ μ‹€ν–‰ 경둜 λ°˜ν™˜. - * λ‹€μš΄λ‘œλ“œλœ λ°”μ΄λ„ˆλ¦¬κ°€ μ‹€ν–‰λ˜μ§€ μ•ŠλŠ” ν™˜κ²½(antivirus, κΆŒν•œ 문제 λ“±)이라면 - * PATH 의 `yt-dlp(.exe)` 둜 ν΄λ°±ν•˜κ³ , 그것도 μ‹€νŒ¨ν•˜λ©΄ 진단정보가 ν¬ν•¨λœ μ—λŸ¬λ₯Ό λ˜μ§„λ‹€. + * .mc_custom/ 디렉터리에 yt-dlp κ°€ μ€€λΉ„λλŠ”μ§€ ν™•μΈν•˜κ³ , μ—†μœΌλ©΄ GitHub Releases μ—μ„œ + * ν˜„μž¬ OS/μ•„ν‚€ν…μ²˜μš© λ„€μ΄ν‹°λΈŒ λ°”μ΄λ„ˆλ¦¬λ₯Ό μžλ™μœΌλ‘œ λ°›μ•„ μ„€μΉ˜ν•œλ‹€. 성곡 μ‹œ μ‹€ν–‰ 경둜 λ°˜ν™˜. + * + * λ„€μ΄ν‹°λΈŒ λ°”μ΄λ„ˆλ¦¬κ°€ μ‹€ν–‰λ˜μ§€ μ•ŠλŠ” ν™˜κ²½(glibc 미슀맀치, musl libc, antivirus 차단 λ“±) + * 이면 λ‹€μŒ μˆœμ„œλ‘œ ν΄λ°±ν•œλ‹€: + * 1) PATH 의 `yt-dlp(.exe)` (μ‹œμŠ€ν…œμ— λ”°λ‘œ 깐 κ±°) + * 2) (POSIX ν•œμ •) λ²”μš© 파이썬 zipapp `yt-dlp` λ₯Ό λ‹€μš΄λ‘œλ“œ ν›„ shebang μ‹€ν–‰ β€” python3 ν•„μš” + * μ „λΆ€ μ‹€νŒ¨ν•˜λ©΄ 각 μ‹œλ„μ˜ 진단정보가 ν¬ν•¨λœ μ—λŸ¬λ₯Ό λ˜μ§„λ‹€. */ export async function ensureYtDlp(): Promise { const target = getYtDlpInstallPath() @@ -105,6 +119,25 @@ async function prepareYtDlp(target: string): Promise { try { await fs.unlink(target) } catch { /* noop */ } } + // 4. POSIX ν•œμ • μ΅œν›„ 폴백: λ²”μš© 파이썬 zipapp `yt-dlp` λ‹€μš΄λ‘œλ“œ ν›„ shebang μ‹€ν–‰. + // λ„€μ΄ν‹°λΈŒ λ°”μ΄λ„ˆλ¦¬κ°€ glibc/musl/arch 문제둜 λͺ» λ„λŠ” λ¦¬λˆ…μŠ€ ν™˜κ²½μ΄λΌλ„ + // python3 κ°€ PATH 에 있으면 λ™μž‘ν•œλ‹€. ~ 3MB 짜리 슀크립트. + if (process.platform !== 'win32') { + const zipappPath = getYtDlpZipappPath() + try { + try { await fs.unlink(zipappPath) } catch { /* noop */ } + await downloadToFile('https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp', zipappPath) + await fs.chmod(zipappPath, 0o755) + const probe = await probeVersion(zipappPath) + if (probe.ok) return zipappPath + diagnostics.push(`zipapp yt-dlp 검증 μ‹€νŒ¨: ${probe.detail} (python3 λˆ„λ½μ΄κ±°λ‚˜ PATH 에 μ—†μŒ)`) + try { await fs.unlink(zipappPath) } catch { /* noop */ } + } catch (err) { + diagnostics.push(`zipapp λ‹€μš΄λ‘œλ“œ μ‹€νŒ¨: ${err instanceof Error ? err.message : String(err)}`) + try { await fs.unlink(zipappPath) } catch { /* noop */ } + } + } + throw new YtDlpUnavailableError( t('youtube.ytdlpVerifyFailedDetail', { detail: diagnostics.join(' | ') }) )