fix: harden youtube retry against data corruption
Add a one-shot retry guard (retriedBy) so concurrent retries of the same failed job can't spawn multiple yt-dlp processes into the same directory, and a sourceType/sourceUrl match check so retrying a job whose video ID was repurposed for a different video is rejected instead of overwriting it. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -226,6 +226,8 @@ export interface DownloadJob {
|
||||
error: string | null
|
||||
/** 다운로드 완료 후 적용할 자르기 구간. null 이면 자르기 없음(전체). */
|
||||
trim: VideoTrim | null
|
||||
/** 이 (실패한) 잡을 재시도해 만든 새 잡 id. 한 잡당 재시도 1회만 허용하는 가드. */
|
||||
retriedBy: string | null
|
||||
}
|
||||
|
||||
const jobs = new Map<string, DownloadJob>()
|
||||
@@ -371,7 +373,8 @@ function enqueueJob(p: {
|
||||
finishedAt: null,
|
||||
outputFile: null,
|
||||
error: null,
|
||||
trim: p.trim
|
||||
trim: p.trim,
|
||||
retriedBy: null
|
||||
}
|
||||
jobs.set(job.id, job)
|
||||
void persistJob(job)
|
||||
@@ -391,10 +394,22 @@ export async function retryDownloadJob(jobId: string): Promise<DownloadJob> {
|
||||
if (prev.status !== 'error') {
|
||||
throw new Error('실패한 작업만 다시 시도할 수 있습니다.')
|
||||
}
|
||||
// 한 잡당 재시도 1회만. 같은 실패 잡으로 retry 가 동시에 여러 번 들어오면
|
||||
// 같은 디렉터리로 yt-dlp 가 여럿 떠 파일이 깨진다. 아래 sync 검증을 통과한
|
||||
// 직후(첫 await 전에) 즉시 표시해 두 번째 호출이 곧바로 거절되게 한다.
|
||||
if (prev.retriedBy) throw new Error('이미 재시도한 작업입니다. 새로 생성된 작업에서 다시 시도해 주세요.')
|
||||
if (!getFolder(prev.folderId)) throw new Error('폴더를 찾을 수 없습니다.')
|
||||
// yt-dlp 가 없으면 다시 시도해도 실패하므로 사전에 던진다.
|
||||
getYtDlpPath()
|
||||
if (!getVideoInFolder(prev.folderId, prev.videoId)) {
|
||||
const existing = getVideoInFolder(prev.folderId, prev.videoId)
|
||||
// 사용자가 실패 카드를 지운 뒤 같은 ID 로 다른 영상을 추가했다면, 그 영상 디렉터리에
|
||||
// 옛 URL 을 덮어쓰면 안 된다. 남아 있는 레코드가 이 잡과 같은 영상일 때만 재시도한다.
|
||||
if (existing && (existing.sourceType !== 'youtube' || existing.sourceUrl !== prev.url)) {
|
||||
throw new Error('이 영상 ID 는 다른 영상으로 교체되어 재시도할 수 없습니다.')
|
||||
}
|
||||
// 검증 통과 — 이 시점부터 같은 잡의 중복 재시도를 막는다 (createVideo await 이전).
|
||||
prev.retriedBy = 'pending'
|
||||
if (!existing) {
|
||||
// 사용자가 실패 카드를 지웠다면 같은 ID 로 레코드를 다시 만든다.
|
||||
await createVideo({
|
||||
id: prev.videoId,
|
||||
@@ -412,6 +427,8 @@ export async function retryDownloadJob(jobId: string): Promise<DownloadJob> {
|
||||
title: prev.title,
|
||||
trim: prev.trim
|
||||
})
|
||||
prev.retriedBy = job.id
|
||||
void persistJob(prev)
|
||||
setImmediate(pump)
|
||||
return job
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user