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(' | ') }) )