Files
minecraft_launcher/src/installer-rp/music.ts
claude-bot 5e3a42ff4f Add ffmpeg prep and music ogg download to rp installer
Add src/installer-rp/ffmpeg.ts that downloads BtbN/FFmpeg-Builds
win64-gpl zip into %appdata%/.mc_custom/, extracts ffmpeg.exe out
of bin/, drops it at %appdata%/.mc_custom/ffmpeg.exe and verifies
with `ffmpeg -version`. Reuses existing extract-zip dep.

Add src/installer-rp/music.ts that spawns yt-dlp with
--extract-audio --audio-format vorbis --ffmpeg-location <ffmpeg.exe>
to produce <tempDir>/NN.ogg per track. Streams yt-dlp stdout to
the log channel and reports stderr on non-zero exit.

Wire both into the install IPC handler: step 2-1 now preps both
binaries, step 2-2 iterates the music list and downloads each
track. Track the currently running child process in state so the
cancel button can kill it instead of waiting for it to finish.

Image / zip / place steps remain stubbed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 15:23:01 +09:00

70 lines
2.3 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process'
import { promises as fs } from 'node:fs'
import path from 'node:path'
export interface DownloadMusicOptions {
ytdlpExe: string
ffmpegExe: string
/** %appdata%/.mc_custom/.temp/ 같은 작업 폴더. */
tempDir: string
/** 1부터 시작하는 곡 번호 (파일명 zero-pad 에 사용). */
index: number
/** 유튜브 영상 주소. */
url: string
log?: (line: string) => void
/** 현재 실행 중인 자식 프로세스를 외부에 알림 (취소용). */
onChild?: (child: ChildProcess) => void
}
/**
* yt-dlp 로 유튜브 영상에서 오디오만 추출해 vorbis(.ogg) 로 변환한다.
* 결과 파일 경로를 돌려준다. 실패하면 reject.
*
* 호출자는 onChild 콜백으로 받은 ChildProcess 에 .kill() 을 호출해 취소할 수 있다.
*/
export function downloadMusicTrack(opts: DownloadMusicOptions): Promise<string> {
const padded = String(opts.index).padStart(2, '0')
const outBase = path.join(opts.tempDir, padded)
const outPath = outBase + '.ogg'
return new Promise((resolve, reject) => {
const args = [
'--no-warnings',
'--no-playlist',
'--extract-audio',
'--audio-format', 'vorbis',
'--audio-quality', '0',
'--ffmpeg-location', opts.ffmpegExe,
'-o', outBase + '.%(ext)s',
opts.url
]
const child = spawn(opts.ytdlpExe, args, { stdio: ['ignore', 'pipe', 'pipe'] })
opts.onChild?.(child)
let stderr = ''
child.stdout?.on('data', (chunk: Buffer) => {
const line = chunk.toString('utf8').trimEnd()
if (line) opts.log?.(`yt-dlp> ${line}`)
})
child.stderr?.on('data', (chunk: Buffer) => {
stderr += chunk.toString('utf8')
})
child.on('error', (err) => reject(err))
child.on('close', async (code, signal) => {
if (signal) {
reject(new Error(`yt-dlp 가 신호 ${signal} 로 종료됨`))
return
}
if (code !== 0) {
reject(new Error(`yt-dlp 종료 코드 ${code}: ${stderr.trim() || '(stderr 없음)'}`))
return
}
// .ogg 가 실제로 생성됐는지 확인
try {
await fs.access(outPath)
resolve(outPath)
} catch {
reject(new Error(`예상 출력파일이 없음: ${outPath}`))
}
})
})
}