692a824e7870ac2cb2917e3372648f1277b42b01
업로드 영상처럼 외부 source_url 이 없는 항목은 `[ID] [URL]` 두 칸 형식이 깨지므로 목록에서 제외한다. youtube-nocookie.com/embed/... 도 YouTube 로 인식해 https://youtu.be/ID 로 간결화한다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
비디오 사이트
영상 업로드/관리/공개 재생을 다 한 곳에서 처리하는 Express + EJS + better-sqlite3 사이트입니다. 컴퓨터 파일이나 YouTube 링크(단일 영상/플레이리스트) 를 가져와 폴더 단위로 정리하고, 짧은 공유 URL로 외부에 노출합니다.
실행
처음 한 번은 setup 으로 의존성 + yt-dlp 바이너리 + 빌드까지 한 번에 끝낼 수 있어요:
cp .env.example .env # 포트/호스트 등 환경변수 편집
npm run setup # npm install + ./bin/yt-dlp 다운로드 + tsc
npm start # .env 의 HOST:PORT 로 바인딩
수동으로 단계별로 하고 싶다면:
npm install
npm run build
npm start
설정은 전부 .env 한 파일로 모았습니다 (.env.example 참고).
HOST— 바인딩 주소. 외부 노출이 필요하면0.0.0.0, 로컬 전용이면127.0.0.1.PORT— 바인딩 포트 (기본3000).SESSION_SECRET— 운영 시 반드시 충분히 긴 랜덤 문자열로 교체.UPLOAD_MAX_BYTES— 업로드 용량 한도(바이트). 비우거나 미설정이면 기본1073741824(1 GiB). 무제한으로 두려면0또는Infinity. 잘못된 값은 기본 1 GiB 로 fallback.HTTP_REQUEST_TIMEOUT_MS— 대용량 업로드용 HTTP 요청 타임아웃(밀리초).0/미설정이면 무제한.MAX_DOWNLOAD_CONCURRENCY— 플레이리스트 임포트 시 동시 yt-dlp 잡 개수 상한 (기본2).- 관리자 비밀번호는
account.json의password값 (초기값admin, 운영 시 반드시 변경).
외부 의존
yt-dlp— YouTube 영상 가져오기.npm run setup이./bin/yt-dlp로 자동 설치하지만 PATH 에 이미 있어도 됩니다. Alpine / distroless 같이 glibc 가 없는 슬림 도커 베이스에서는 번들 바이너리가 안 도니apk add yt-dlp(또는apt-get install yt-dlp,pip install yt-dlp) 로 PATH 에 직접 설치하세요. 도커 빌드에서 아예 받지 않으려면SKIP_YT_DLP=1 npm run setup. (검증 실패해도 setup 은 경고만 출력하고 빌드는 계속 진행됩니다.)ffmpeg/ffprobe— 영상 트림 저장, 60fps/FHD 변환, 메타 조회 (PATH에 설치). 없으면 다운로드는 되지만 후처리가 건너뛰어집니다.- 하드웨어 인코더(
h264_nvenc/h264_qsv/h264_videotoolbox/h264_vaapi) — 있으면 자동 감지해 사용. 없으면libx264 -preset ultrafast폴백. VAAPI 는/dev/dri/renderD128이 보일 때만 후보로 들어갑니다. - YouTube 로그인 쿠키 — 연령 제한("Sign in to confirm your age") 영상은 로그인 세션 없이는 yt-dlp 가 받지 못합니다. 관리자 대시보드(
/op/dashboard)의 "YouTube 쿠키" 패널에서 브라우저 확장으로 내보낸 Netscape 형식cookies.txt를 등록하면 단일 영상 probe·플레이리스트 probe·실제 다운로드에 모두--cookies로 적용됩니다. 파일은data/cookies.txt에 권한0600으로 저장됩니다(세션 토큰이 들어 있어 git 에는 올라가지 않음). 등록된 쿠키로도 못 보는 영상이면 그 영상을 볼 수 있는 계정의 쿠키가 필요합니다.
데이터 위치
data/
app.db # 메인 sqlite (폴더 트리, 영상 메타, 세션)
folders/<topName>[/<subName>]/<videoId>/ # 영상 파일
original.<ext> # 원본 (항상 보존)
edited.<ext> # 트림 저장 시 생성
jobs/ # 백그라운드 yt-dlp 다운로드 잡 상태
tmp/ # 작업 임시 (업로드/변환 중간 산출물)
폴더 트리 / 영상 메타는 app.db 에서 관리하고, 실제 영상 파일은 폴더 경로 + videoId 디렉터리에 저장됩니다. 폴더 이름을 바꾸면 디스크 디렉터리도 같이 rename 됩니다.
URL 규칙
공개:
/— 최상위 폴더 목록/folder/:topName— 1단계 폴더 (하위 폴더 + 영상 혼합 표시)/folder/:topName/:subName— 2단계 폴더 (영상만 표시, 더 깊이 들어갈 수 없음)/video/:topName/:videoId또는/video/:topName/:subName/:videoId— 외부 공유용 짧은 URL. 경로의 폴더 부분이 영상 위치와 일치할 때만 200./api/video/:topName[/:subName]/:videoId— 실제 파일 스트림 (정규). 재생 페이지와 동일한 폴더 경로 규칙으로, 경로가 영상 위치와 일치할 때만 200.?edited=0으로 원본 강제./file/video/:videoId— ID 기반 파일 스트림 (레거시·외부 공유 호환). ID 가 전역에서 유일할 때만 200, 여러 폴더에 같은 ID 가 있으면 404.
관리자(/op):
/op— 폴더 목록 (로그인 필요)/op/folder/:topName[/[:subName]]— 관리자 폴더 화면/op/folder/:topName[/[:subName]]/video/editor— 영상 추가/편집 화면
스펙
메인 페이지 (/)
- 최상위 폴더를 카드 그리드로 보여줍니다.
- 폴더를 누르면
/folder/:폴더이름으로 이동.
폴더 화면 (/folder/:topName[/:subName])
- 1단계 폴더 화면: 하위 폴더 카드 + 이 폴더에 직접 담긴 영상을 함께 보여줍니다.
- 2단계 폴더 화면: 영상만 보여줍니다 (썸네일 + 제목). 더 깊이 들어갈 수 없습니다.
- 영상 카드 클릭 →
/video/:topName[/:subName]/:videoId로 이동, 플레이어가 뜨고 재생.
관리자 페이지 (/op)
- 로그인 로직과 navbar 는
git.tkrmagid.kr/tkrmagid/minecraft_launcher의 op 페이지에서 가져온 형식을 따릅니다. - 메인 페이지처럼 폴더를 나열, 우클릭으로 이름 수정 / 삭제.
- 오른쪽 위 "폴더 추가" 버튼으로 폴더 생성 (팝업에서 이름 입력).
관리자 폴더 화면 (/op/folder/...)
- 영상/서브폴더 목록을 유튜브 스타일로 나열. 우클릭으로 수정/삭제.
- 영상 ID 인라인 편집 가능 (전체 영상에 걸쳐 고유, 안전한 ID 형태만 허용. 변경하면 공유 URL 의 마지막 세그먼트가 같이 바뀝니다).
- 오른쪽 위 "영상 추가" →
/op/folder/.../video/editor로 이동. 1단계·2단계 폴더 어디서나 영상을 추가할 수 있습니다 (1단계 폴더에는 "하위 폴더 추가" 버튼도 함께 노출). - 2단계 폴더에서만 노출되는 "플레이리스트 추가" 버튼: YouTube 플레이리스트 URL 을 넣으면 항목을 미리 보여주고 시작할 수 있습니다.
- 항목별 썸네일 미리보기(YouTube
mqdefault), 드래그 정렬 (정렬 UI 는 플레이리스트 임포트 화면 한정), 제목 / ID 인라인 편집. - ID 정책 토글:
- 랜덤 — 항목별로 24자 hex (crypto.randomUUID 압축, 폴백은 16진수 24자) 자동 생성.
- 시퀀셜 — 원래 플레이리스트 순서 기준 1, 2, 3, ... 으로 부여. zero-pad 옵션을 켜면 항목 총개수 자릿수에 맞춰
001, 002, ...처럼 패딩.
- "가져오기 시작" 누르면 각 항목이 큐에 들어가고 백그라운드에서 다운로드 + 후처리.
- 항목별 썸네일 미리보기(YouTube
영상추가 화면 (/op/folder/.../video/editor)
- 입력: 컴퓨터 파일 드래그&드롭, 파일 선택 다이얼로그, YouTube URL.
- YouTube URL 의 경우:
- "확인" 버튼으로
probe(title / duration / 예상 파일 크기). probe 가 끝나야 "가져오기" 버튼이 활성화됩니다 (probe 전에 가져오기 누르는 것 자체가 막힘). - 예상 소요시간이 5분 이상으로 추정되면 경고를 띄움.
- 백그라운드 잡이 시작되고, 다른 페이지를 만지더라도 진행 상황이 계속 업데이트됩니다.
- "확인" 버튼으로
- 진행률 바: yt-dlp 가 video / audio 스트림을 순차로 받으면서 매번 0%→100% 로 보고하는 걸 누적해 부드럽게 0→50% 로 climb 합니다. 50→99% 구간은 후처리(60fps 변환 + FHD 캡) 진행률. ffmpeg 가 잠시 진행률을 안 뱉어도 멈춰 보이지 않도록 2초마다 경과시간 메시지가 갱신됩니다.
- 다운로드 해상도는 강제 FHD 캡. 원본이 FHD 이하면 그대로, 초과면 강제로 1920×1080 이하로 다운스케일.
- 다운로드 후 자동 후처리:
- 원본이 60fps 미만이면 fps=60 프레임 복제로 끌어올림.
- 해상도가 FHD 초과면 FHD 로 다운스케일 (종횡비 유지, 짝수 변 보정).
- 위 둘 모두 불필요하면 인코딩 자체를 건너뜁니다.
- 위쪽 navbar 왼쪽에서 영상 이름 수정, 오른쪽 위 "저장" 으로 트림 적용. 원본은 항상 보존, 편집본은
edited.<ext>로 따로 저장. 다시 편집해도 원본에서 시작합니다.
성능 메모
- 60fps/FHD 변환에 호스트 환경에서 사용 가능한 하드웨어 H.264 인코더를 자동 감지해 우선 사용합니다 (NVENC > QSV > VideoToolbox > VAAPI > libx264 ultrafast). 1프레임 lavfi 테스트로 실제 동작을 확인한 뒤 캐시합니다.
- 1프레임 테스트는 통과했지만 실파일 인코딩에서 깨지는 HW 인코더가 있어, 그 잡 한 건에 한해 libx264 ultrafast 로 자동 재시도합니다 (캐시는 그대로 둠 — 다음 잡 입력이 더 일반적이면 HW 가 다시 돕니다).
- 플레이리스트 임포트는 한 번에 너무 많은 yt-dlp 프로세스가 떠 디스크/CPU 를 죽이지 않도록
MAX_DOWNLOAD_CONCURRENCY(기본 2) 로 제한된 풀에서 순차로 시작됩니다. 대기 항목은queued상태로 큐에서 대기.
Description
Languages
TypeScript
47.9%
JavaScript
34.3%
EJS
10%
CSS
7.1%
Dockerfile
0.7%