feat: 연령제한 영상용 YouTube 쿠키(cookies.txt) 등록 + yt-dlp --cookies 적용
연령 제한("Sign in to confirm your age") 영상이 플레이리스트 임포트에서
실패하던 문제 해결. 관리자 대시보드에서 Netscape cookies.txt 를 등록하면
단일/플레이리스트 probe 와 실제 다운로드에 모두 --cookies 로 적용된다.
쿠키는 data/cookies.txt 에 0600 으로 저장(git 제외). 연령/로그인 실패 시
yt-dlp 원문 대신 쿠키 등록을 안내하는 메시지를 표시.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,9 @@ import {
|
||||
} from '../youtube.js'
|
||||
import { FfmpegUnavailableError, applyTrimToVideo } from '../editor.js'
|
||||
import { getToolsStatus, updateFfmpeg, updateYtDlp } from '../tools.js'
|
||||
import { cookiesStatus, saveCookies, deleteCookies } from '../cookies.js'
|
||||
import { collectFolderSegments } from './helpers.js'
|
||||
import { promises as fs } from 'node:fs'
|
||||
|
||||
export const opRouter = Router()
|
||||
|
||||
@@ -62,6 +64,12 @@ const upload = multer({
|
||||
limits: { fileSize: uploadMaxBytes }
|
||||
})
|
||||
|
||||
// 쿠키 파일은 작다 — 별도 multer 로 1 MiB 로 제한해 대형 파일 오용을 막는다.
|
||||
const cookieUpload = multer({
|
||||
dest: tmpDir,
|
||||
limits: { fileSize: 1024 * 1024 }
|
||||
})
|
||||
|
||||
function pickStr(v: unknown): string {
|
||||
if (Array.isArray(v)) return typeof v[0] === 'string' ? v[0] : ''
|
||||
return typeof v === 'string' ? v : ''
|
||||
@@ -145,6 +153,34 @@ opRouter.post('/op/tools/:name/update', requireAuth, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ─── YouTube 로그인 쿠키 (연령 제한 영상 다운로드용) ───────────────────────
|
||||
opRouter.get('/op/cookies', requireAuth, (req, res) => {
|
||||
res.json({ ok: true, cookies: cookiesStatus() })
|
||||
})
|
||||
|
||||
opRouter.post('/op/cookies', requireAuth, cookieUpload.single('file'), async (req, res) => {
|
||||
const file = req.file
|
||||
try {
|
||||
if (!file) throw new Error('cookies.txt 파일을 선택해 주세요.')
|
||||
const content = await fs.readFile(file.path, 'utf8')
|
||||
const status = await saveCookies(content)
|
||||
res.json({ ok: true, cookies: status })
|
||||
} catch (err) {
|
||||
res.status(400).json({ ok: false, message: (err as Error).message })
|
||||
} finally {
|
||||
if (file) await fs.rm(file.path, { force: true }).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
opRouter.post('/op/cookies/delete', requireAuth, async (req, res) => {
|
||||
try {
|
||||
await deleteCookies()
|
||||
res.json({ ok: true, cookies: cookiesStatus() })
|
||||
} catch (err) {
|
||||
res.status(400).json({ ok: false, message: (err as Error).message })
|
||||
}
|
||||
})
|
||||
|
||||
function folderViewHandler(req: Request, res: Response, next: NextFunction): void {
|
||||
try {
|
||||
const segments = collectFolderSegments(req.params)
|
||||
|
||||
Reference in New Issue
Block a user