mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
208 lines
7.8 KiB
TypeScript
208 lines
7.8 KiB
TypeScript
import type { Library } from '../types'
|
||
|
||
export type CloudPlaybackMode = 'strm' | 'redirect_proxy'
|
||
|
||
export function currentOrigin() {
|
||
if (typeof window === 'undefined') return ''
|
||
return window.location.origin.replace(/\/+$/, '')
|
||
}
|
||
|
||
export function isLocalPlaybackBase(raw: string) {
|
||
try {
|
||
const u = new URL(raw)
|
||
const host = u.hostname.toLowerCase()
|
||
return host === 'localhost' || host === '127.0.0.1' || host === '::1'
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
export function preferredSTRMBaseURL(saved: string) {
|
||
const current = currentOrigin()
|
||
const trimmed = saved.trim().replace(/\/+$/, '')
|
||
if (!trimmed) return current
|
||
if (current && isLocalPlaybackBase(trimmed) && trimmed !== current) return current
|
||
return trimmed
|
||
}
|
||
|
||
export function playbackStatusText(
|
||
strmPlaybackEnabled: boolean,
|
||
redirectProxyEnabled: boolean,
|
||
cloudPlaybackMode: CloudPlaybackMode,
|
||
) {
|
||
if (strmPlaybackEnabled && redirectProxyEnabled) {
|
||
return `两者开启 · 优先${cloudPlaybackMode === 'strm' ? 'STRMURL' : '302/反代'}`
|
||
}
|
||
if (strmPlaybackEnabled) return '仅 STRMURL'
|
||
if (redirectProxyEnabled) return '仅 302/反代'
|
||
return '云盘第三方播放关闭'
|
||
}
|
||
|
||
export function suggestedSTRMOutputDir(root: string, library: Library) {
|
||
const base = trimPath(root)
|
||
const subdir = strmLibraryOutputSubdir(library)
|
||
if (!subdir) return base
|
||
if (pathEndsWith(base, subdir) || pathEndsWith(base, pathBase(subdir))) return base
|
||
const tail = missingCategoryTail(base, subdir)
|
||
if (tail) return joinPath(base, tail)
|
||
return joinPath(base || 'data/strm', subdir)
|
||
}
|
||
|
||
export function inferSTRMOutputRoot(saved: string, libraries: Library[]) {
|
||
const normalized = trimPath(saved)
|
||
if (!normalized) return ''
|
||
const slashSaved = toSlash(normalized).toLowerCase()
|
||
for (const library of libraries) {
|
||
const subdir = strmLibraryOutputSubdir(library)
|
||
if (!subdir) continue
|
||
const suffix = `/${toSlash(subdir).toLowerCase()}`
|
||
if (slashSaved.endsWith(suffix)) {
|
||
return trimPath(normalized.slice(0, normalized.length - suffix.length))
|
||
}
|
||
if (slashSaved === toSlash(subdir).toLowerCase()) return ''
|
||
}
|
||
const inferredBySTRMRoot = stripLibraryCategoryAfterSTRMRoot(normalized)
|
||
if (inferredBySTRMRoot) return inferredBySTRMRoot
|
||
return normalized
|
||
}
|
||
|
||
export function strmLibraryOutputSubdir(library: Library) {
|
||
const parts = categoryPartsFromPath(libraryPathParts(library.path))
|
||
?? categoryPartsFromPath(nameParts(library.name))
|
||
?? mediaTypeRootParts(library.type)
|
||
?? []
|
||
return joinPath('', ...parts.map(sanitizePathPart).filter(Boolean))
|
||
}
|
||
|
||
function categoryPartsFromPath(parts: string[]) {
|
||
for (let index = 0; index < parts.length; index += 1) {
|
||
const part = parts[index]
|
||
const root = canonicalRoot(part)
|
||
if (root) return [root, ...parts.slice(index + 1)]
|
||
const categoryRoot = categoryRootFor(part)
|
||
if (categoryRoot) return [categoryRoot, canonicalCategory(part)]
|
||
}
|
||
return null
|
||
}
|
||
|
||
function libraryPathParts(raw: string) {
|
||
const cloudParts = cloudDisplayPathParts(raw)
|
||
if (cloudParts.length > 0) return cloudParts
|
||
const cleaned = toSlash(raw).replace(/^[A-Za-z]:/, '').replace(/^\/+|\/+$/g, '')
|
||
return splitPath(cleaned)
|
||
}
|
||
|
||
function cloudDisplayPathParts(raw: string) {
|
||
if (!/^cloud:\/\//i.test(raw.trim())) return []
|
||
try {
|
||
const url = new URL(raw)
|
||
return splitPath(decodeURIComponent(url.pathname.replace(/^\/+|\/+$/g, '')))
|
||
} catch {
|
||
const rest = raw.trim().replace(/^cloud:\/\/[^/]+\/?/i, '').split('?')[0] ?? ''
|
||
return splitPath(decodeURIComponent(rest.replace(/^\/+|\/+$/g, '')))
|
||
}
|
||
}
|
||
|
||
function nameParts(name: string) {
|
||
return splitPath(name.replace(/[·>||\\]/g, '/'))
|
||
}
|
||
|
||
function mediaTypeRootParts(type: string) {
|
||
const normalized = type.trim().toLowerCase()
|
||
if (normalized === 'movie') return ['电影']
|
||
if (normalized === 'tv' || normalized === 'variety') return ['电视剧']
|
||
if (normalized === 'anime') return ['动漫']
|
||
if (normalized === 'adult') return ['成人']
|
||
return null
|
||
}
|
||
|
||
function canonicalRoot(part: string) {
|
||
const key = part.trim().toLowerCase()
|
||
if (['电影', 'movie', 'movies', 'film', 'films'].includes(key)) return '电影'
|
||
if (['电视剧', '剧集', 'tv', 'tvs', 'series', 'show', 'shows'].includes(key)) return '电视剧'
|
||
if (['动漫', '动画', 'anime', 'bangumi'].includes(key)) return '动漫'
|
||
if (['成人', 'adult', 'adults', 'jav', 'nsfw', '9kg'].includes(key)) return '成人'
|
||
return ''
|
||
}
|
||
|
||
function categoryRootFor(part: string) {
|
||
const key = part.trim().toLowerCase()
|
||
if (['演唱会', '音乐会', '纪录片', '纪录', '动画电影', '动漫电影', '华语电影', '国产电影', '外语电影', '外国电影', '欧美电影', '日韩电影', '日本电影', '韩国电影'].includes(key)) return '电影'
|
||
if (['国产剧', '欧美剧', '日韩剧', '日剧', '韩剧', '综艺', '真人秀', '儿童', '少儿', '未分类'].includes(key)) return '电视剧'
|
||
if (['国漫', '国产动漫', '日番', '番剧', '日漫', '日本动漫', '日本动画', '韩漫', '韩国动漫', '韩国动画', '美漫', '欧美动漫', '欧美动画', '西方动画', '其他', '其他动漫', '其它动漫'].includes(key)) return '动漫'
|
||
if (key === '番号') return '成人'
|
||
return ''
|
||
}
|
||
|
||
function canonicalCategory(part: string) {
|
||
const key = part.trim().toLowerCase()
|
||
if (key === '音乐会') return '演唱会'
|
||
if (key === '纪录') return '纪录片'
|
||
if (key === '动漫电影') return '动画电影'
|
||
if (key === '国产电影') return '华语电影'
|
||
if (key === '外语电影' || key === '外国电影') return '欧美电影'
|
||
if (key === '日本电影' || key === '韩国电影') return '日韩电影'
|
||
if (key === '日剧' || key === '韩剧') return '日韩剧'
|
||
if (key === '真人秀') return '综艺'
|
||
if (key === '少儿') return '儿童'
|
||
if (key === '未分类') return '欧美剧'
|
||
if (key === '国产动漫') return '国漫'
|
||
if (['番剧', '日漫', '日本动漫', '日本动画'].includes(key)) return '日番'
|
||
if (key === '韩国动漫' || key === '韩国动画') return '韩漫'
|
||
if (['欧美动漫', '欧美动画', '西方动画'].includes(key)) return '美漫'
|
||
if (key === '其他动漫' || key === '其它动漫') return '其他'
|
||
if (key === '番号') return '成人'
|
||
return part.trim()
|
||
}
|
||
|
||
function splitPath(raw: string) {
|
||
return raw.split('/').map((part) => part.trim()).filter((part) => part && part !== '.')
|
||
}
|
||
|
||
function stripLibraryCategoryAfterSTRMRoot(raw: string) {
|
||
const normalized = toSlash(raw)
|
||
const parts = normalized.split('/')
|
||
let strmIndex = -1
|
||
for (let index = 0; index < parts.length; index += 1) {
|
||
if (parts[index].trim().toLowerCase() === 'strm') strmIndex = index
|
||
}
|
||
if (strmIndex < 0 || strmIndex >= parts.length - 1) return ''
|
||
const tail = parts.slice(strmIndex + 1).filter((part) => part.trim())
|
||
const categoryParts = categoryPartsFromPath(tail)
|
||
if (!categoryParts || categoryParts.length < 2) return ''
|
||
return trimPath(parts.slice(0, strmIndex + 1).join('/'))
|
||
}
|
||
|
||
function joinPath(root: string, ...parts: string[]) {
|
||
return [trimPath(root), ...parts.map(trimPath)].filter(Boolean).join('/')
|
||
}
|
||
|
||
function missingCategoryTail(base: string, subdir: string) {
|
||
const parts = splitPath(subdir)
|
||
if (parts.length < 2 || !pathEndsWith(base, parts[0])) return ''
|
||
return joinPath('', ...parts.slice(1))
|
||
}
|
||
|
||
function pathEndsWith(pathValue: string, suffix: string) {
|
||
const path = trimPath(pathValue).toLowerCase()
|
||
const tail = trimPath(suffix).toLowerCase()
|
||
return !!path && !!tail && (path === tail || path.endsWith(`/${tail}`))
|
||
}
|
||
|
||
function pathBase(pathValue: string) {
|
||
const parts = splitPath(pathValue)
|
||
return parts[parts.length - 1] || ''
|
||
}
|
||
|
||
function trimPath(raw: string) {
|
||
return toSlash(raw).replace(/\/+$/g, '').trim()
|
||
}
|
||
|
||
function toSlash(raw: string) {
|
||
return raw.trim().replace(/\\/g, '/')
|
||
}
|
||
|
||
function sanitizePathPart(part: string) {
|
||
return part.replace(/[/:*?"<>|\\]/g, ' ').trim()
|
||
}
|