feat: implement video site per README spec

- Express + EJS + express-session stack (auth/navbar ported from minecraft_launcher)
- Public: main folder list, folder video grid, internal popup player (/player/:videoId)
- Admin (/op): login, folder CRUD with right-click context menu + add-folder modal
- Admin folder: video grid with right-click edit/rename/delete, "영상 추가" -> editor
- Video editor: drag-drop upload, file picker, YouTube URL probe (ETA + 5분 경고),
  background yt-dlp download with progress polling, navbar title edit, trim controls,
  save runs ffmpeg trim (original preserved)
- Filesystem storage under data/folders/<name>/<videoId>/{meta.json, original.<ext>, edited.<ext>}

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 16:42:00 +09:00
parent 8d13d155de
commit 0db04cf5cd
30 changed files with 3300 additions and 0 deletions

51
public/player.js Normal file
View File

@@ -0,0 +1,51 @@
(function () {
var overlay = document.getElementById('playerOverlay')
var closeBtn = document.getElementById('playerClose')
var video = document.getElementById('playerVideo')
var titleEl = document.getElementById('playerTitle')
function openPlayer(videoId, title) {
// 스펙: /player/:videoId 로 이동한 것처럼 동작하면서 내부 팝업으로 띄운다.
// pushState 로 URL 만 바꿔, 새로고침/직접접근 시 player.ejs 가 응답한다.
history.pushState({ player: true, videoId: videoId }, '', '/player/' + encodeURIComponent(videoId))
titleEl.textContent = title || ''
video.src = '/api/video/' + encodeURIComponent(videoId) + '/file'
overlay.hidden = false
video.play().catch(function () { /* 자동재생 막힘 무시 */ })
}
function closePlayer() {
overlay.hidden = true
video.pause()
video.removeAttribute('src')
video.load()
// 폴더 페이지로 되돌리기
if (history.state && history.state.player) {
history.back()
}
}
document.querySelectorAll('.videoCard').forEach(function (card) {
card.addEventListener('click', function () {
var id = card.getAttribute('data-video-id')
var title = card.querySelector('.videoTitle')
openPlayer(id, title ? title.textContent : '')
})
})
if (closeBtn) closeBtn.addEventListener('click', closePlayer)
if (overlay) overlay.addEventListener('click', function (e) {
if (e.target === overlay) closePlayer()
})
window.addEventListener('popstate', function () {
if (!overlay.hidden) {
overlay.hidden = true
video.pause()
video.removeAttribute('src')
video.load()
}
})
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !overlay.hidden) closePlayer()
})
})()