feat: 플레이리스트 실패 항목 재시도 버튼 + API
실패한 다운로드는 영상 레코드가 남아 같은 ID 재등록이 막혔고, 진행 화면의 "개별로 다시 시도" 안내에 실제 버튼이 없었다. 실패 행에 "재시도" 버튼을 추가하고 POST /op/job/:id/retry 로 같은 ID·URL·자르기 설정으로 새 잡을 시작한다. retryDownloadJob 은 기존 영상 레코드를 재사용(레코드가 지워졌으면 같은 ID로 복원) 하므로 UNIQUE 충돌 없이 재시도된다. 연령 제한 영상은 쿠키 등록 후 재시도하면 받힌다. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -542,19 +542,66 @@
|
||||
var msg = document.createElement('div')
|
||||
msg.className = 'progressMsg muted'
|
||||
msg.textContent = it.message
|
||||
var retry = document.createElement('button')
|
||||
retry.type = 'button'
|
||||
retry.className = 'secondaryButton progressRetry'
|
||||
retry.textContent = '재시도'
|
||||
retry.hidden = true
|
||||
retry.addEventListener('click', function () { retryItem(it, items) })
|
||||
li.appendChild(name)
|
||||
li.appendChild(bar)
|
||||
li.appendChild(msg)
|
||||
li.appendChild(retry)
|
||||
progressList.appendChild(li)
|
||||
it.barEl = bar
|
||||
it.msgEl = msg
|
||||
it.rowEl = li
|
||||
it.retryEl = retry
|
||||
})
|
||||
}
|
||||
|
||||
// 실패한 항목을 같은 ID·URL·자르기 설정으로 다시 받는다. 백엔드가 기존 영상
|
||||
// 레코드를 재사용하므로 ID 충돌 없이 새 잡이 시작된다.
|
||||
function retryItem(it, items) {
|
||||
if (!it.error || it.retrying) return
|
||||
it.retrying = true
|
||||
if (it.retryEl) { it.retryEl.disabled = true; it.retryEl.textContent = '재시도 중…' }
|
||||
fetch('/op/job/' + encodeURIComponent(it.jobId) + '/retry', { method: 'POST' })
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (j) {
|
||||
if (j && j.ok) {
|
||||
it.jobId = j.jobId
|
||||
it.done = false
|
||||
it.error = false
|
||||
it.retrying = false
|
||||
it.status = 'queued'
|
||||
it.progress = 0
|
||||
it.message = '재시도 대기 중'
|
||||
if (it.barEl) it.barEl.classList.remove('progressBarError')
|
||||
if (it.retryEl) { it.retryEl.hidden = true; it.retryEl.disabled = false; it.retryEl.textContent = '재시도' }
|
||||
updateProgressUI(items)
|
||||
if (progressDoneBtn) progressDoneBtn.hidden = true
|
||||
if (!polling) pollAllJobs(items)
|
||||
} else {
|
||||
it.retrying = false
|
||||
if (it.retryEl) { it.retryEl.disabled = false; it.retryEl.textContent = '재시도' }
|
||||
it.message = '재시도 실패: ' + ((j && j.message) || '오류')
|
||||
updateProgressUI(items)
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
it.retrying = false
|
||||
if (it.retryEl) { it.retryEl.disabled = false; it.retryEl.textContent = '재시도' }
|
||||
it.message = '재시도 실패: ' + (err && err.message ? err.message : '요청 오류')
|
||||
updateProgressUI(items)
|
||||
})
|
||||
}
|
||||
|
||||
var polling = false
|
||||
function pollAllJobs(items) {
|
||||
var pending = items.filter(function (it) { return !it.done })
|
||||
if (pending.length === 0) { finishProgress(items); return }
|
||||
if (pending.length === 0) { polling = false; finishProgress(items); return }
|
||||
polling = true
|
||||
Promise.all(pending.map(function (it) {
|
||||
return fetch('/op/job/' + encodeURIComponent(it.jobId), { cache: 'no-store' })
|
||||
.then(function (r) { return r.json() })
|
||||
@@ -580,6 +627,7 @@
|
||||
if (items.some(function (it) { return !it.done })) {
|
||||
setTimeout(function () { pollAllJobs(items) }, 1500)
|
||||
} else {
|
||||
polling = false
|
||||
finishProgress(items)
|
||||
}
|
||||
})
|
||||
@@ -599,6 +647,7 @@
|
||||
it.rowEl.classList.toggle('progressRowDone', it.done && !it.error)
|
||||
it.rowEl.classList.toggle('progressRowError', it.error)
|
||||
}
|
||||
if (it.retryEl && !it.retrying) it.retryEl.hidden = !it.error
|
||||
if (it.done) doneCount += 1
|
||||
sum += it.progress
|
||||
})
|
||||
@@ -617,7 +666,7 @@
|
||||
var statusEl = document.getElementById('progressStatus')
|
||||
if (statusEl) {
|
||||
statusEl.textContent = errCount
|
||||
? '실패한 항목은 폴더에서 개별로 다시 시도할 수 있습니다.'
|
||||
? '실패한 항목은 아래 "재시도" 버튼으로 다시 받을 수 있습니다. (연령 제한 영상은 먼저 대시보드에서 YouTube 쿠키를 등록하세요.)'
|
||||
: '모든 영상이 폴더에 추가되었습니다.'
|
||||
}
|
||||
if (progressDoneBtn) progressDoneBtn.hidden = false
|
||||
|
||||
Reference in New Issue
Block a user