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 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 16:35:11 +09:00
parent 8c9dc88e8b
commit 3248d096e4
2 changed files with 38 additions and 5 deletions

0
�@@P� Normal file
View File

View File

@@ -32,21 +32,35 @@ function getYtDlpAssetName(): string {
return 'yt-dlp' // 그 외 OS: 순수 파이썬 zipapp. python3 가 PATH 에 있어야 동작
}
/** 로컬 설치 경로: %appdata%/.mc_custom/<asset> */
/**
* 로컬 설치 경로: OS별 사용자 데이터 디렉터리 안의 .mc_custom/<asset>.
* - 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<string> | 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<string> {
const target = getYtDlpInstallPath()
@@ -105,6 +119,25 @@ async function prepareYtDlp(target: string): Promise<string> {
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(' | ') })
)