플레이리스트 가져오기 화면에 다음을 추가:
- 항목별 제목 입력을 절반으로 줄이고 오른쪽에 시작/종료 시간 입력 추가
(기본 시작 0, 종료는 영상 길이)
- 정렬 옵션 옆에 일괄 시작/종료 입력과 적용/초기화 버튼 추가
(초기화 = 시작 0, 종료 = 영상 길이)
- 항목 썸네일 클릭 시 YouTube 임베드로 미리보기 재생
- 항목 우클릭 → 자르기 메뉴 → /video/editor 식 타임라인 모달(YouTube IFrame)
에서 구간을 정하면 항목의 시작/종료 입력에 반영
- 백엔드: playlist/start 가 항목별 startSec/endSec 를 받아 다운로드 잡에
실어 보내고, 60fps 후처리 후 ffmpeg 로 해당 구간을 잘라 편집본 생성
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
플레이리스트 가져오기 미리보기 목록에 각 항목 썸네일을 표시한다.
YouTube video id 로 mqdefault 썸네일 URL 을 만들고, id 가 없으면
yt-dlp 가 준 thumbnail 필드로 폴백. 깨진 이미지는 빈 박스 처리.
- youtube.ts: PlaylistEntry 에 thumbnailUrl 추가 + flat-playlist 파싱에서 채움
- playlist.js: probe 매핑에 thumbnailUrl 반영, 각 행에 img 렌더(lazy + onerror)
- styles.css: 행 그리드에 썸네일 칼럼(96px 16:9) 추가
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
simplify skill 결과 정리:
- editor.ts: HW→libx264 잡 폴백 패턴을 encodeWithFallback() 한 곳으로
모으고 upscale/trim 양쪽에서 호출. 진행률 무전송 변형은 runner 인자로 분기.
- editor.ts: upscaleOriginalTo60Fps 가 fps/해상도/길이를 위해 spawnSync
ffprobe 를 3번 돌리던 것을 probeVideoMeta() 한 번 호출로 통합. 핫패스에서
이벤트 루프 블로킹을 줄임. 이제 미사용이 된 probeVideoResolution/
probeVideoDuration 은 제거.
- routes/helpers.ts: op.ts/public.ts 양쪽에 동일하게 있던
collectFolderSegments 를 공용 모듈로 추출.
- youtube.ts: probeYoutube/probeYoutubePlaylist 의 stderr 누적을
runFfmpeg 와 동일하게 마지막 2KB 만 유지하도록 캡.
README.md 도 5/31 STEP 1~5 (중첩 폴더 + 영상 ID 편집 + 플레이리스트
임포트 + 짧은 공유 URL) 과 5/16 묶음(부드러운 진행바 + 60fps/FHD 캡 +
HW 인코더 자동 감지) 까지 반영해 전반 갱신.
`npm run build` 통과.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5/16 사용자 보고 "진행률이 0 → 50% 한방에 튀고 60fps 변환에서 하루종일 멈춤"
의 두 가지 직접 원인을 잡습니다.
1) yt-dlp 가 `bv*+ba` 로 영상/오디오 두 스트림을 순서대로 받을 때,
기존 `Math.max(prev, pct)` 누적 방식은 첫 스트림이 100% 도달하면
매핑값이 50% 에 박혀 둘째 스트림 진행이 사용자에게 보이지 않았다.
스트림 카운트 + 가중평균 방식으로 바꿔 전체 다운로드 phase 가
부드럽게 0..50% 로 차오르게 한다. (검증: 5%, 13%, 25%, 38%, 50%,
65%, 80%, 100% 의 8단계 클라이밍을 60ms 간격으로 관찰)
2) 60fps/FHD 후처리 단계가 `-progress pipe:1` 라인을 전혀 못 받는
경우(영상 길이 unknown, 1초 미만 짧은 클립 등) progress 가 50% 에
고정돼 사용자는 "멈췄다"고 인식했다. heartbeat interval(2s) 로
메시지에 경과시간을 박아 화면이 살아있다는 신호를 준다. progress
막대 자체는 실제 ffmpeg 데이터가 들어올 때만 움직이며, 거짓
증가는 하지 않는다.
플레이리스트 일괄 등록 시 모든 항목이 즉시 yt-dlp+ffmpeg 를 띄워
서버를 박살내던 문제 수정. `startYoutubeDownload` 는 더 이상 직접
runJob 을 setImmediate 로 띄우지 않고, `queued` 상태로만 등록한
뒤 `pump()` 가 활성 잡 수를 보고 슬롯이 비었을 때만 launchJob
한다. 완료/실패 시 `finally` 에서 카운터를 줄이고 다음 큐를 펌핑.
기본 동시 실행 한도는 2 (네트워크 다운로드 + CPU 후처리가 한쌍은
같이 굴러갈 만한 선). `MAX_DOWNLOAD_CONCURRENCY=N` 환경변수로 조정.
스모크 (yt-dlp 스텁 + 3s sleep):
- MAX=1, 3건 등록 → t+0.5s: 1 downloading + 2 queued → t+4s:
1 done + 1 downloading + 1 queued → t+10s: 3 done
- MAX=2 (기본), 4건 등록 → t+0.5s: 2 downloading + 2 queued → t+8s: 4 done
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
영상 단건 import 와 동일한 패턴으로 2단계 폴더 전용 endpoint 두 개를
추가했다.
- src/youtube.ts: `probeYoutubePlaylist(url)` — yt-dlp 의
`--flat-playlist --dump-json` 으로 한 줄당 한 항목을 받아
`{ url, title, durationSec, originalIndex }` 배열로 정리한다.
내부 컨테이너 라인(`_type==='playlist'`) 은 스킵.
- src/routes/op.ts:
- POST `/op/folder/:topName/:subName/playlist/probe`
URL 만 받고 위 함수를 호출해서 entries 를 돌려준다.
- POST `/op/folder/:topName/:subName/playlist/start`
UI 가 큐레이션한 `[{ url, title, videoId }]` 를 받아
`isSafeVideoId` 형식 검사, 요청 내 중복 검사, DB 충돌 검사를
순서대로 한 뒤 항목별로 `startYoutubeDownload` 를 호출.
두 endpoint 모두 1단계 폴더 path 는 router 매칭 자체에서 빠져
(sub 만 등록) 자연스럽게 404 가 난다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User wants YouTube imports capped at FHD: below-FHD sources stay as-is,
above-FHD sources are forcefully downscaled to FHD preserving aspect.
Two layers:
- yt-dlp `-f bv*[height<=1080]+ba/b[height<=1080]/bv*+ba/b` strictly
picks ≤1080 formats when available; falls back to anything otherwise.
- ffmpeg post-process: probeVideoResolution checks output; if >FHD,
add scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease
to the existing 60fps vfilter chain (no extra encode pass — combined
into the one we already run). trunc(/2)*2 ensures even dims for libx264.
The post-process now triggers when either (fps<60) OR (res>FHD), so a
60fps 4K source that previously skipped re-encoding will now get
downscaled.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
세 가지 사용자 보고 처리:
1) 다운로드 바가 멈춰 있다가 한번에 50% 로 점프
- 원인: yt-dlp 는 파이썬 스크립트라 stdout 이 pipe 로 연결되면
block-buffered 가 되어 진행률 라인들이 4KB 버퍼에 모였다가 종료 직전에
쏟아짐. node 의 stdout 'data' 핸들러가 그 전엔 아무 것도 못 봄.
- 수정: spawn 의 env 에 PYTHONUNBUFFERED=1 추가. 라인 단위로 즉시 flush.
2) 변환을 20배 빠르게
- 원인: minterpolate(mci) 는 motion estimation 자체가 무거워 mci 기본값
이어도 영상 길이의 수 배 시간이 든다 (blend 도 5~10배가 한계).
- 수정: 다운로드 후 60fps 변환과 trim 재인코딩 양쪽에서 minterpolate 를
`fps=60` (단순 프레임 복제) 으로 교체. preset 도 `veryfast` → `ultrafast`.
실측상 영상 길이의 1/3 ~ 1배 시간 — mci 대비 수십 배 빠름.
- 시각적 부드러움은 30fps 와 동일하지만 컨테이너/타이밍은 60fps cfr 로 유지.
사용자가 알려준 "timing 만 60fps 로 바뀌고 실제 부드러움은 그대로" 경로.
3) "확인" 전 "가져오기" 재클릭 막기
- probe 클릭 시 ytProbeBtn 도 disabled.
- probe 실패 / 5분 경고 취소 / start 실패 시만 재활성화.
- 정상 흐름은 probe 성공 → 확인 클릭 → 다운로드 → redirect 라 재활성화
필요 없음. ytStartBtn 도 클릭 직후 disabled 로 중복 클릭 방지.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
다운로드 후 바가 한 번에 99% 로 튄 뒤 멈춰 있던 문제 두 가지를 같이 고침.
1) 진행률 구간 분리
- 다운로드: 0 ~ 50% (yt-dlp 의 0-100% 를 절반으로 매핑)
- 60fps 변환: 50 ~ 99%
- yt-dlp 가 video+audio 두 스트림을 받으면 각 스트림이 0→100% 를
반복하므로 단조 증가만 인정 (downloadPctRaw = max(...)).
2) ffmpeg minterpolate 진행률 실시간 보고
- editor.ts 에 `probeVideoDuration` 추가, `runFfmpegWithProgress`
도입해 ffmpeg `-progress pipe:1` 의 out_time_us/ms 를 파싱.
- `upscaleOriginalTo60Fps` 에 `onProgress(pct)` 콜백 추가.
- youtube.ts 가 콜백을 받아 50~99% 로 매핑하고 job.message 를
"60fps 변환 NN%" 로 갱신. persistJob 은 2초 throttle.
3) mci 옵션 단순화로 속도 개선
- 기존: `mci:mc_mode=aobmc:me_mode=bidir` (가장 느린 조합)
- 현재: `mci` 기본값 (obmc + bilat). 화질 약간 양보, 속도 수 배 개선.
- 여전히 mci 자체가 무거워 5분 30fps 영상 변환에 수십 분 단위 시간이
걸릴 수 있음 — 이제 그 동안 진행률은 실시간으로 움직임.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review P1: yt-dlp 가 fps 1순위로 정렬해 가져와도, 영상 자체에 60fps
포맷이 없는 경우 original.* 이 30fps 그대로 저장되어 "원본도 60fps 로
받아줘" 요청과 어긋났습니다.
다운로드 완료 직후 후처리 단계를 추가:
- editor.ts 에 `upscaleOriginalTo60Fps(dir, inputName)` 노출
· ffprobe 로 source fps 측정
· ≥60fps 면 그대로 두고 inputName 반환
· <60fps 면 minterpolate(mci, aobmc, bidir) 로 60fps 까지 끌어올려
`original.mp4` 로 저장하고 기존 파일 제거
· ffmpeg/ffprobe 없거나 보간 실패하면 원본 그대로 유지 (다운로드 살림)
- youtube.ts `runJob` 마지막에 이 함수를 호출하고, 새 파일명이 돌아오면
meta.originalFile 도 업데이트. 후처리 중 진행률을 99% 로 표시하고
실패해도 다운로드 자체는 성공으로 마감.
이로써 편집기에서 다시 보간할 일도 없어집니다 (이미 60fps 원본).
편집 코드 쪽 보호 분기는 직접 업로드 경로용 안전망으로 그대로 둡니다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- youtube.ts: yt-dlp 에 `-S 'fps,res,br,ext'` 추가. 기본 정렬은 fps 를
가중치로 안 써서 60fps 가 있어도 30fps 를 잡아오는 일이 잦았는데,
이제 fps 1순위로 정렬해 가능한 한 부드러운 원본을 받는다. 60fps 가
아예 없는 영상은 자연스럽게 그 영상의 최고 fps 로 폴백.
- editor.ts: 편집본은 항상 60fps 이상이 되도록 보장.
· ffprobe 로 원본 fps 확인
· ≥60fps 이면 기존대로 stream copy 로 빠르게 trim
· <60fps 이면 minterpolate(mci, aobmc, bidir) 로 모션 보간해 60fps 로
재인코딩. mci 는 느리지만 단순 프레임 복제보다 모션이 훨씬 자연스러움.
· ffprobe 실패 시(확인 불가) 기존 동작 유지(stream copy → 재인코딩 폴백).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>