Wire yt-dlp.exe prep into resource pack installer
Add src/installer-rp/ytdlp.ts that downloads https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe into %appdata%/.mc_custom/yt-dlp.exe (fixed path, Windows-only since the installer is shipped as .exe). If the file is already there and --version works it is reused; otherwise it is re-downloaded and verified. The server's existing OS-aware ensureYtDlp stays intact. The install IPC handler now calls ensureYtDlpExe() in step 2-1 and logs the resolved path. Music / image / zip / place steps are still stubbed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { URL } from 'node:url'
|
||||
import type { Manifest, PackList } from '../shared/types.js'
|
||||
import { getAppDataDir, getMcCustomDir } from '../shared/paths.js'
|
||||
import type { RpFetchedPack } from './types.js'
|
||||
import { ensureYtDlpExe } from './ytdlp.js'
|
||||
|
||||
interface RpInstallerState {
|
||||
manifestUrl: string
|
||||
@@ -141,9 +142,10 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
await fsp.mkdir(tempRoot, { recursive: true })
|
||||
|
||||
try {
|
||||
// 2-1. yt-dlp 준비
|
||||
sendLog('yt-dlp 준비 중… (TODO)')
|
||||
// TODO: ensureYtDlp() — shared 모듈로 분리 예정.
|
||||
// 2-1. yt-dlp 준비 (%appdata%/.mc_custom/yt-dlp.exe)
|
||||
sendLog('yt-dlp 준비 중…')
|
||||
const ytDlpBin = await ensureYtDlpExe(sendLog)
|
||||
sendLog(`yt-dlp 경로: ${ytDlpBin}`)
|
||||
throwIfCancelled()
|
||||
|
||||
// 2-2. 음악 다운로드 (1번부터 순차, ogg 변환)
|
||||
|
||||
109
src/installer-rp/ytdlp.ts
Normal file
109
src/installer-rp/ytdlp.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { promises as fs, createWriteStream, constants as fsConst } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import https from 'node:https'
|
||||
import http from 'node:http'
|
||||
import { getMcCustomDir } from '../shared/paths.js'
|
||||
|
||||
/**
|
||||
* 리소스팩 간편설치기는 Windows .exe 로 배포되므로 yt-dlp.exe 한 종류만 사용.
|
||||
* 경로: %appdata%/.mc_custom/yt-dlp.exe
|
||||
*/
|
||||
export function getYtDlpExePath(): string {
|
||||
return path.join(getMcCustomDir(), 'yt-dlp.exe')
|
||||
}
|
||||
|
||||
const YT_DLP_DOWNLOAD_URL =
|
||||
'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'
|
||||
|
||||
let installPromise: Promise<string> | null = null
|
||||
|
||||
/**
|
||||
* %appdata%/.mc_custom/yt-dlp.exe 가 없거나 실행 불가능하면 GitHub Releases
|
||||
* 의 최신 yt-dlp.exe 를 받아 설치하고, 그 절대경로를 돌려준다.
|
||||
*/
|
||||
export async function ensureYtDlpExe(
|
||||
log?: (line: string) => void
|
||||
): Promise<string> {
|
||||
const target = getYtDlpExePath()
|
||||
if (await canExecute(target)) {
|
||||
log?.(`yt-dlp.exe 이미 있음: ${target}`)
|
||||
return target
|
||||
}
|
||||
if (installPromise) return installPromise
|
||||
|
||||
installPromise = (async () => {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
log?.(`yt-dlp.exe 다운로드 중: ${YT_DLP_DOWNLOAD_URL}`)
|
||||
await downloadToFile(YT_DLP_DOWNLOAD_URL, target)
|
||||
const okVersion = await probeVersion(target)
|
||||
if (!okVersion) {
|
||||
throw new Error('yt-dlp.exe 다운로드는 됐지만 실행 검증에 실패했습니다.')
|
||||
}
|
||||
log?.(`yt-dlp.exe 준비 완료: ${target}`)
|
||||
return target
|
||||
} catch (err) {
|
||||
// 부분 다운로드 흔적 정리
|
||||
try { await fs.unlink(target) } catch { /* noop */ }
|
||||
throw new Error(
|
||||
'yt-dlp.exe 자동 설치 실패: ' +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
)
|
||||
} finally {
|
||||
installPromise = null
|
||||
}
|
||||
})()
|
||||
return installPromise
|
||||
}
|
||||
|
||||
async function canExecute(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath, fsConst.F_OK)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return probeVersion(filePath)
|
||||
}
|
||||
|
||||
function probeVersion(bin: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(bin, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let ok = false
|
||||
child.stdout.on('data', () => { ok = true })
|
||||
child.on('error', () => resolve(false))
|
||||
child.on('close', (code) => resolve(ok && code === 0))
|
||||
})
|
||||
}
|
||||
|
||||
/** GitHub Releases 의 latest URL 은 302 리다이렉트를 사용하므로 따라가며 받음. */
|
||||
function downloadToFile(url: string, dest: string, redirects = 0): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (redirects > 8) {
|
||||
reject(new Error('redirect 가 너무 많습니다.'))
|
||||
return
|
||||
}
|
||||
const lib = url.startsWith('https://') ? https : http
|
||||
const req = lib.get(url, {
|
||||
headers: { 'user-agent': 'mc-music-quiz-rp-installer' }
|
||||
}, (res) => {
|
||||
const code = res.statusCode || 0
|
||||
if (code >= 300 && code < 400 && res.headers.location) {
|
||||
res.resume()
|
||||
downloadToFile(res.headers.location, dest, redirects + 1).then(resolve, reject)
|
||||
return
|
||||
}
|
||||
if (code !== 200) {
|
||||
res.resume()
|
||||
reject(new Error(`HTTP ${code} (${url})`))
|
||||
return
|
||||
}
|
||||
const out = createWriteStream(dest)
|
||||
res.pipe(out)
|
||||
out.on('finish', () => out.close((err) => err ? reject(err) : resolve()))
|
||||
out.on('error', reject)
|
||||
res.on('error', reject)
|
||||
})
|
||||
req.on('error', reject)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user