feat(playlist): show download progress page after import instead of navigating away
플레이리스트 등록 후 폴더로 바로 나가지 않고, 각 다운로드 잡을 폴링하며 항목별/전체 진행률을 보여주는 진행 화면을 추가. 단일 영상 추가 흐름과 동일하게 /op/job/:id 를 폴링하고, 모두 끝나면 "폴더로 이동" 버튼을 노출한다. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,12 @@
|
||||
var zeroPadToggle = document.getElementById('zeroPadToggle')
|
||||
var registerBtn = document.getElementById('registerBtn')
|
||||
var registerStatus = document.getElementById('registerStatus')
|
||||
var probeSection = document.getElementById('probeSection')
|
||||
var progressSection = document.getElementById('progressSection')
|
||||
var progressList = document.getElementById('progressList')
|
||||
var progressSummary = document.getElementById('progressSummary')
|
||||
var progressOverall = document.getElementById('progressOverall')
|
||||
var progressDoneBtn = document.getElementById('progressDoneBtn')
|
||||
|
||||
// ─── 유틸 ─────────────────────────────────────────────────────────────
|
||||
function randomId() {
|
||||
@@ -484,13 +490,7 @@
|
||||
updateRegisterEnable()
|
||||
return
|
||||
}
|
||||
setRegisterStatus(
|
||||
(res.body.jobs ? res.body.jobs.length : 0) + ' 개 다운로드가 큐에 등록되었습니다. 폴더 페이지로 이동합니다…',
|
||||
'idStatusOk'
|
||||
)
|
||||
setTimeout(function () {
|
||||
location.href = folderBaseUrl
|
||||
}, 800)
|
||||
startDownloadProgress(res.body.jobs || [])
|
||||
})
|
||||
.catch(function (err) {
|
||||
state.busy = false
|
||||
@@ -499,6 +499,130 @@
|
||||
})
|
||||
})
|
||||
|
||||
// ─── 다운로드 진행 화면 ────────────────────────────────────────────────
|
||||
// 등록 후 폴더로 바로 나가지 않고, 각 잡을 폴링하며 진행률을 보여준다.
|
||||
// (백엔드는 잡을 큐에 넣고 순차 처리 — queued/downloading/done/error.)
|
||||
function startDownloadProgress(jobs) {
|
||||
if (probeSection) probeSection.hidden = true
|
||||
if (entriesSection) entriesSection.hidden = true
|
||||
if (progressSection) progressSection.hidden = false
|
||||
var items = jobs.map(function (j, i) {
|
||||
var title = (state.entries[i] && state.entries[i].title) || j.videoId
|
||||
return {
|
||||
jobId: j.jobId,
|
||||
videoId: j.videoId,
|
||||
title: title,
|
||||
status: 'queued',
|
||||
progress: 0,
|
||||
message: '대기 중',
|
||||
done: false,
|
||||
error: false,
|
||||
barEl: null,
|
||||
msgEl: null
|
||||
}
|
||||
})
|
||||
renderProgressRows(items)
|
||||
updateProgressUI(items)
|
||||
if (items.length === 0) { finishProgress(items); return }
|
||||
pollAllJobs(items)
|
||||
}
|
||||
|
||||
function renderProgressRows(items) {
|
||||
progressList.innerHTML = ''
|
||||
items.forEach(function (it) {
|
||||
var li = document.createElement('li')
|
||||
li.className = 'progressRow'
|
||||
var name = document.createElement('div')
|
||||
name.className = 'progressName'
|
||||
name.textContent = it.title
|
||||
var bar = document.createElement('progress')
|
||||
bar.className = 'progressBar'
|
||||
bar.max = 100
|
||||
bar.value = 0
|
||||
var msg = document.createElement('div')
|
||||
msg.className = 'progressMsg muted'
|
||||
msg.textContent = it.message
|
||||
li.appendChild(name)
|
||||
li.appendChild(bar)
|
||||
li.appendChild(msg)
|
||||
progressList.appendChild(li)
|
||||
it.barEl = bar
|
||||
it.msgEl = msg
|
||||
it.rowEl = li
|
||||
})
|
||||
}
|
||||
|
||||
function pollAllJobs(items) {
|
||||
var pending = items.filter(function (it) { return !it.done })
|
||||
if (pending.length === 0) { finishProgress(items); return }
|
||||
Promise.all(pending.map(function (it) {
|
||||
return fetch('/op/job/' + encodeURIComponent(it.jobId), { cache: 'no-store' })
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (j) {
|
||||
if (j && j.ok && j.job) {
|
||||
it.status = j.job.status
|
||||
it.message = j.job.message || ''
|
||||
it.progress = typeof j.job.progress === 'number' ? j.job.progress : it.progress
|
||||
if (j.job.status === 'done') {
|
||||
it.done = true
|
||||
it.progress = 100
|
||||
it.message = '완료'
|
||||
} else if (j.job.status === 'error') {
|
||||
it.done = true
|
||||
it.error = true
|
||||
it.message = '실패: ' + (j.job.error || j.job.message || '')
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () { /* 일시 오류는 다음 틱에 재시도 */ })
|
||||
})).then(function () {
|
||||
updateProgressUI(items)
|
||||
if (items.some(function (it) { return !it.done })) {
|
||||
setTimeout(function () { pollAllJobs(items) }, 1500)
|
||||
} else {
|
||||
finishProgress(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateProgressUI(items) {
|
||||
var total = items.length
|
||||
var doneCount = 0
|
||||
var sum = 0
|
||||
items.forEach(function (it) {
|
||||
if (it.barEl) {
|
||||
it.barEl.value = it.progress
|
||||
if (it.error) it.barEl.classList.add('progressBarError')
|
||||
}
|
||||
if (it.msgEl) it.msgEl.textContent = it.message
|
||||
if (it.rowEl) {
|
||||
it.rowEl.classList.toggle('progressRowDone', it.done && !it.error)
|
||||
it.rowEl.classList.toggle('progressRowError', it.error)
|
||||
}
|
||||
if (it.done) doneCount += 1
|
||||
sum += it.progress
|
||||
})
|
||||
if (progressOverall) progressOverall.value = total ? Math.round(sum / total) : 0
|
||||
if (progressSummary) progressSummary.textContent = '다운로드 ' + doneCount + ' / ' + total + ' 완료'
|
||||
}
|
||||
|
||||
function finishProgress(items) {
|
||||
var errCount = items.filter(function (it) { return it.error }).length
|
||||
if (progressOverall) progressOverall.value = 100
|
||||
if (progressSummary) {
|
||||
progressSummary.textContent = errCount
|
||||
? ('완료 — ' + items.length + ' 개 중 ' + errCount + ' 개 실패')
|
||||
: ('전체 ' + items.length + ' 개 다운로드 완료')
|
||||
}
|
||||
var statusEl = document.getElementById('progressStatus')
|
||||
if (statusEl) {
|
||||
statusEl.textContent = errCount
|
||||
? '실패한 항목은 폴더에서 개별로 다시 시도할 수 있습니다.'
|
||||
: '모든 영상이 폴더에 추가되었습니다.'
|
||||
}
|
||||
if (progressDoneBtn) progressDoneBtn.hidden = false
|
||||
}
|
||||
|
||||
// ─── 시간 파싱/표시 + YouTube id 추출 ──────────────────────────────────
|
||||
// "ss" / "mm:ss" / "hh:mm:ss" / 소수 초 를 모두 받아 초(number)로. 빈칸/오류는 null.
|
||||
function parseClock(str) {
|
||||
|
||||
Reference in New Issue
Block a user