openlist增加路径元信息配置分类和自动刷新链接功能

This commit is contained in:
mtvpls
2026-07-18 15:51:30 +08:00
parent f742787044
commit d9e1b343e3
14 changed files with 586 additions and 50 deletions
+10
View File
@@ -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?: {
+3
View File
@@ -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 = {};
}
}
// 用户信息已迁移到新版数据库
+127
View File
@@ -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;
}
+8
View File
@@ -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,
};
}
+4
View File
@@ -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;
}
// 豆瓣数据结构