Compare commits
2 Commits
0abd21533b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692a824e78 | ||
|
|
745b1db1cc |
@@ -306,6 +306,54 @@ opRouter.get(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ─── 목록 뽑기 (영상 ID + 실제 영상 주소 txt) ──────────────────────────
|
||||||
|
|
||||||
|
/** YouTube URL 의 11자 영상 ID 추출. watch?v= / youtu.be / embed / shorts / live, nocookie 도메인 지원. */
|
||||||
|
function extractYoutubeId(url: string): string | null {
|
||||||
|
const m = url.match(
|
||||||
|
/(?:youtu\.be\/|(?:youtube\.com|youtube-nocookie\.com)\/(?:watch\?(?:[^#]*&)?v=|embed\/|shorts\/|live\/|v\/))([\w-]{11})/
|
||||||
|
)
|
||||||
|
return m ? m[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 영상의 "실제 주소". YouTube 는 https://youtu.be/ID 로 간결화, 그 외(네이버 등)는 원본 그대로. */
|
||||||
|
function videoSourceUrl(sourceUrl: string | null): string {
|
||||||
|
if (!sourceUrl) return ''
|
||||||
|
const ytId = extractYoutubeId(sourceUrl)
|
||||||
|
return ytId ? `https://youtu.be/${ytId}` : sourceUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
opRouter.get(
|
||||||
|
'/op/folder/:topName/:subName/list.txt',
|
||||||
|
requireAuth,
|
||||||
|
(req, res, next) => {
|
||||||
|
try {
|
||||||
|
const folder = getFolderByPath(collectFolderSegments(req.params))
|
||||||
|
if (!folder) {
|
||||||
|
res.status(404).send('폴더를 찾을 수 없습니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 실제 외부 주소(유튜브/네이버 등)가 있는 영상만. 업로드 영상처럼 주소가
|
||||||
|
// 없는 항목은 `[ID] [URL]` 두 칸 형식이 깨지므로 목록에서 제외한다.
|
||||||
|
const lines = listVideosInFolder(folder.id)
|
||||||
|
.map((v) => ({ id: v.id, url: videoSourceUrl(v.sourceUrl) }))
|
||||||
|
.filter((r) => r.url !== '')
|
||||||
|
.map((r) => `${r.id} ${r.url}`)
|
||||||
|
const body = lines.join('\n') + (lines.length ? '\n' : '')
|
||||||
|
const baseName = folderPathNames(folder.id).join('_') || 'list'
|
||||||
|
const asciiName = baseName.replace(/[^\x20-\x7e]+/g, '_').replace(/["\\]/g, '_')
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${asciiName}.txt"; filename*=UTF-8''${encodeURIComponent(baseName)}.txt`
|
||||||
|
)
|
||||||
|
res.send(body)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// ─── 플레이리스트 임시저장(draft) ──────────────────────────────────────
|
// ─── 플레이리스트 임시저장(draft) ──────────────────────────────────────
|
||||||
// 다운로드 시작 전 수정해둔 값(URL + 항목별 제목/ID/구간)을 폴더당 1개 저장/복원.
|
// 다운로드 시작 전 수정해둔 값(URL + 항목별 제목/ID/구간)을 폴더당 1개 저장/복원.
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
var parentLabel = isSubFolder ? '← ' + breadcrumb[0] : '← 폴더 목록'
|
var parentLabel = isSubFolder ? '← ' + breadcrumb[0] : '← 폴더 목록'
|
||||||
var editorHref = '/op/folder/' + folderPathEnc + '/video/editor'
|
var editorHref = '/op/folder/' + folderPathEnc + '/video/editor'
|
||||||
var playlistHref = '/op/folder/' + folderPathEnc + '/playlist/new'
|
var playlistHref = '/op/folder/' + folderPathEnc + '/playlist/new'
|
||||||
|
var listTxtHref = '/op/folder/' + folderPathEnc + '/list.txt'
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<main class="pageWrap">
|
<main class="pageWrap">
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
<a class="primaryButton" href="<%= editorHref %>">영상 추가</a>
|
<a class="primaryButton" href="<%= editorHref %>">영상 추가</a>
|
||||||
<% if (isSubFolder) { %>
|
<% if (isSubFolder) { %>
|
||||||
<a class="primaryButton" href="<%= playlistHref %>">플레이리스트 추가</a>
|
<a class="primaryButton" href="<%= playlistHref %>">플레이리스트 추가</a>
|
||||||
|
<a class="primaryButton" href="<%= listTxtHref %>" download>목록 뽑기</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user