变更私人影库的key值方式
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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' ? '电影' : '剧集',
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -63,8 +63,8 @@ export default function PrivateLibraryPage() {
|
||||
};
|
||||
|
||||
const handleVideoClick = (video: Video) => {
|
||||
// 跳转到播放页面
|
||||
router.push(`/play?source=openlist&id=${encodeURIComponent(video.folder)}`);
|
||||
// 跳转到播放页面,使用 id(key)而不是 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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 唯一的 key(SHA256 的前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 加密算法
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user