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