变更私人影库的key值方式

This commit is contained in:
mtvpls
2025-12-26 21:13:12 +08:00
parent df78abaf11
commit 3050cd48a9
15 changed files with 120 additions and 84 deletions

View File

@@ -30,7 +30,7 @@
"@upstash/redis": "^1.25.0",
"@vidstack/react": "^1.12.13",
"anime4k-webgpu": "^1.0.0",
"artplayer": "^5.2.5",
"artplayer": "^5.3.0",
"artplayer-plugin-danmuku": "^5.2.0",
"bs58": "^6.0.0",
"cheerio": "^1.1.2",

View File

@@ -2959,7 +2959,7 @@ const OpenListConfigComponent = ({
});
};
const handleDeleteVideo = async (folder: string, title: string) => {
const handleDeleteVideo = async (key: string, title: string) => {
// 显示确认对话框,直接在 onConfirm 中执行删除操作
showAlert({
type: 'warning',
@@ -2971,7 +2971,7 @@ const OpenListConfigComponent = ({
const response = await fetch('/api/openlist/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folder }),
body: JSON.stringify({ key }),
});
if (!response.ok) {
@@ -3295,7 +3295,7 @@ const OpenListConfigComponent = ({
{video.failed ? '立即纠错' : '纠错'}
</button>
<button
onClick={() => handleDeleteVideo(video.folder, video.title)}
onClick={() => handleDeleteVideo(video.id, video.title)}
className={buttonStyles.dangerSmall}
>
@@ -3331,7 +3331,7 @@ const OpenListConfigComponent = ({
<CorrectDialog
isOpen={correctDialogOpen}
onClose={() => setCorrectDialogOpen(false)}
folder={selectedVideo.folder}
videoKey={selectedVideo.id}
currentTitle={selectedVideo.title}
onCorrect={handleCorrectSuccess}
/>

View File

@@ -307,12 +307,12 @@ async function handleOpenListProxy(request: NextRequest) {
if (wd) {
const results = Object.entries(metaInfo.folders)
.filter(
([folderName, info]) =>
folderName.toLowerCase().includes(wd.toLowerCase()) ||
([key, info]) =>
info.folderName.toLowerCase().includes(wd.toLowerCase()) ||
info.title.toLowerCase().includes(wd.toLowerCase())
)
.map(([folderName, info]) => ({
vod_id: folderName,
.map(([key, info]) => ({
vod_id: key,
vod_name: info.title,
vod_pic: getTMDBImageUrl(info.poster_path),
vod_remarks: info.media_type === 'movie' ? '电影' : '剧集',
@@ -333,8 +333,8 @@ async function handleOpenListProxy(request: NextRequest) {
// 详情模式
if (ids) {
const folderName = ids;
const info = metaInfo.folders[folderName];
const key = ids;
const info = metaInfo.folders[key];
if (!info) {
return NextResponse.json(
@@ -343,6 +343,8 @@ async function handleOpenListProxy(request: NextRequest) {
);
}
const folderName = info.folderName;
// 获取视频详情
try {
const detailResponse = await fetch(
@@ -376,7 +378,7 @@ async function handleOpenListProxy(request: NextRequest) {
total: 1,
list: [
{
vod_id: folderName,
vod_id: key,
vod_name: info.title,
vod_pic: getTMDBImageUrl(info.poster_path),
vod_remarks: info.media_type === 'movie' ? '电影' : '剧集',
@@ -399,8 +401,8 @@ async function handleOpenListProxy(request: NextRequest) {
// 默认返回所有视频
const results = Object.entries(metaInfo.folders).map(
([folderName, info]) => ({
vod_id: folderName,
([key, info]) => ({
vod_id: key,
vod_name: info.title,
vod_pic: getTMDBImageUrl(info.poster_path),
vod_remarks: info.media_type === 'movie' ? '电影' : '剧集',

View File

@@ -37,10 +37,10 @@ export async function GET(request: NextRequest) {
}
const rootPath = openListConfig.RootPath || '/';
const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${id}`;
// 1. 读取 metainfo 获取元数据
let metaInfo: any = null;
let folderMeta: any = null;
try {
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
const { db } = await import('@/lib/db');
@@ -54,10 +54,20 @@ export async function GET(request: NextRequest) {
setCachedMetaInfo(rootPath, metaInfo);
}
}
// 使用 key 查找文件夹信息
folderMeta = metaInfo?.folders?.[id];
if (!folderMeta) {
throw new Error('未找到该视频信息');
}
} catch (error) {
// 忽略错误
throw new Error('读取视频信息失败: ' + (error as Error).message);
}
// 使用 folderName 构建实际路径
const folderName = folderMeta.folderName;
const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${folderName}`;
// 2. 直接调用 OpenList 客户端获取视频列表
const { OpenListClient } = await import('@/lib/openlist.client');
const { getCachedVideoInfo, setCachedVideoInfo } = await import('@/lib/openlist-cache');
@@ -71,24 +81,6 @@ export async function GET(request: NextRequest) {
let videoInfo = getCachedVideoInfo(folderPath);
if (!videoInfo) {
try {
const videoinfoPath = `${folderPath}/videoinfo.json`;
const fileResponse = await client.getFile(videoinfoPath);
if (fileResponse.code === 200 && fileResponse.data.raw_url) {
const contentResponse = await fetch(fileResponse.data.raw_url);
const content = await contentResponse.text();
videoInfo = JSON.parse(content);
if (videoInfo) {
setCachedVideoInfo(folderPath, videoInfo);
}
}
} catch (error) {
// 忽略错误
}
}
const listResponse = await client.listDirectory(folderPath);
if (listResponse.code !== 200) {
@@ -138,19 +130,18 @@ export async function GET(request: NextRequest) {
.sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName));
// 3. 从 metainfo 中获取元数据
const folderMeta = metaInfo?.folders?.[id];
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
const result = {
source: 'openlist',
source_name: '私人影库',
id: id,
title: folderMeta?.title || id,
title: folderMeta?.title || folderName,
poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '',
year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '',
douban_id: 0,
desc: folderMeta?.overview || '',
episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(id)}&fileName=${encodeURIComponent(ep.fileName)}`),
episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(folderName)}&fileName=${encodeURIComponent(ep.fileName)}`),
episodes_titles: episodes.map((ep) => ep.title),
};

View File

@@ -28,7 +28,7 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const {
folder,
key,
tmdbId,
title,
posterPath,
@@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
seasonName,
} = body;
if (!folder || !tmdbId) {
if (!key || !tmdbId) {
return NextResponse.json(
{ error: '缺少必要参数' },
{ status: 400 }
@@ -97,8 +97,20 @@ export async function POST(request: NextRequest) {
);
}
// 检查 key 是否存在
if (!metaInfo.folders[key]) {
return NextResponse.json(
{ error: '视频不存在' },
{ status: 404 }
);
}
// 保留原始文件夹名称
const folderName = metaInfo.folders[key].folderName;
// 更新视频信息
metaInfo.folders[folder] = {
metaInfo.folders[key] = {
folderName: folderName,
tmdb_id: tmdbId,
title: title,
poster_path: posterPath,

View File

@@ -28,10 +28,10 @@ export async function POST(request: NextRequest) {
// 获取请求参数
const body = await request.json();
const { folder } = body;
const { key } = body;
if (!folder) {
return NextResponse.json({ error: '缺少 folder 参数' }, { status: 400 });
if (!key) {
return NextResponse.json({ error: '缺少 key 参数' }, { status: 400 });
}
// 获取配置
@@ -62,16 +62,16 @@ export async function POST(request: NextRequest) {
const metaInfo: MetaInfo = JSON.parse(metainfoContent);
// 检查文件夹是否存在
if (!metaInfo.folders[folder]) {
// 检查 key 是否存在
if (!metaInfo.folders[key]) {
return NextResponse.json(
{ error: '未找到该视频记录' },
{ status: 404 }
);
}
// 删除文件夹记录
delete metaInfo.folders[folder];
// 删除记录
delete metaInfo.folders[key];
// 保存到数据库
const updatedMetainfoContent = JSON.stringify(metaInfo);

View File

@@ -125,16 +125,10 @@ export async function GET(request: NextRequest) {
const allVideos = Object.entries(metaInfo.folders)
.filter(([, info]) => includeFailed || !info.failed) // 根据参数过滤失败的视频
.map(
([folderName, info]) => {
// 构建 id如果是第二季及以后id 也要包含季度信息
let videoId = folderName;
if (info.season_number && info.season_number > 1 && info.season_name) {
videoId = `${folderName} ${info.season_name}`;
}
([key, info]) => {
return {
id: videoId,
folder: folderName,
id: key,
folder: info.folderName,
tmdbId: info.tmdb_id,
title: info.title,
poster: getTMDBImageUrl(info.poster_path),

View File

@@ -48,15 +48,6 @@ export async function POST(request: NextRequest) {
openListConfig.Password
);
// 删除 videoinfo.json
const videoinfoPath = `${folderPath}/videoinfo.json`;
try {
await client.deleteFile(videoinfoPath);
} catch (error) {
console.log('videoinfo.json 不存在或删除失败');
}
// 清除缓存
invalidateVideoInfoCache(folderPath);

View File

@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { generateFolderKey } from '@/lib/crypto';
import { db } from '@/lib/db';
import { OpenListClient } from '@/lib/openlist.client';
import {
@@ -188,6 +189,15 @@ async function performScan(
let existingCount = 0;
let errorCount = 0;
// 收集已存在的 key用于冲突检测
const existingKeys = new Set<string>(Object.keys(metaInfo.folders));
// 创建文件夹名到 key 的映射(用于查找已存在的文件夹)
const folderNameToKey = new Map<string, string>();
for (const [key, info] of Object.entries(metaInfo.folders)) {
folderNameToKey.set(info.folderName, key);
}
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
@@ -195,11 +205,15 @@ async function performScan(
updateScanTaskProgress(taskId, i + 1, folders.length, folder.name);
// 如果是立即扫描(不清空 metainfo且文件夹已存在跳过
if (!clearMetaInfo && metaInfo.folders[folder.name]) {
if (!clearMetaInfo && folderNameToKey.has(folder.name)) {
existingCount++;
continue;
}
// 生成文件夹的 key
const folderKey = generateFolderKey(folder.name, existingKeys);
existingKeys.add(folderKey);
try {
// 解析文件夹名称,提取季度信息和年份
const seasonInfo = parseSeasonFromTitle(folder.name);
@@ -221,6 +235,7 @@ async function performScan(
// 基础信息
const folderInfo: any = {
folderName: folder.name, // 保存原始文件夹名称
tmdb_id: result.id,
title: result.title || result.name || folder.name,
poster_path: result.poster_path,
@@ -275,11 +290,12 @@ async function performScan(
}
}
metaInfo.folders[folder.name] = folderInfo;
metaInfo.folders[folderKey] = folderInfo;
newCount++;
} else {
// 记录失败的文件夹
metaInfo.folders[folder.name] = {
metaInfo.folders[folderKey] = {
folderName: folder.name, // 保存原始文件夹名称
tmdb_id: 0,
title: folder.name,
poster_path: null,
@@ -298,7 +314,8 @@ async function performScan(
} catch (error) {
console.error(`[OpenList Refresh] 处理文件夹失败: ${folder.name}`, error);
// 记录失败的文件夹
metaInfo.folders[folder.name] = {
metaInfo.folders[folderKey] = {
folderName: folder.name, // 保存原始文件夹名称
tmdb_id: 0,
title: folder.name,
poster_path: null,

View File

@@ -72,13 +72,13 @@ export async function GET(request: NextRequest) {
if (metaInfo && metaInfo.folders) {
openlistResults = Object.entries(metaInfo.folders)
.filter(([folderName, info]: [string, any]) => {
const matchFolder = folderName.toLowerCase().includes(query.toLowerCase());
.filter(([key, info]: [string, any]) => {
const matchFolder = info.folderName.toLowerCase().includes(query.toLowerCase());
const matchTitle = info.title.toLowerCase().includes(query.toLowerCase());
return matchFolder || matchTitle;
})
.map(([folderName, info]: [string, any]) => ({
id: folderName,
.map(([key, info]: [string, any]) => ({
id: key,
source: 'openlist',
source_name: '私人影库',
title: info.title,

View File

@@ -109,13 +109,13 @@ export async function GET(request: NextRequest) {
if (metaInfo && metaInfo.folders) {
const openlistResults = Object.entries(metaInfo.folders)
.filter(([folderName, info]: [string, any]) => {
const matchFolder = folderName.toLowerCase().includes(query.toLowerCase());
.filter(([key, info]: [string, any]) => {
const matchFolder = info.folderName.toLowerCase().includes(query.toLowerCase());
const matchTitle = info.title.toLowerCase().includes(query.toLowerCase());
return matchFolder || matchTitle;
})
.map(([folderName, info]: [string, any]) => ({
id: folderName,
.map(([key, info]: [string, any]) => ({
id: key,
source: 'openlist',
source_name: '私人影库',
title: info.title,

View File

@@ -63,8 +63,8 @@ export default function PrivateLibraryPage() {
};
const handleVideoClick = (video: Video) => {
// 跳转到播放页面
router.push(`/play?source=openlist&id=${encodeURIComponent(video.folder)}`);
// 跳转到播放页面,使用 idkey而不是 folder
router.push(`/play?source=openlist&id=${encodeURIComponent(video.id)}`);
};
return (
@@ -106,7 +106,7 @@ export default function PrivateLibraryPage() {
{videos.map((video) => (
<VideoCard
key={video.id}
id={video.folder}
id={video.id}
source='openlist'
title={video.title}
poster={video.poster}

View File

@@ -35,7 +35,7 @@ interface TMDBSeason {
interface CorrectDialogProps {
isOpen: boolean;
onClose: () => void;
folder: string;
videoKey: string;
currentTitle: string;
onCorrect: () => void;
}
@@ -43,7 +43,7 @@ interface CorrectDialogProps {
export default function CorrectDialog({
isOpen,
onClose,
folder,
videoKey,
currentTitle,
onCorrect,
}: CorrectDialogProps) {
@@ -181,7 +181,7 @@ export default function CorrectDialog({
}
const body: any = {
folder,
key: videoKey,
tmdbId: finalTmdbId,
title: finalTitle,
posterPath: season?.poster_path || result.poster_path,

View File

@@ -1,5 +1,33 @@
import CryptoJS from 'crypto-js';
/**
* 生成 SHA256 哈希值
* @param data 要哈希的数据
* @returns SHA256 哈希值(十六进制字符串)
*/
export function sha256(data: string): string {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex);
}
/**
* 生成文件夹的唯一 key
* @param folderName 文件夹名称
* @param existingKeys 已存在的 key 集合,用于检测冲突
* @returns 唯一的 keySHA256 的前10位
*/
export function generateFolderKey(folderName: string, existingKeys: Set<string> = new Set()): string {
let hash = sha256(folderName);
let key = hash.substring(0, 10);
// 如果遇到冲突,继续 sha256 直到不冲突
while (existingKeys.has(key)) {
hash = sha256(hash);
key = hash.substring(0, 10);
}
return key;
}
/**
* 简单的对称加密工具
* 使用 AES 加密算法

View File

@@ -18,7 +18,8 @@ const VIDEOINFO_CACHE: Map<string, VideoInfoCacheEntry> = new Map();
export interface MetaInfo {
folders: {
[folderName: string]: {
[key: string]: {
folderName: string; // 原始文件夹名称
tmdb_id: number;
title: string;
poster_path: string | null;