openlist增加路径元信息配置分类和自动刷新链接功能
This commit is contained in:
@@ -166,6 +166,16 @@ export interface AdminConfig {
|
||||
ScanInterval?: number; // 定时扫描间隔(分钟),0表示关闭,最低60分钟
|
||||
ScanMode?: 'torrent' | 'name' | 'hybrid'; // 扫描模式:torrent=种子库匹配,name=名字匹配,hybrid=混合模式(默认)
|
||||
DisableVideoPreview?: boolean; // 禁用预览视频,直接返回直连链接
|
||||
/**
|
||||
* 路径元信息(最长前缀匹配 folder 路径)
|
||||
* key: 规范化路径前缀,如 /videos 可匹配 /videos/某影片
|
||||
*/
|
||||
PathMeta?: {
|
||||
[path: string]: {
|
||||
category: string; // 分类名
|
||||
refresh14m: boolean; // 该路径播放时是否启用 14 分钟 URL 续期
|
||||
};
|
||||
};
|
||||
};
|
||||
NetDiskConfig?: {
|
||||
Quark?: {
|
||||
|
||||
@@ -708,6 +708,9 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (adminConfig.OpenListConfig.OfflineDownloadPassword === undefined) {
|
||||
adminConfig.OpenListConfig.OfflineDownloadPassword = '';
|
||||
}
|
||||
if (adminConfig.OpenListConfig.PathMeta === undefined) {
|
||||
adminConfig.OpenListConfig.PathMeta = {};
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息已迁移到新版数据库
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* OpenList 路径元信息工具
|
||||
* PathMeta: { [path]: { category, refresh14m } }
|
||||
* 匹配规则:规范化后最长前缀匹配
|
||||
* 例:配置 /videos 可匹配 /videos/某影片
|
||||
*/
|
||||
|
||||
export interface OpenListPathMetaEntry {
|
||||
category: string;
|
||||
refresh14m: boolean;
|
||||
}
|
||||
|
||||
export type OpenListPathMetaMap = Record<string, OpenListPathMetaEntry>;
|
||||
|
||||
export const EMPTY_PATH_META: OpenListPathMetaEntry = {
|
||||
category: '',
|
||||
refresh14m: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 规范化路径:去 BOM / 零宽字符、trim、去掉末尾 /(根路径 / 除外)
|
||||
*/
|
||||
export function normalizeOpenListPath(path: string): string {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let cleaned = path
|
||||
// UTF-8 BOM
|
||||
.replace(/^/, '')
|
||||
.replace(//g, '')
|
||||
// zero-width chars U+200B-U+200D, U+FEFF
|
||||
.replace(/[-]/g, '')
|
||||
.trim()
|
||||
.replace(/\\/g, '/');
|
||||
|
||||
// 去掉末尾 /(除非就是 /)
|
||||
if (cleaned.length > 1 && cleaned.endsWith('/')) {
|
||||
cleaned = cleaned.slice(0, -1);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 PathMeta map 的 key,合并重复路径(后者覆盖前者)
|
||||
*/
|
||||
export function normalizePathMetaMap(
|
||||
pathMeta: OpenListPathMetaMap | undefined | null
|
||||
): OpenListPathMetaMap {
|
||||
if (!pathMeta || typeof pathMeta !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result: OpenListPathMetaMap = {};
|
||||
for (const [rawPath, entry] of Object.entries(pathMeta)) {
|
||||
const pathKey = normalizeOpenListPath(rawPath);
|
||||
if (!pathKey) continue;
|
||||
result[pathKey] = {
|
||||
category: typeof entry?.category === 'string' ? entry.category.trim() : '',
|
||||
refresh14m: Boolean(entry?.refresh14m),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最长前缀匹配解析路径元信息
|
||||
* - 全等命中
|
||||
* - 或以 key + '/' 为前缀(避免 /videos 误匹配 /videos2)
|
||||
* - 多条命中时取最长 key
|
||||
*/
|
||||
export function resolvePathMeta(
|
||||
folderPath: string,
|
||||
pathMeta: OpenListPathMetaMap | undefined | null
|
||||
): OpenListPathMetaEntry {
|
||||
const normalized = normalizeOpenListPath(folderPath);
|
||||
if (!normalized || !pathMeta) {
|
||||
return { ...EMPTY_PATH_META };
|
||||
}
|
||||
|
||||
const map = normalizePathMetaMap(pathMeta);
|
||||
let bestKey = '';
|
||||
let best: OpenListPathMetaEntry | null = null;
|
||||
|
||||
for (const [key, entry] of Object.entries(map)) {
|
||||
if (!key) continue;
|
||||
const matched =
|
||||
normalized === key ||
|
||||
(key === '/'
|
||||
? normalized.startsWith('/')
|
||||
: normalized.startsWith(key + '/'));
|
||||
if (!matched) continue;
|
||||
if (key.length >= bestKey.length) {
|
||||
bestKey = key;
|
||||
best = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (!best) {
|
||||
return { ...EMPTY_PATH_META };
|
||||
}
|
||||
|
||||
return {
|
||||
category: best.category || '',
|
||||
refresh14m: Boolean(best.refresh14m),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PathMeta 提取非空分类列表(去重、保序)
|
||||
*/
|
||||
export function listPathMetaCategories(
|
||||
pathMeta: OpenListPathMetaMap | undefined | null
|
||||
): string[] {
|
||||
const map = normalizePathMetaMap(pathMeta);
|
||||
const seen = new Set<string>();
|
||||
const categories: string[] = [];
|
||||
for (const entry of Object.values(map)) {
|
||||
const cat = (entry.category || '').trim();
|
||||
if (cat && !seen.has(cat)) {
|
||||
seen.add(cat);
|
||||
categories.push(cat);
|
||||
}
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
@@ -286,6 +286,12 @@ export async function getOpenListDetail(
|
||||
|
||||
// 3. 从 metainfo 中获取元数据
|
||||
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
|
||||
const { resolvePathMeta } = await import('@/lib/openlist-path-meta');
|
||||
// folderName 为 metainfo 中的完整路径,PathMeta 按最长前缀匹配
|
||||
const pathMetaResolved = resolvePathMeta(
|
||||
folderName,
|
||||
openListConfig.PathMeta
|
||||
);
|
||||
|
||||
return {
|
||||
source: 'openlist',
|
||||
@@ -306,6 +312,8 @@ export async function getOpenListDetail(
|
||||
),
|
||||
episodes_titles: episodes.map((ep) => ep.title!),
|
||||
proxyMode: false, // openlist 源不使用代理模式
|
||||
category: pathMetaResolved.category || undefined,
|
||||
refresh14m: pathMetaResolved.refresh14m,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -310,6 +310,10 @@ export interface SearchResult {
|
||||
rating?: number; // 评分
|
||||
initialEpisodeIndex?: number; // 初始集数索引(用于小雅源从文件点击进入时指定集数)
|
||||
metadataSource?: 'folder' | 'nfo' | 'tmdb' | 'file'; // 元数据来源(用于小雅源判断是否保留fileName)
|
||||
/** OpenList 路径元信息:是否启用 14 分钟播放 URL 续期 */
|
||||
refresh14m?: boolean;
|
||||
/** OpenList 路径元信息:分类 */
|
||||
category?: string;
|
||||
}
|
||||
|
||||
// 豆瓣数据结构
|
||||
|
||||
Reference in New Issue
Block a user