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:
94
src/routes/public.ts
Normal file
94
src/routes/public.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Router } from 'express'
|
||||
import path from 'node:path'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import {
|
||||
findVideoAnywhere,
|
||||
folderPath,
|
||||
listFolders,
|
||||
listVideos,
|
||||
loadVideoMeta,
|
||||
sanitizeFolderName,
|
||||
videoDir,
|
||||
videoFileFsPath
|
||||
} from '../store.js'
|
||||
|
||||
export const publicRouter = Router()
|
||||
|
||||
publicRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const folders = await listFolders()
|
||||
res.render('index', { folders })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
publicRouter.get('/folder/:name', async (req, res, next) => {
|
||||
try {
|
||||
const safe = sanitizeFolderName(req.params.name)
|
||||
if (!safe) {
|
||||
res.status(404).send('폴더를 찾을 수 없습니다.')
|
||||
return
|
||||
}
|
||||
// 존재 확인
|
||||
try {
|
||||
await fs.access(folderPath(safe))
|
||||
} catch {
|
||||
res.status(404).send('폴더를 찾을 수 없습니다.')
|
||||
return
|
||||
}
|
||||
const videos = await listVideos(safe)
|
||||
res.render('folder', { folder: safe, videos, isAdmin: false })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
publicRouter.get('/player/:videoId', async (req, res, next) => {
|
||||
try {
|
||||
const found = await findVideoAnywhere(req.params.videoId)
|
||||
if (!found) {
|
||||
res.status(404).send('영상을 찾을 수 없습니다.')
|
||||
return
|
||||
}
|
||||
res.render('player', { folder: found.folder, video: found.meta })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 영상 파일 스트리밍. ?edited=1 이면 편집본을, 아니면 원본을 보낸다. */
|
||||
publicRouter.get('/api/video/:videoId/file', async (req, res, next) => {
|
||||
try {
|
||||
const found = await findVideoAnywhere(req.params.videoId)
|
||||
if (!found) {
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
const wantEdited = req.query.edited === '1' || req.query.edited === 'true'
|
||||
const fileName =
|
||||
wantEdited && found.meta.editedFile ? found.meta.editedFile : found.meta.originalFile
|
||||
if (!fileName || fileName.includes('%(ext)s')) {
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
const fsPath = videoFileFsPath(found.folder, found.meta.id, fileName)
|
||||
res.sendFile(fsPath)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/** 비디오 메타 조회 (플레이어/관리자 양쪽에서 사용) */
|
||||
publicRouter.get('/api/video/:videoId', async (req, res, next) => {
|
||||
try {
|
||||
const found = await findVideoAnywhere(req.params.videoId)
|
||||
if (!found) {
|
||||
res.status(404).json({ ok: false, message: '영상을 찾을 수 없습니다.' })
|
||||
return
|
||||
}
|
||||
res.json({ ok: true, folder: found.folder, video: found.meta })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user