diff --git a/package.json b/package.json index c9a3f25..853b688 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "next": "^14.2.33", "next-pwa": "^5.6.0", "next-themes": "^0.4.6", + "node-fetch": "^2.7.0", "parse-torrent-name": "^0.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -68,6 +69,7 @@ "@types/bs58": "^5.0.0", "@types/he": "^1.2.3", "@types/node": "24.0.3", + "@types/node-fetch": "^2.6.13", "@types/react": "^18.3.18", "@types/react-dom": "^19.1.6", "@types/testing-library__jest-dom": "^5.14.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2855873..53c0307 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + node-fetch: + specifier: ^2.7.0 + version: 2.7.0 parse-torrent-name: specifier: ^0.5.4 version: 0.5.4 @@ -147,6 +150,9 @@ importers: '@types/node': specifier: 24.0.3 version: 24.0.3 + '@types/node-fetch': + specifier: ^2.6.13 + version: 2.6.13 '@types/react': specifier: ^18.3.18 version: 18.3.23 @@ -1565,6 +1571,9 @@ packages: '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node@24.0.3': resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} @@ -3007,6 +3016,10 @@ packages: resolution: {integrity: sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -4082,6 +4095,15 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -5057,6 +5079,9 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -5282,6 +5307,9 @@ packages: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -5334,6 +5362,9 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -7228,6 +7259,11 @@ snapshots: '@types/minimist@1.2.5': {} + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 24.0.3 + form-data: 4.0.5 + '@types/node@24.0.3': dependencies: undici-types: 7.8.0 @@ -8874,6 +8910,14 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fraction.js@4.3.7: {} framer-motion@12.18.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -10249,6 +10293,10 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-int64@0.4.0: {} node-releases@2.0.19: {} @@ -11257,6 +11305,8 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -11501,6 +11551,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} webidl-conversions@5.0.0: {} @@ -11571,6 +11623,11 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/src/app/api/cms-proxy/route.ts b/src/app/api/cms-proxy/route.ts index 31efcbc..a11baf2 100644 --- a/src/app/api/cms-proxy/route.ts +++ b/src/app/api/cms-proxy/route.ts @@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getConfig } from '@/lib/config'; +import { db } from '@/lib/db'; import { OpenListClient } from '@/lib/openlist.client'; import { getCachedMetaInfo, @@ -273,30 +274,19 @@ async function handleOpenListProxy(request: NextRequest) { const rootPath = openListConfig.RootPath || '/'; const client = new OpenListClient(openListConfig.URL, openListConfig.Token); - // 读取 metainfo.json + // 读取 metainfo (从数据库或缓存) let metaInfo: MetaInfo | null = getCachedMetaInfo(rootPath); if (!metaInfo) { try { - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - const fileResponse = await client.getFile(metainfoPath); - - if (fileResponse.code === 200 && fileResponse.data.raw_url) { - const downloadUrl = fileResponse.data.raw_url; - const contentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - const content = await contentResponse.text(); - metaInfo = JSON.parse(content) as MetaInfo; + const metainfoJson = await db.getGlobalValue('video.metainfo'); + if (metainfoJson) { + metaInfo = JSON.parse(metainfoJson) as MetaInfo; setCachedMetaInfo(rootPath, metaInfo); } } catch (error) { return NextResponse.json( - { code: 0, msg: 'metainfo.json 不存在', list: [] }, + { code: 0, msg: 'metainfo 不存在', list: [] }, { status: 200 } ); } diff --git a/src/app/api/detail/route.ts b/src/app/api/detail/route.ts index 21e3a41..66717fd 100644 --- a/src/app/api/detail/route.ts +++ b/src/app/api/detail/route.ts @@ -32,35 +32,24 @@ export async function GET(request: NextRequest) { const rootPath = openListConfig.RootPath || '/'; - // 1. 读取 metainfo.json 获取元数据 + // 1. 读取 metainfo 获取元数据 let metaInfo: any = null; try { - const { OpenListClient } = await import('@/lib/openlist.client'); - const { getCachedMetaInfo } = await import('@/lib/openlist-cache'); + const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache'); const { getTMDBImageUrl } = await import('@/lib/tmdb.search'); + const { db } = await import('@/lib/db'); - const client = new OpenListClient(openListConfig.URL, openListConfig.Token); metaInfo = getCachedMetaInfo(rootPath); if (!metaInfo) { - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - const fileResponse = await client.getFile(metainfoPath); - - if (fileResponse.code === 200 && fileResponse.data.raw_url) { - const downloadUrl = fileResponse.data.raw_url; - const contentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - const content = await contentResponse.text(); - metaInfo = JSON.parse(content); + const metainfoJson = await db.getGlobalValue('video.metainfo'); + if (metainfoJson) { + metaInfo = JSON.parse(metainfoJson); + setCachedMetaInfo(rootPath, metaInfo); } } } catch (error) { - console.error('[Detail] 读取 metainfo.json 失败:', error); + console.error('[Detail] 从数据库读取 metainfo 失败:', error); } // 2. 调用 openlist detail API diff --git a/src/app/api/openlist/correct/route.ts b/src/app/api/openlist/correct/route.ts index cbea3cd..8a27ea5 100644 --- a/src/app/api/openlist/correct/route.ts +++ b/src/app/api/openlist/correct/route.ts @@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig } from '@/lib/config'; +import { db } from '@/lib/db'; import { OpenListClient } from '@/lib/openlist.client'; import { getCachedMetaInfo, @@ -53,35 +54,21 @@ export async function POST(request: NextRequest) { openListConfig.Password ); - // 读取现有 metainfo.json + // 读取现有 metainfo (从数据库或缓存) let metaInfo: MetaInfo | null = getCachedMetaInfo(rootPath); if (!metaInfo) { try { - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - const fileResponse = await client.getFile(metainfoPath); + console.log('[OpenList Correct] 尝试从数据库读取 metainfo'); + const metainfoJson = await db.getGlobalValue('video.metainfo'); - if (fileResponse.code === 200 && fileResponse.data.raw_url) { - const downloadUrl = fileResponse.data.raw_url; - const contentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - - if (!contentResponse.ok) { - throw new Error(`下载失败: ${contentResponse.status}`); - } - - const content = await contentResponse.text(); - metaInfo = JSON.parse(content); + if (metainfoJson) { + metaInfo = JSON.parse(metainfoJson); } } catch (error) { - console.error('[OpenList Correct] 读取 metainfo.json 失败:', error); + console.error('[OpenList Correct] 从数据库读取 metainfo 失败:', error); return NextResponse.json( - { error: 'metainfo.json 读取失败' }, + { error: 'metainfo 读取失败' }, { status: 500 } ); } @@ -107,11 +94,10 @@ export async function POST(request: NextRequest) { failed: false, // 纠错后标记为成功 }; - // 保存 metainfo.json - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - const metainfoContent = JSON.stringify(metaInfo, null, 2); + // 保存 metainfo 到数据库 + const metainfoContent = JSON.stringify(metaInfo); - await client.uploadFile(metainfoPath, metainfoContent); + await db.setGlobalValue('video.metainfo', metainfoContent); // 更新缓存 invalidateMetaInfoCache(rootPath); diff --git a/src/app/api/openlist/detail/route.ts b/src/app/api/openlist/detail/route.ts index 118b3bd..1d40be3 100644 --- a/src/app/api/openlist/detail/route.ts +++ b/src/app/api/openlist/detail/route.ts @@ -125,14 +125,7 @@ export async function GET(request: NextRequest) { }; } - // 保存 videoinfo.json - const videoinfoPath = `${folderPath}/videoinfo.json`; - await client.uploadFile( - videoinfoPath, - JSON.stringify(videoInfo, null, 2) - ); - - // 缓存 + // 仅缓存到内存,不再持久化到 OpenList setCachedVideoInfo(folderPath, videoInfo); } diff --git a/src/app/api/openlist/list/route.ts b/src/app/api/openlist/list/route.ts index 1fffba6..e5e2db9 100644 --- a/src/app/api/openlist/list/route.ts +++ b/src/app/api/openlist/list/route.ts @@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig } from '@/lib/config'; +import { db } from '@/lib/db'; import { OpenListClient } from '@/lib/openlist.client'; import { getCachedMetaInfo, @@ -48,7 +49,7 @@ export async function GET(request: NextRequest) { openListConfig.Password ); - // 读取 metainfo.json + // 读取 metainfo (从数据库或缓存) let metaInfo: MetaInfo | null = getCachedMetaInfo(rootPath); console.log('[OpenList List] 缓存检查:', { @@ -58,40 +59,15 @@ export async function GET(request: NextRequest) { if (!metaInfo) { try { - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - console.log('[OpenList List] 尝试读取 metainfo.json:', metainfoPath); + console.log('[OpenList List] 尝试从数据库读取 metainfo'); - const fileResponse = await client.getFile(metainfoPath); - console.log('[OpenList List] getFile 完整响应:', JSON.stringify(fileResponse, null, 2)); + const metainfoJson = await db.getGlobalValue('video.metainfo'); - if (fileResponse.code === 200 && fileResponse.data.raw_url) { - console.log('[OpenList List] 使用 raw_url 获取文件内容'); - - const downloadUrl = fileResponse.data.raw_url; - console.log('[OpenList List] 下载 URL:', downloadUrl); - - const contentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - console.log('[OpenList List] fetch 响应:', { - status: contentResponse.status, - ok: contentResponse.ok, - }); - - if (!contentResponse.ok) { - throw new Error(`获取文件内容失败: ${contentResponse.status}`); - } - - const content = await contentResponse.text(); - console.log('[OpenList List] 文件内容长度:', content.length); - console.log('[OpenList List] 文件内容预览:', content.substring(0, 200)); + if (metainfoJson) { + console.log('[OpenList List] 从数据库获取到数据,长度:', metainfoJson.length); try { - metaInfo = JSON.parse(content); + metaInfo = JSON.parse(metainfoJson); console.log('[OpenList List] JSON 解析成功'); console.log('[OpenList List] metaInfo 结构:', { hasfolders: !!metaInfo?.folders, @@ -114,18 +90,13 @@ export async function GET(request: NextRequest) { throw new Error(`JSON 解析失败: ${(parseError as Error).message}`); } } else { - console.error('[OpenList List] getFile 失败或无 sign:', { - code: fileResponse.code, - message: fileResponse.message, - data: fileResponse.data, - }); - throw new Error(`getFile 返回错误: code=${fileResponse.code}, message=${fileResponse.message}`); + throw new Error('数据库中没有 metainfo 数据'); } } catch (error) { - console.error('[OpenList List] 读取 metainfo.json 失败:', error); + console.error('[OpenList List] 从数据库读取 metainfo 失败:', error); return NextResponse.json( { - error: 'metainfo.json 读取失败', + error: 'metainfo 读取失败', details: (error as Error).message, list: [], total: 0, diff --git a/src/app/api/openlist/refresh/route.ts b/src/app/api/openlist/refresh/route.ts index 3b3ec30..409a070 100644 --- a/src/app/api/openlist/refresh/route.ts +++ b/src/app/api/openlist/refresh/route.ts @@ -117,54 +117,25 @@ async function performScan( updateScanTaskProgress(taskId, 0, 0); try { - // 1. 读取现有 metainfo.json (如果存在) + // 1. 读取现有 metainfo (从数据库或缓存) let existingMetaInfo: MetaInfo | null = getCachedMetaInfo(rootPath); if (!existingMetaInfo) { try { - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - console.log('[OpenList Refresh] 尝试读取现有 metainfo.json:', metainfoPath); + console.log('[OpenList Refresh] 尝试从数据库读取 metainfo'); + const metainfoJson = await db.getGlobalValue('video.metainfo'); - const fileResponse = await client.getFile(metainfoPath); - console.log('[OpenList Refresh] getFile 完整响应:', JSON.stringify(fileResponse, null, 2)); - - if (fileResponse.code === 200 && fileResponse.data.raw_url) { - const downloadUrl = fileResponse.data.raw_url; - console.log('[OpenList Refresh] 下载 URL:', downloadUrl); - - const contentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - - console.log('[OpenList Refresh] fetch 响应:', { - status: contentResponse.status, - ok: contentResponse.ok, - }); - - if (!contentResponse.ok) { - throw new Error(`下载失败: ${contentResponse.status}`); - } - - const content = await contentResponse.text(); - console.log('[OpenList Refresh] 文件内容:', { - length: content.length, - preview: content.substring(0, 300), - }); - - existingMetaInfo = JSON.parse(content); - console.log('[OpenList Refresh] 读取到现有数据:', { + if (metainfoJson) { + existingMetaInfo = JSON.parse(metainfoJson); + console.log('[OpenList Refresh] 从数据库读取到现有数据:', { hasfolders: !!existingMetaInfo?.folders, foldersType: typeof existingMetaInfo?.folders, videoCount: Object.keys(existingMetaInfo?.folders || {}).length, }); } } catch (error) { - console.error('[OpenList Refresh] 读取 metainfo.json 失败:', error); - console.log('[OpenList Refresh] 将创建新文件'); + console.error('[OpenList Refresh] 从数据库读取 metainfo 失败:', error); + console.log('[OpenList Refresh] 将创建新数据'); } } else { console.log('[OpenList Refresh] 使用缓存的 metainfo,视频数:', Object.keys(existingMetaInfo.folders).length); @@ -294,41 +265,23 @@ async function performScan( } } - // 4. 更新 metainfo.json + // 4. 保存 metainfo 到数据库 metaInfo.last_refresh = Date.now(); - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - const metainfoContent = JSON.stringify(metaInfo, null, 2); - console.log('[OpenList Refresh] 上传 metainfo.json:', { - path: metainfoPath, + const metainfoContent = JSON.stringify(metaInfo); + console.log('[OpenList Refresh] 保存 metainfo 到数据库:', { videoCount: Object.keys(metaInfo.folders).length, contentLength: metainfoContent.length, - contentPreview: metainfoContent.substring(0, 300), }); - await client.uploadFile(metainfoPath, metainfoContent); - console.log('[OpenList Refresh] 上传成功'); + await db.setGlobalValue('video.metainfo', metainfoContent); + console.log('[OpenList Refresh] 保存成功'); - // 验证上传:立即读取文件 + // 验证保存:立即读取数据库 try { - console.log('[OpenList Refresh] 验证上传:读取文件'); - const verifyResponse = await client.getFile(metainfoPath); - if (verifyResponse.code === 200 && verifyResponse.data.raw_url) { - const downloadUrl = verifyResponse.data.raw_url; - const verifyContentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - const verifyContent = await verifyContentResponse.text(); - console.log('[OpenList Refresh] 验证读取成功:', { - contentLength: verifyContent.length, - contentPreview: verifyContent.substring(0, 300), - }); - - // 尝试解析 + console.log('[OpenList Refresh] 验证保存:读取数据库'); + const verifyContent = await db.getGlobalValue('video.metainfo'); + if (verifyContent) { const verifyParsed = JSON.parse(verifyContent); console.log('[OpenList Refresh] 验证解析成功:', { hasfolders: !!verifyParsed.folders, diff --git a/src/app/api/search/ws/route.ts b/src/app/api/search/ws/route.ts index 8ee868b..0747b17 100644 --- a/src/app/api/search/ws/route.ts +++ b/src/app/api/search/ws/route.ts @@ -80,37 +80,25 @@ export async function GET(request: NextRequest) { // 搜索 OpenList(如果配置了) if (hasOpenList) { try { - const { getCachedMetaInfo } = await import('@/lib/openlist-cache'); + const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache'); const { getTMDBImageUrl } = await import('@/lib/tmdb.search'); - const { OpenListClient } = await import('@/lib/openlist.client'); + const { db } = await import('@/lib/db'); const rootPath = config.OpenListConfig!.RootPath || '/'; let metaInfo = getCachedMetaInfo(rootPath); - // 如果没有缓存,尝试从 OpenList 读取 + // 如果没有缓存,尝试从数据库读取 if (!metaInfo) { try { - const client = new OpenListClient( - config.OpenListConfig!.URL, - config.OpenListConfig!.Token - ); - const metainfoPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}metainfo.json`; - const fileResponse = await client.getFile(metainfoPath); - - if (fileResponse.code === 200 && fileResponse.data.raw_url) { - const downloadUrl = fileResponse.data.raw_url; - const contentResponse = await fetch(downloadUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - }, - }); - const content = await contentResponse.text(); - metaInfo = JSON.parse(content); + const metainfoJson = await db.getGlobalValue('video.metainfo'); + if (metainfoJson) { + metaInfo = JSON.parse(metainfoJson); + if (metaInfo) { + setCachedMetaInfo(rootPath, metaInfo); + } } } catch (error) { - console.error('[Search WS] 读取 metainfo.json 失败:', error); + console.error('[Search WS] 从数据库读取 metainfo 失败:', error); } } diff --git a/src/app/api/tmdb/search/route.ts b/src/app/api/tmdb/search/route.ts index ca5fad8..d6f7cfe 100644 --- a/src/app/api/tmdb/search/route.ts +++ b/src/app/api/tmdb/search/route.ts @@ -5,26 +5,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { getConfig } from '@/lib/config'; import { HttpsProxyAgent } from 'https-proxy-agent'; +import nodeFetch from 'node-fetch'; export const runtime = 'nodejs'; -// 代理 agent 缓存 -const proxyAgentCache = new Map>(); - -function getProxyAgent(proxy: string): HttpsProxyAgent { - if (!proxyAgentCache.has(proxy)) { - const agent = new HttpsProxyAgent(proxy, { - timeout: 30000, - keepAlive: true, - keepAliveMsecs: 60000, - maxSockets: 10, - maxFreeSockets: 5, - }); - proxyAgentCache.set(proxy, agent); - } - return proxyAgentCache.get(proxy)!; -} - /** * GET /api/tmdb/search?query=xxx * 搜索TMDB,返回多个结果供用户选择 @@ -59,14 +43,18 @@ export async function GET(request: NextRequest) { const fetchOptions: any = tmdbProxy ? { - agent: getProxyAgent(tmdbProxy), + agent: new HttpsProxyAgent(tmdbProxy, { + timeout: 30000, + keepAlive: false, + }), signal: AbortSignal.timeout(30000), } : { signal: AbortSignal.timeout(15000), }; - const response = await fetch(url, fetchOptions); + // 使用 node-fetch 而不是原生 fetch + const response = await nodeFetch(url, fetchOptions); if (!response.ok) { console.error('TMDB 搜索失败:', response.status, response.statusText); diff --git a/src/lib/openlist-cache.ts b/src/lib/openlist-cache.ts index 6f82558..1b8240e 100644 --- a/src/lib/openlist-cache.ts +++ b/src/lib/openlist-cache.ts @@ -11,7 +11,7 @@ interface VideoInfoCacheEntry { } const METAINFO_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7天 -const VIDEOINFO_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 1天 +const VIDEOINFO_CACHE_TTL_MS = 60 * 60 * 1000; // 1小时 const METAINFO_CACHE: Map = new Map(); const VIDEOINFO_CACHE: Map = new Map(); diff --git a/src/lib/tmdb.client.ts b/src/lib/tmdb.client.ts index 5636a19..d5f4ff6 100644 --- a/src/lib/tmdb.client.ts +++ b/src/lib/tmdb.client.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any,no-console */ import { HttpsProxyAgent } from 'https-proxy-agent'; +import nodeFetch from 'node-fetch'; export interface TMDBMovie { id: number; @@ -45,29 +46,6 @@ interface TMDBTVAiringTodayResponse { total_results: number; } -// 代理 agent 缓存,避免每次都创建新实例 -const proxyAgentCache = new Map>(); - -/** - * 获取或创建代理 agent(复用连接池) - */ -function getProxyAgent(proxy: string): HttpsProxyAgent { - if (!proxyAgentCache.has(proxy)) { - const agent = new HttpsProxyAgent(proxy, { - // 增加超时时间 - timeout: 30000, // 30秒 - // 保持连接活跃 - keepAlive: true, - keepAliveMsecs: 60000, // 60秒 - // 最大空闲连接数 - maxSockets: 10, - maxFreeSockets: 5, - }); - proxyAgentCache.set(proxy, agent); - } - return proxyAgentCache.get(proxy)!; -} - /** * 获取即将上映的电影 * @param apiKey - TMDB API Key @@ -90,21 +68,25 @@ export async function getTMDBUpcomingMovies( const url = `https://api.themoviedb.org/3/movie/upcoming?api_key=${apiKey}&language=zh-CN&page=${page}®ion=${region}`; const fetchOptions: any = proxy ? { - agent: getProxyAgent(proxy), + agent: new HttpsProxyAgent(proxy, { + timeout: 30000, + keepAlive: false, + }), signal: AbortSignal.timeout(30000), } : { signal: AbortSignal.timeout(15000), }; - const response = await fetch(url, fetchOptions); + // 使用 node-fetch 而不是原生 fetch + const response = await nodeFetch(url, fetchOptions); if (!response.ok) { console.error('TMDB API 请求失败:', response.status, response.statusText); return { code: response.status, list: [] }; } - const data: TMDBUpcomingResponse = await response.json(); + const data: TMDBUpcomingResponse = await response.json() as TMDBUpcomingResponse; return { code: 200, @@ -137,21 +119,25 @@ export async function getTMDBUpcomingTVShows( const url = `https://api.themoviedb.org/3/tv/on_the_air?api_key=${apiKey}&language=zh-CN&page=${page}`; const fetchOptions: any = proxy ? { - agent: getProxyAgent(proxy), + agent: new HttpsProxyAgent(proxy, { + timeout: 30000, + keepAlive: false, + }), signal: AbortSignal.timeout(30000), } : { signal: AbortSignal.timeout(15000), }; - const response = await fetch(url, fetchOptions); + // 使用 node-fetch 而不是原生 fetch + const response = await nodeFetch(url, fetchOptions); if (!response.ok) { console.error('TMDB TV API 请求失败:', response.status, response.statusText); return { code: response.status, list: [] }; } - const data: TMDBTVAiringTodayResponse = await response.json(); + const data: TMDBTVAiringTodayResponse = await response.json() as TMDBTVAiringTodayResponse; return { code: 200, diff --git a/src/lib/tmdb.search.ts b/src/lib/tmdb.search.ts index c226baa..b35ec27 100644 --- a/src/lib/tmdb.search.ts +++ b/src/lib/tmdb.search.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { HttpsProxyAgent } from 'https-proxy-agent'; +import nodeFetch from 'node-fetch'; export interface TMDBSearchResult { id: number; @@ -21,29 +22,6 @@ interface TMDBSearchResponse { total_results: number; } -// 代理 agent 缓存,避免每次都创建新实例 -const proxyAgentCache = new Map>(); - -/** - * 获取或创建代理 agent(复用连接池) - */ -function getProxyAgent(proxy: string): HttpsProxyAgent { - if (!proxyAgentCache.has(proxy)) { - const agent = new HttpsProxyAgent(proxy, { - // 增加超时时间 - timeout: 30000, // 30秒 - // 保持连接活跃 - keepAlive: true, - keepAliveMsecs: 60000, // 60秒 - // 最大空闲连接数 - maxSockets: 10, - maxFreeSockets: 5, - }); - proxyAgentCache.set(proxy, agent); - } - return proxyAgentCache.get(proxy)!; -} - /** * 搜索 TMDB (电影+电视剧) */ @@ -59,26 +37,28 @@ export async function searchTMDB( // 使用 multi search 同时搜索电影和电视剧 const url = `https://api.themoviedb.org/3/search/multi?api_key=${apiKey}&language=zh-CN&query=${encodeURIComponent(query)}&page=1`; - + const fetchOptions: any = proxy ? { - agent: getProxyAgent(proxy), - // 设置请求超时(30秒) + agent: new HttpsProxyAgent(proxy, { + timeout: 30000, + keepAlive: false, + }), signal: AbortSignal.timeout(30000), } : { - // 即使不用代理也设置超时 signal: AbortSignal.timeout(15000), }; - const response = await fetch(url, fetchOptions); + // 使用 node-fetch 而不是原生 fetch,因为原生 fetch 不支持 agent 选项 + const response = await nodeFetch(url, fetchOptions); if (!response.ok) { console.error('TMDB 搜索失败:', response.status, response.statusText); return { code: response.status, result: null }; } - const data: TMDBSearchResponse = await response.json(); + const data: TMDBSearchResponse = await response.json() as TMDBSearchResponse; // 过滤出电影和电视剧,取第一个结果 const validResults = data.results.filter(