yt-dlp/ffmpeg: reinstall latest on failure, retry once
오래된 yt-dlp/ffmpeg 가 유튜브 변경을 못 따라가 다운로드가 실패할 때 최신 버전으로 강제 재설치 후 한 번 더 시도한다. - server youtube.ts: ensureYtDlp(force) 추가(캐시·zipapp 삭제 후 최신 재다운로드). fetchVideoMeta/fetchPlaylistEntries 를 runYtDlp 로 묶어 1차 실패 시 강제 재설치 후 재시도. - installer ytdlp.ts/ffmpeg.ts: ensure*Exe(log, force) 추가. - installer main.ts: 음악 워커가 곡 다운로드 실패 시 전역 1회 강제 재설치 (refreshBinariesOnce) 후 해당 곡을 1회 재시도.
This commit is contained in:
@@ -311,16 +311,30 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
// 2-1. yt-dlp / ffmpeg 준비 (%appdata%/.mc_custom/{yt-dlp,ffmpeg}.exe)
|
||||
sendLog(t('log.ytdlpPreparing'))
|
||||
sendProgress({ phase: 'prep', message: t('progress.ytdlpPreparing') })
|
||||
const ytDlpBin = await ensureYtDlpExe(sendLog)
|
||||
let ytDlpBin = await ensureYtDlpExe(sendLog)
|
||||
sendLog(t('log.ytdlpPath', { path: ytDlpBin }))
|
||||
throwIfCancelled()
|
||||
sendLog(t('log.ffmpegPreparing'))
|
||||
sendProgress({ phase: 'prep', message: t('progress.ffmpegPreparing') })
|
||||
const ffmpegBin = await ensureFfmpegExe(sendLog)
|
||||
let ffmpegBin = await ensureFfmpegExe(sendLog)
|
||||
sendLog(t('log.ffmpegPath', { path: ffmpegBin }))
|
||||
sendProgress({ phase: 'prep', message: t('progress.ready'), done: true })
|
||||
throwIfCancelled()
|
||||
|
||||
// 음악 다운로드가 실패하면 yt-dlp/ffmpeg 가 너무 오래된 버전이라 유튜브 변경을
|
||||
// 못 따라가는 경우일 수 있다. 그때 최신 버전으로 한 번만 강제 재설치한다.
|
||||
// 워커 여러 개가 동시에 실패해도 재설치는 단 한 번만 일어나도록 락으로 직렬화.
|
||||
let binRefreshPromise: Promise<void> | null = null
|
||||
async function refreshBinariesOnce(): Promise<void> {
|
||||
if (!binRefreshPromise) {
|
||||
binRefreshPromise = (async () => {
|
||||
ytDlpBin = await ensureYtDlpExe(sendLog, true)
|
||||
ffmpegBin = await ensureFfmpegExe(sendLog, true)
|
||||
})()
|
||||
}
|
||||
await binRefreshPromise
|
||||
}
|
||||
|
||||
// 2-2. 음악 다운로드 (CPU 코어 수 기반 자동 동시 다운로드, 시차 출발, ogg 변환)
|
||||
const musicDir = path.join(tempRoot, 'music')
|
||||
await fsp.mkdir(musicDir, { recursive: true })
|
||||
@@ -346,38 +360,51 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
const idx = i + 1
|
||||
sendLog(t('log.musicTrackStart', { idx }))
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'running' })
|
||||
let child: ChildProcess | null = null
|
||||
try {
|
||||
const outPath = await downloadMusicTrack({
|
||||
ytdlpExe: ytDlpBin,
|
||||
ffmpegExe: ffmpegBin,
|
||||
tempDir: musicDir,
|
||||
index: idx,
|
||||
url: entry.url,
|
||||
log: sendLog,
|
||||
onChild: (c) => {
|
||||
child = c
|
||||
state.activeChildren.add(c)
|
||||
},
|
||||
onProgress: (pct) => {
|
||||
// 다운로드(0~90%) + 변환(90~100%) 으로 매핑.
|
||||
sendProgress({
|
||||
phase: 'item', kind: 'music', index: idx, total: musicTotal,
|
||||
percent: Math.min(90, pct * 0.9), status: 'running'
|
||||
})
|
||||
// 한 곡당 최대 2회 시도: 1차 실패 시 yt-dlp/ffmpeg 를 최신으로 강제
|
||||
// 재설치(전역 1회)한 뒤 같은 곡을 다시 받아본다.
|
||||
let attemptedRefresh = false
|
||||
while (true) {
|
||||
let child: ChildProcess | null = null
|
||||
try {
|
||||
const outPath = await downloadMusicTrack({
|
||||
ytdlpExe: ytDlpBin,
|
||||
ffmpegExe: ffmpegBin,
|
||||
tempDir: musicDir,
|
||||
index: idx,
|
||||
url: entry.url,
|
||||
log: sendLog,
|
||||
onChild: (c) => {
|
||||
child = c
|
||||
state.activeChildren.add(c)
|
||||
},
|
||||
onProgress: (pct) => {
|
||||
// 다운로드(0~90%) + 변환(90~100%) 으로 매핑.
|
||||
sendProgress({
|
||||
phase: 'item', kind: 'music', index: idx, total: musicTotal,
|
||||
percent: Math.min(90, pct * 0.9), status: 'running'
|
||||
})
|
||||
}
|
||||
})
|
||||
if (child) state.activeChildren.delete(child)
|
||||
sendLog(t('log.musicTrackDone', { idx, name: path.basename(outPath) }))
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 100, status: 'done' })
|
||||
break
|
||||
} catch (err) {
|
||||
if (child) state.activeChildren.delete(child)
|
||||
if (state.cancelRequested) {
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: t('progress.cancelled') })
|
||||
return
|
||||
}
|
||||
})
|
||||
if (child) state.activeChildren.delete(child)
|
||||
sendLog(t('log.musicTrackDone', { idx, name: path.basename(outPath) }))
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 100, status: 'done' })
|
||||
} catch (err) {
|
||||
if (child) state.activeChildren.delete(child)
|
||||
if (state.cancelRequested) {
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: t('progress.cancelled') })
|
||||
return
|
||||
if (!attemptedRefresh) {
|
||||
attemptedRefresh = true
|
||||
sendLog(t('log.musicRetryAfterRefresh', { idx, message: (err as Error).message }))
|
||||
await refreshBinariesOnce()
|
||||
if (state.cancelRequested) return
|
||||
continue
|
||||
}
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: (err as Error).message })
|
||||
throw new Error(t('errors.musicDownloadFailed', { idx, message: (err as Error).message }))
|
||||
}
|
||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: (err as Error).message })
|
||||
throw new Error(t('errors.musicDownloadFailed', { idx, message: (err as Error).message }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user