perf(installer-rp): parallel music download (3 concurrent) + fragmented chunks
- yt-dlp 인자에 --concurrent-fragments 5 추가 (HLS/DASH 청크 병렬 다운로드) - yt-dlp 인자에 --newline 추가 (진행률 라인 안정화) - 음악 다운로드 루프를 단일 순차 → worker pool 3개 동시 처리로 전환 - state.currentChild (단일) → state.activeChildren (Set) 으로 확장, 취소 시 실행 중인 모든 자식 프로세스 kill - UI 는 카드 그리드라 병렬 진행 상태가 그대로 표시됨 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,13 @@ interface RpInstallerState {
|
|||||||
selectedKey: string | null
|
selectedKey: string | null
|
||||||
/** 현재 설치 진행 중인지 여부. 취소 신호로 사용. */
|
/** 현재 설치 진행 중인지 여부. 취소 신호로 사용. */
|
||||||
cancelRequested: boolean
|
cancelRequested: boolean
|
||||||
/** 현재 실행 중인 외부 프로세스(yt-dlp/ffmpeg). 취소 시 kill 대상. */
|
/** 현재 실행 중인 외부 프로세스들(yt-dlp/ffmpeg). 취소 시 모두 kill. */
|
||||||
currentChild: ChildProcess | null
|
activeChildren: Set<ChildProcess>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 동시 yt-dlp 프로세스 수. 너무 높이면 유튜브가 throttle. */
|
||||||
|
const MUSIC_CONCURRENCY = 3
|
||||||
|
|
||||||
const DEFAULT_MANIFEST_URL = process.env.MANIFEST_URL ?? 'http://127.0.0.1:3000/manifest.json'
|
const DEFAULT_MANIFEST_URL = process.env.MANIFEST_URL ?? 'http://127.0.0.1:3000/manifest.json'
|
||||||
|
|
||||||
const state: RpInstallerState = {
|
const state: RpInstallerState = {
|
||||||
@@ -35,7 +38,7 @@ const state: RpInstallerState = {
|
|||||||
packs: new Map(),
|
packs: new Map(),
|
||||||
selectedKey: null,
|
selectedKey: null,
|
||||||
cancelRequested: false,
|
cancelRequested: false,
|
||||||
currentChild: null
|
activeChildren: new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
@@ -196,16 +199,24 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
sendProgress({ phase: 'prep', message: '준비 완료', done: true })
|
sendProgress({ phase: 'prep', message: '준비 완료', done: true })
|
||||||
throwIfCancelled()
|
throwIfCancelled()
|
||||||
|
|
||||||
// 2-2. 음악 다운로드 (1번부터 순차, ogg 변환)
|
// 2-2. 음악 다운로드 (MUSIC_CONCURRENCY 개씩 병렬, ogg 변환)
|
||||||
const musicDir = path.join(tempRoot, 'music')
|
const musicDir = path.join(tempRoot, 'music')
|
||||||
await fsp.mkdir(musicDir, { recursive: true })
|
await fsp.mkdir(musicDir, { recursive: true })
|
||||||
sendLog(`음악 다운로드 시작 (${musicTotal}곡)`)
|
sendLog(`음악 다운로드 시작 (${musicTotal}곡, 동시 ${MUSIC_CONCURRENCY}개)`)
|
||||||
for (let i = 0; i < musicTotal; i++) {
|
|
||||||
throwIfCancelled()
|
// 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias.
|
||||||
const entry = pack.list.music[i]
|
const musicList = pack.list.music
|
||||||
|
let nextIndex = 0
|
||||||
|
async function musicWorker(): Promise<void> {
|
||||||
|
while (true) {
|
||||||
|
if (state.cancelRequested) return
|
||||||
|
const i = nextIndex++
|
||||||
|
if (i >= musicTotal) return
|
||||||
|
const entry = musicList[i]
|
||||||
const idx = i + 1
|
const idx = i + 1
|
||||||
sendLog(`${idx}번 노래 다운로드 중…`)
|
sendLog(`${idx}번 노래 다운로드 시작`)
|
||||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'running' })
|
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'running' })
|
||||||
|
let child: ChildProcess | null = null
|
||||||
try {
|
try {
|
||||||
const outPath = await downloadMusicTrack({
|
const outPath = await downloadMusicTrack({
|
||||||
ytdlpExe: ytDlpBin,
|
ytdlpExe: ytDlpBin,
|
||||||
@@ -214,7 +225,10 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
index: idx,
|
index: idx,
|
||||||
url: entry.url,
|
url: entry.url,
|
||||||
log: sendLog,
|
log: sendLog,
|
||||||
onChild: (c) => { state.currentChild = c },
|
onChild: (c) => {
|
||||||
|
child = c
|
||||||
|
state.activeChildren.add(c)
|
||||||
|
},
|
||||||
onProgress: (pct) => {
|
onProgress: (pct) => {
|
||||||
// 다운로드(0~90%) + 변환(90~100%) 으로 매핑.
|
// 다운로드(0~90%) + 변환(90~100%) 으로 매핑.
|
||||||
sendProgress({
|
sendProgress({
|
||||||
@@ -223,19 +237,26 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
state.currentChild = null
|
if (child) state.activeChildren.delete(child)
|
||||||
sendLog(`${idx}번 노래 완료: ${path.basename(outPath)}`)
|
sendLog(`${idx}번 노래 완료: ${path.basename(outPath)}`)
|
||||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 100, status: 'done' })
|
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 100, status: 'done' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.currentChild = null
|
if (child) state.activeChildren.delete(child)
|
||||||
if (state.cancelRequested) {
|
if (state.cancelRequested) {
|
||||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: '취소됨' })
|
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', message: '취소됨' })
|
||||||
throwIfCancelled()
|
return
|
||||||
}
|
}
|
||||||
sendProgress({ phase: 'item', kind: 'music', index: idx, total: musicTotal, percent: 0, status: 'error', 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(`${idx}번 노래 다운로드 실패: ${(err as Error).message}`)
|
throw new Error(`${idx}번 노래 다운로드 실패: ${(err as Error).message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerCount = Math.min(MUSIC_CONCURRENCY, musicTotal)
|
||||||
|
const workers: Promise<void>[] = []
|
||||||
|
for (let w = 0; w < workerCount; w++) workers.push(musicWorker())
|
||||||
|
await Promise.all(workers)
|
||||||
|
throwIfCancelled()
|
||||||
|
|
||||||
// 2-3. 사진 다운로드 + painting variant 정규화
|
// 2-3. 사진 다운로드 + painting variant 정규화
|
||||||
const paintingDir = path.join(tempRoot, 'painting')
|
const paintingDir = path.join(tempRoot, 'painting')
|
||||||
@@ -296,9 +317,9 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
|||||||
|
|
||||||
ipcMain.handle('rp:install:cancel', async () => {
|
ipcMain.handle('rp:install:cancel', async () => {
|
||||||
state.cancelRequested = true
|
state.cancelRequested = true
|
||||||
sendLog('취소 요청됨. 진행 중 작업을 중단합니다…')
|
sendLog(`취소 요청됨. 실행 중 프로세스 ${state.activeChildren.size}개 중단…`)
|
||||||
if (state.currentChild && !state.currentChild.killed) {
|
for (const child of state.activeChildren) {
|
||||||
state.currentChild.kill()
|
if (!child.killed) child.kill()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export function downloadMusicTrack(opts: DownloadMusicOptions): Promise<string>
|
|||||||
const args = [
|
const args = [
|
||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
'--no-playlist',
|
'--no-playlist',
|
||||||
|
// 단일 파일이 아니라 HLS/DASH fragmented 스트림일 때 청크를 병렬로.
|
||||||
|
// 일반 progressive 다운로드에는 영향 없음.
|
||||||
|
'--concurrent-fragments', '5',
|
||||||
|
// 진행률 표시 안정화 (yt-dlp 가 \r 대신 새 줄로 출력).
|
||||||
|
'--newline',
|
||||||
'--extract-audio',
|
'--extract-audio',
|
||||||
'--audio-format', 'vorbis',
|
'--audio-format', 'vorbis',
|
||||||
'--audio-quality', '0',
|
'--audio-quality', '0',
|
||||||
|
|||||||
Reference in New Issue
Block a user