installer-rp: defer yt-dlp/ffmpeg reinstall until all music workers finish
Avoid the Windows file-lock race where one worker deletes/overwrites yt-dlp.exe/ffmpeg.exe while sibling workers still run those processes. Now pass 1 downloads all tracks and collects failures without any mid-flight refresh; after Promise.all (no live child processes), the binaries are force-reinstalled once and only the failed tracks retry. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,7 @@
|
|||||||
"musicStart": "음악 다운로드 시작 ({{total}}곡, 동시 {{concurrency}}개, 시차 {{stagger}}ms)",
|
"musicStart": "음악 다운로드 시작 ({{total}}곡, 동시 {{concurrency}}개, 시차 {{stagger}}ms)",
|
||||||
"musicTrackStart": "{{idx}}번 노래 다운로드 시작",
|
"musicTrackStart": "{{idx}}번 노래 다운로드 시작",
|
||||||
"musicTrackDone": "{{idx}}번 노래 완료: {{name}}",
|
"musicTrackDone": "{{idx}}번 노래 완료: {{name}}",
|
||||||
"musicRetryAfterRefresh": "{{idx}}번 노래 실패({{message}}) → yt-dlp/ffmpeg 최신 버전으로 재설치 후 재시도",
|
"musicRefreshRetry": "{{count}}곡 다운로드 실패 → yt-dlp/ffmpeg 최신 버전으로 재설치 후 실패한 곡만 재시도",
|
||||||
"ytdlpReinstall": "yt-dlp.exe 최신 버전으로 강제 재설치 중…",
|
"ytdlpReinstall": "yt-dlp.exe 최신 버전으로 강제 재설치 중…",
|
||||||
"ffmpegReinstall": "ffmpeg.exe 최신 버전으로 강제 재설치 중…",
|
"ffmpegReinstall": "ffmpeg.exe 최신 버전으로 강제 재설치 중…",
|
||||||
"imageStart": "사진 다운로드 시작 ({{total}}장)",
|
"imageStart": "사진 다운로드 시작 ({{total}}장)",
|
||||||
|
|||||||
@@ -347,6 +347,59 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
|
|
||||||
// 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias.
|
// 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias.
|
||||||
const musicList = pack.list.music
|
const musicList = pack.list.music
|
||||||
|
// 곡별 마지막 실패 메시지(재시도 단계에서 최종 에러 메시지로 사용).
|
||||||
|
const failedMessages = new Map<number, string>()
|
||||||
|
|
||||||
|
// 한 곡을 한 번 받아본다. 성공 true / 실패 false.
|
||||||
|
// emitErrorProgress=false 면 실패해도 UI 에 'error' 상태를 보내지 않는다(재시도 예정).
|
||||||
|
async function tryDownloadTrack(i: number, emitErrorProgress: boolean): Promise<boolean> {
|
||||||
|
const entry = musicList[i]
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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' })
|
||||||
|
return true
|
||||||
|
} 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 false
|
||||||
|
}
|
||||||
|
failedMessages.set(i, (err as Error).message)
|
||||||
|
if (emitErrorProgress) {
|
||||||
|
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: (err as Error).message })
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1차 다운로드: 동시 워커로 전부 받아보고, 실패한 곡 인덱스만 모은다.
|
||||||
|
// 여기서는 yt-dlp/ffmpeg 재설치를 하지 않는다(다른 워커가 같은 exe 를 실행 중일 수
|
||||||
|
// 있어 Windows 파일 잠금으로 삭제/덮어쓰기가 실패할 수 있기 때문).
|
||||||
|
const failed: number[] = []
|
||||||
let nextIndex = 0
|
let nextIndex = 0
|
||||||
async function musicWorker(): Promise<void> {
|
async function musicWorker(): Promise<void> {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -356,56 +409,8 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
// 시차 게이트: 새 다운로드 시작은 직전 시작과 최소 MUSIC_START_STAGGER_MS 간격을 둠.
|
// 시차 게이트: 새 다운로드 시작은 직전 시작과 최소 MUSIC_START_STAGGER_MS 간격을 둠.
|
||||||
await acquireMusicStartSlot()
|
await acquireMusicStartSlot()
|
||||||
if (state.cancelRequested) return
|
if (state.cancelRequested) return
|
||||||
const entry = musicList[i]
|
const ok = await tryDownloadTrack(i, false)
|
||||||
const idx = i + 1
|
if (!ok && !state.cancelRequested) failed.push(i)
|
||||||
sendLog(t('log.musicTrackStart', { idx }))
|
|
||||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, 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 (!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 }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +420,29 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
await Promise.all(workers)
|
await Promise.all(workers)
|
||||||
throwIfCancelled()
|
throwIfCancelled()
|
||||||
|
|
||||||
|
// 1차에서 실패한 곡이 있으면, 모든 워커가 끝나 실행 중인 yt-dlp/ffmpeg 자식
|
||||||
|
// 프로세스가 하나도 없는 지금 시점에 단 한 번 최신 버전으로 강제 재설치한다.
|
||||||
|
// (각 워커 promise 는 자식 프로세스 close 후 resolve 되므로 여기선 exe 가 잠겨
|
||||||
|
// 있지 않다 → Windows 파일 잠금 문제 없음.) 그런 다음 실패한 곡만 순차 재시도.
|
||||||
|
if (failed.length > 0) {
|
||||||
|
failed.sort((a, b) => a - b)
|
||||||
|
sendLog(t('log.musicRefreshRetry', { count: failed.length }))
|
||||||
|
await refreshBinariesOnce()
|
||||||
|
throwIfCancelled()
|
||||||
|
nextMusicStartAt = Date.now()
|
||||||
|
for (const i of failed) {
|
||||||
|
throwIfCancelled()
|
||||||
|
await acquireMusicStartSlot()
|
||||||
|
throwIfCancelled()
|
||||||
|
const ok = await tryDownloadTrack(i, true)
|
||||||
|
if (!ok) {
|
||||||
|
throwIfCancelled()
|
||||||
|
const idx = i + 1
|
||||||
|
throw new Error(t('errors.musicDownloadFailed', { idx, message: failedMessages.get(i) ?? '' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2-3. 사진 다운로드 + painting variant 정규화
|
// 2-3. 사진 다운로드 + painting variant 정규화
|
||||||
const paintingDir = path.join(tempRoot, 'painting')
|
const paintingDir = path.join(tempRoot, 'painting')
|
||||||
await fsp.mkdir(paintingDir, { recursive: true })
|
await fsp.mkdir(paintingDir, { recursive: true })
|
||||||
|
|||||||
Reference in New Issue
Block a user