diff --git a/public/playlist.js b/public/playlist.js index 74dd475..444530e 100644 --- a/public/playlist.js +++ b/public/playlist.js @@ -17,10 +17,11 @@ var startUrl = folderBaseUrl + '/playlist/start' var ID_RE = /^[a-zA-Z0-9_-]{1,64}$/ + var DEFAULT_LENGTH = 15 // 기본 재생 길이(초) // ─── state ──────────────────────────────────────────────────────────── // entries: [{ url, title, videoId, durationSec, thumbnailUrl, ytId, - // startSec:number(기본0), endSec:number|null(null=끝까지), + // startSec:number(기본0), lengthSec:number(기본15, 시작부터 재생할 초), // idStatus:'pending'|'ok'|'bad', idReason:string, idDebounce:number }] var state = { entries: [], @@ -187,7 +188,7 @@ + '' + '
' + '' - + '' + + '' + '
' + '' + '' @@ -196,7 +197,7 @@ var titleInput = li.querySelector('.entryTitleInput') var idInput = li.querySelector('.entryIdInput') var startInput = li.querySelector('.entryStartInput') - var endInput = li.querySelector('.entryEndInput') + var lengthInput = li.querySelector('.entryLengthInput') var durSpan = li.querySelector('.entryDuration') var removeBtn = li.querySelector('.entryRemove') @@ -215,7 +216,7 @@ titleInput.value = entry.title || '' idInput.value = entry.videoId || '' durSpan.textContent = formatDuration(entry.durationSec) - paintTrimInputs(idx, startInput, endInput) + paintTrimInputs(idx, startInput, lengthInput) titleInput.addEventListener('input', function () { state.entries[idx].title = titleInput.value @@ -230,15 +231,13 @@ var e = state.entries[idx] var sec = parseClock(startInput.value) e.startSec = sec != null && sec > 0 ? sec : 0 - // 시작이 종료 이상이면 종료를 풀어 끝까지로. - if (e.endSec != null && e.endSec <= e.startSec) e.endSec = null - paintTrimInputs(idx, startInput, endInput) + paintTrimInputs(idx, startInput, lengthInput) }) - endInput.addEventListener('change', function () { + lengthInput.addEventListener('change', function () { var e = state.entries[idx] - var sec = parseClock(endInput.value) - e.endSec = normalizeEnd(sec, e.startSec, e.durationSec) - paintTrimInputs(idx, startInput, endInput) + var sec = parseClock(lengthInput.value) + e.lengthSec = sec != null && sec > 0 ? sec : DEFAULT_LENGTH + paintTrimInputs(idx, startInput, lengthInput) }) thumbBox.addEventListener('click', function () { openPreview(idx) }) li.addEventListener('contextmenu', function (ev) { @@ -260,24 +259,19 @@ return li } - // 항목의 시작/종료 입력칸 값을 상태로부터 다시 그린다. - // 종료는 endSec 이 null 이면 영상 길이(기본값)를 표시한다. - function paintTrimInputs(idx, startInput, endInput) { + // 항목의 시작/길이 입력칸 값을 상태로부터 다시 그린다. + function paintTrimInputs(idx, startInput, lengthInput) { var e = state.entries[idx] if (!e) return if (!startInput) { var row = findRow(idx) if (!row) return startInput = row.querySelector('.entryStartInput') - endInput = row.querySelector('.entryEndInput') - if (!startInput || !endInput) return + lengthInput = row.querySelector('.entryLengthInput') + if (!startInput || !lengthInput) return } startInput.value = formatClock(e.startSec || 0) - if (e.endSec != null) { - endInput.value = formatClock(e.endSec) - } else { - endInput.value = e.durationSec != null ? formatClock(e.durationSec) : '' - } + lengthInput.value = formatLength(e.lengthSec) } function findRow(idx) { @@ -441,7 +435,7 @@ ytId: extractYouTubeId(e.url), videoId: '', startSec: 0, - endSec: null, + lengthSec: DEFAULT_LENGTH, idStatus: 'pending', idReason: '' } @@ -469,7 +463,7 @@ title: (e.title || '').trim(), videoId: (e.videoId || '').trim(), startSec: e.startSec || 0, - endSec: e.endSec != null ? e.endSec : null + endSec: computeEndSec(e) } }) } @@ -538,39 +532,47 @@ ) return m ? m[1] : null } - // 종료초 정규화: null/오류/시작이하/영상끝 이상 → null(끝까지), 그 외 number. - function normalizeEnd(sec, startSec, durationSec) { - if (sec == null || !isFinite(sec)) return null - if (sec <= (startSec || 0)) return null - if (durationSec != null && sec >= durationSec - 0.01) return null - return sec + // 재생 길이(초) 표시용. 정수면 "15", 소수면 "12.3". + function formatLength(sec) { + if (sec == null || !isFinite(sec) || sec <= 0) return '' + var r = Math.round(sec * 10) / 10 + return r % 1 === 0 ? String(r) : r.toFixed(1) + } + // 시작 + 길이 → 백엔드용 종료초. 영상 끝을 넘으면 null(끝까지)로. + function computeEndSec(e) { + var start = e.startSec || 0 + var len = e.lengthSec != null && e.lengthSec > 0 ? e.lengthSec : DEFAULT_LENGTH + var end = start + len + if (e.durationSec != null && end >= e.durationSec - 0.01) return null + return end } function clampNum(v, lo, hi) { return Math.min(hi, Math.max(lo, v)) } - // ─── 일괄 시작/종료 적용 + 초기화 ────────────────────────────────────── + // ─── 일괄 시작/길이 적용 + 초기화 ────────────────────────────────────── var bulkStart = document.getElementById('bulkStart') - var bulkEnd = document.getElementById('bulkEnd') + var bulkLength = document.getElementById('bulkLength') var bulkApplyBtn = document.getElementById('bulkApplyBtn') var bulkResetBtn = document.getElementById('bulkResetBtn') bulkApplyBtn.addEventListener('click', function () { var s = parseClock(bulkStart.value) var startSec = s != null && s > 0 ? s : 0 - var endRaw = parseClock(bulkEnd.value) // 빈칸이면 null → 끝까지 + var len = parseClock(bulkLength.value) + var lengthSec = len != null && len > 0 ? len : DEFAULT_LENGTH for (var i = 0; i < state.entries.length; i++) { var e = state.entries[i] e.startSec = startSec - e.endSec = normalizeEnd(endRaw, startSec, e.durationSec) + e.lengthSec = lengthSec } render() }) bulkResetBtn.addEventListener('click', function () { for (var i = 0; i < state.entries.length; i++) { state.entries[i].startSec = 0 - state.entries[i].endSec = null // 끝까지(=영상 길이) + state.entries[i].lengthSec = DEFAULT_LENGTH } bulkStart.value = '' - bulkEnd.value = '' + bulkLength.value = '' render() }) @@ -722,11 +724,17 @@ } trimTitle.textContent = e.title || '' trimOverlay.hidden = false + // 항목의 시작 + 길이를 타임라인의 시작/끝으로 변환. 영상 길이를 넘으면 끝까지로. + var startSec = e.startSec || 0 + var lenSec = e.lengthSec != null && e.lengthSec > 0 ? e.lengthSec : DEFAULT_LENGTH + var dur = e.durationSec || 0 + var endSec = startSec + lenSec + if (dur && endSec >= dur - 0.01) endSec = null tctx = { idx: idx, - duration: e.durationSec || 0, - start: e.startSec || 0, - end: e.endSec != null ? e.endSec : null + duration: dur, + start: startSec, + end: endSec } selStopAt = null renderTrimBar() @@ -778,7 +786,10 @@ var e = state.entries[tctx.idx] if (e) { e.startSec = tctx.start > 0 ? tctx.start : 0 - e.endSec = normalizeEnd(tctx.end, e.startSec, tctx.duration) + // 타임라인의 끝(없으면 영상 끝) - 시작 = 재생 길이. + var end = tctx.end != null ? tctx.end : tctx.duration + var len = end - e.startSec + e.lengthSec = len > 0 ? len : DEFAULT_LENGTH if (tctx.duration && e.durationSec == null) e.durationSec = tctx.duration paintTrimInputs(tctx.idx) } diff --git a/views/op/playlist.ejs b/views/op/playlist.ejs index d4dabed..65178cf 100644 --- a/views/op/playlist.ejs +++ b/views/op/playlist.ejs @@ -42,7 +42,7 @@
일괄 구간 - +
@@ -101,7 +101,7 @@ 선택: 0.0초 -

재생바의 파란 핸들을 끌어 구간을 정합니다. 적용하면 항목의 시작/종료 시간이 갱신되고, 다운로드 완료 후 이 구간으로 잘립니다.

+

재생바의 파란 핸들을 끌어 구간을 정합니다. 적용하면 항목의 시작/길이가 갱신되고, 다운로드 완료 후 이 구간으로 잘립니다.