diff --git a/README.md b/README.md index 87ac977..bde8e37 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ data/ - `/folder/:topName` — 1단계 폴더 (하위 폴더 + 영상 혼합 표시) - `/folder/:topName/:subName` — 2단계 폴더 (영상만 표시, 더 깊이 들어갈 수 없음) - `/video/:topName/:videoId` 또는 `/video/:topName/:subName/:videoId` — 외부 공유용 짧은 URL. 경로의 폴더 부분이 영상 위치와 일치할 때만 200. -- `/file/video/:videoId` — 실제 파일 스트림 (편집본 있으면 편집본). `?edited=0` 으로 원본 강제. +- `/api/video/:topName[/:subName]/:videoId` — 실제 파일 스트림 (정규). 재생 페이지와 동일한 폴더 경로 규칙으로, 경로가 영상 위치와 일치할 때만 200. `?edited=0` 으로 원본 강제. +- `/file/video/:videoId`, `/api/video/:videoId/file` — ID 기반 파일 스트림 (레거시·외부 공유 호환). 관리자(`/op`): - `/op` — 폴더 목록 (로그인 필요) diff --git a/public/player.js b/public/player.js index cd63561..5baa591 100644 --- a/public/player.js +++ b/public/player.js @@ -11,7 +11,11 @@ var url = shareUrl || ('/file/video/' + encodeURIComponent(videoId)) history.pushState({ player: true, videoId: videoId }, '', url) titleEl.textContent = title || '' - video.src = '/api/video/' + encodeURIComponent(videoId) + '/file' + // 파일 스트림 URL 은 공유 URL(/video/폴더/.../아이디)의 /video/ 를 + // /api/video/ 로 바꾼 폴더 경로 기반 주소. shareUrl 이 없으면 레거시 폴백. + video.src = shareUrl + ? shareUrl.replace(/^\/video\//, '/api/video/') + : ('/file/video/' + encodeURIComponent(videoId)) overlay.hidden = false video.play().catch(function () { /* 자동재생 막힘 무시 */ }) } diff --git a/src/routes/public.ts b/src/routes/public.ts index 1bc36f5..85f1a57 100644 --- a/src/routes/public.ts +++ b/src/routes/public.ts @@ -83,10 +83,23 @@ const videoViewHandler: RequestHandler = (req, res, next) => { publicRouter.get('/video/:topName/:videoId', videoViewHandler) publicRouter.get('/video/:topName/:subName/:videoId', videoViewHandler) +/** 결정된 비디오의 파일을 응답으로 보낸다 (?edited=0 이면 원본 강제). */ +function sendVideoFile(video: Video, req: Parameters[0], res: Parameters[1]): void { + const editedParam = typeof req.query.edited === 'string' ? req.query.edited : '' + const wantOriginal = editedParam === '0' || editedParam === 'false' + const fileName = + !wantOriginal && video.editedFile ? video.editedFile : video.originalFile + if (!fileName || fileName.includes('%(ext)s')) { + res.status(404).end() + return + } + res.sendFile(videoFileFsPath(video.id, fileName)) +} + /** - * 영상 파일 스트리밍 (짧은 ID-only URL — 외부 공유 호환용). + * 영상 파일 스트리밍 (짧은 ID-only URL — 외부 공유 호환용 / 레거시). * - GET /file/video/:videoId - * - GET /api/video/:videoId/file (legacy alias) + * - GET /api/video/:videoId/file * ?edited=0 이면 원본을 강제, 기본은 편집본 있으면 편집본. */ const streamVideoHandler: RequestHandler = (req, res, next) => { @@ -96,16 +109,7 @@ const streamVideoHandler: RequestHandler = (req, res, next) => { res.status(404).end() return } - const editedParam = typeof req.query.edited === 'string' ? req.query.edited : '' - const wantOriginal = editedParam === '0' || editedParam === 'false' - const fileName = - !wantOriginal && video.editedFile ? video.editedFile : video.originalFile - if (!fileName || fileName.includes('%(ext)s')) { - res.status(404).end() - return - } - const fsPath = videoFileFsPath(video.id, fileName) - res.sendFile(fsPath) + sendVideoFile(video, req, res) } catch (err) { next(err) } @@ -113,6 +117,37 @@ const streamVideoHandler: RequestHandler = (req, res, next) => { publicRouter.get('/file/video/:videoId', streamVideoHandler) publicRouter.get('/api/video/:videoId/file', streamVideoHandler) +/** + * 영상 파일 스트리밍 (폴더 경로 + ID — 정규 공유 URL). + * - GET /api/video/:topName/:videoId + * - GET /api/video/:topName/:subName/:videoId + * + * 재생 페이지(/video/...)와 동일한 경로 규칙. 마지막 세그먼트가 영상 ID 이고 + * 앞쪽 세그먼트는 그 영상이 실제 속한 폴더 경로와 일치해야 한다. + * + * 라우트 등록 순서 주의: 위의 `/api/video/:videoId/file` (2세그·끝이 리터럴 "file") + * 보다 뒤에 둬서, 레거시 URL 이 이 경로 라우트에 가로채이지 않게 한다. + */ +const streamVideoByPathHandler: RequestHandler = (req, res, next) => { + try { + const { folderSegments, videoId } = collectVideoSegments(req.params) + const video = getVideo(videoId) + if (!video) { + res.status(404).end() + return + } + if (!segmentsEqual(folderPathNames(video.folderId), folderSegments)) { + res.status(404).end() + return + } + sendVideoFile(video, req, res) + } catch (err) { + next(err) + } +} +publicRouter.get('/api/video/:topName/:videoId', streamVideoByPathHandler) +publicRouter.get('/api/video/:topName/:subName/:videoId', streamVideoByPathHandler) + /** * 영상 썸네일 (카드용). 없으면 ffmpeg 로 즉석 생성 후 thumb.jpg 로 캐시. * 생성 불가(ffmpeg 없음/다운로드 미완 등)면 404 → 카드에서 ▶ placeholder 로 폴백. diff --git a/views/op/editor.ejs b/views/op/editor.ejs index b1eafe9..c347432 100644 --- a/views/op/editor.ejs +++ b/views/op/editor.ejs @@ -49,7 +49,7 @@ + <% if (video) { %>src="/api/video/<%= folderPathEnc %>/<%= encodeURIComponent(video.id) %>?edited=0"<% } %>>
diff --git a/views/player.ejs b/views/player.ejs index 7ca14a7..7760197 100644 --- a/views/player.ejs +++ b/views/player.ejs @@ -27,7 +27,7 @@
- +