/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ import { NextRequest, NextResponse } from 'next/server'; import { getConfig } from '@/lib/config'; import { db } from '@/lib/db'; import { OpenListClient } from '@/lib/openlist.client'; import { getCachedMetaInfo, MetaInfo, setCachedMetaInfo, } from '@/lib/openlist-cache'; import { getTMDBImageUrl } from '@/lib/tmdb.search'; export const runtime = 'nodejs'; /** * CMS 采集站代理接口 * 用于代理 CMS API 请求,并自动将播放链接替换为带去广告的代理链接 * GET /api/cms-proxy?api=&参数1=值1&参数2=值2... */ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const apiUrl = searchParams.get('api'); if (!apiUrl) { return NextResponse.json( { error: '缺少必要参数: api' }, { status: 400 } ); } // 特殊处理 openlist if (apiUrl === 'openlist') { return handleOpenListProxy(request); } // 构建完整的 API 请求 URL,包含所有查询参数 const targetUrl = new URL(apiUrl); // 将所有查询参数(除了 api)转发到目标 API searchParams.forEach((value, key) => { if (key !== 'api') { targetUrl.searchParams.append(key, value); } }); // 请求原始 CMS API console.log('CMS 代理请求:', targetUrl.toString()); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时 try { const response = await fetch(targetUrl.toString(), { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'application/json', }, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { console.error('CMS API 请求失败:', response.status, response.statusText); return NextResponse.json( { error: '请求 CMS API 失败' }, { status: response.status } ); } const data = await response.json(); console.log('CMS API 返回数据:', { code: data.code, msg: data.msg, page: data.page, pagecount: data.pagecount, limit: data.limit, total: data.total, listCount: data.list?.length || 0, }); // 获取当前请求的 origin // 优先级:SITE_BASE 环境变量 > 从请求头构建 let origin = process.env.SITE_BASE; if (!origin) { // 从请求头中获取 Host 和协议 const host = request.headers.get('host') || request.headers.get('x-forwarded-host'); const proto = request.headers.get('x-forwarded-proto') || (host?.includes('localhost') || host?.includes('127.0.0.1') ? 'http' : 'https'); origin = `${proto}://${host}`; } console.log('CMS 代理 origin:', origin); // 处理返回数据,替换播放链接为代理链接 const processedData = processPlayUrls(data, origin); // 输出处理后的第一个视频的播放信息(用于调试) if (processedData.list && processedData.list.length > 0) { const firstItem = processedData.list[0]; console.log('第一个视频处理后的播放信息:', { vod_name: firstItem.vod_name, vod_play_from: firstItem.vod_play_from, vod_play_url_length: firstItem.vod_play_url?.length || 0, vod_play_url_preview: firstItem.vod_play_url?.substring(0, 200) || '', }); } return NextResponse.json(processedData, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-cache, no-store, must-revalidate', }, }); } catch (fetchError: any) { clearTimeout(timeoutId); if (fetchError.name === 'AbortError') { console.error('CMS API 请求超时:', targetUrl.toString()); return NextResponse.json( { error: '请求超时' }, { status: 504 } ); } throw fetchError; } } catch (error) { console.error('CMS 代理失败:', error); return NextResponse.json( { error: '代理失败', details: (error as Error).message }, { status: 500 } ); } } /** * 处理 CMS API 返回数据,将播放链接替换为代理链接 */ function processPlayUrls(data: any, proxyOrigin: string): any { if (!data || typeof data !== 'object') { return data; } // 深拷贝数据,避免修改原始对象 const processedData = JSON.parse(JSON.stringify(data)); // 获取 M3U8 代理 token const proxyToken = process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || ''; const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; // 处理列表数据 if (processedData.list && Array.isArray(processedData.list)) { processedData.list = processedData.list.map((item: any, index: number) => { // 只处理有播放地址的项目 if (item.vod_play_url && typeof item.vod_play_url === 'string') { try { const originalUrl = item.vod_play_url; item.vod_play_url = processPlayUrlString(item.vod_play_url, item.vod_play_from || '', proxyOrigin, tokenParam); // 只为第一个视频输出详细日志 if (index === 0) { console.log('播放地址处理:', { vod_name: item.vod_name, vod_play_from: item.vod_play_from, original_length: originalUrl.length, processed_length: item.vod_play_url.length, original_preview: originalUrl.substring(0, 100), processed_preview: item.vod_play_url.substring(0, 150), }); } } catch (error) { // 如果处理失败,保持原样 console.error('处理播放地址失败:', error, item.vod_name); } } return item; }); } return processedData; } /** * 处理播放地址字符串 * 格式: 第01集$url1#第02集$url2#... */ function processPlayUrlString(playUrl: string, playFrom: string, proxyOrigin: string, tokenParam: string): string { if (!playUrl) return playUrl; // 按 $ 分割,分别处理每个播放源 const playSources = playUrl.split('$$$'); return playSources.map(source => { // 处理每个播放源的剧集列表 const episodes = source.split('#'); return episodes.map(episode => { // 格式: 第01集$url 或 url // 使用 indexOf 找到第一个 $ 的位置 const dollarIndex = episode.indexOf('$'); if (dollarIndex > 0) { // 有标题的格式: 第01集$url 或 第01集$url$其他 const title = episode.substring(0, dollarIndex); const rest = episode.substring(dollarIndex + 1); // 检查后面是否还有 $,如果有就保留 const nextDollarIndex = rest.indexOf('$'); if (nextDollarIndex > 0) { // 格式: 第01集$url$其他 const url = rest.substring(0, nextDollarIndex); const other = rest.substring(nextDollarIndex); const processedUrl = processUrl(url.trim(), playFrom, proxyOrigin, tokenParam); return `${title}$${processedUrl}${other}`; } else { // 格式: 第01集$url const processedUrl = processUrl(rest.trim(), playFrom, proxyOrigin, tokenParam); return `${title}$${processedUrl}`; } } else if (episode.trim()) { // 只有 URL 的格式 const processedUrl = processUrl(episode.trim(), playFrom, proxyOrigin, tokenParam); return processedUrl; } return episode; }).join('#'); }).join('$$$'); } /** * 处理单个播放地址 */ function processUrl(url: string, playFrom: string, proxyOrigin: string, tokenParam: string): string { if (!url) return url; // 只处理 m3u8 链接 if (url.includes('.m3u8')) { // 提取播放源类型(如果有的话) const source = playFrom ? `&source=${encodeURIComponent(playFrom)}` : ''; // 将 m3u8 链接替换为代理链接 return `${proxyOrigin}/api/proxy-m3u8?url=${encodeURIComponent(url)}${source}${tokenParam}`; } // 非 m3u8 链接不处理 return url; } /** * 处理 OpenList 代理请求 */ async function handleOpenListProxy(request: NextRequest) { const { searchParams } = new URL(request.url); const wd = searchParams.get('wd'); // 搜索关键词 const ids = searchParams.get('ids'); // 详情ID const config = await getConfig(); const openListConfig = config.OpenListConfig; if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) { return NextResponse.json( { code: 0, msg: 'OpenList 未配置', list: [] }, { status: 200 } ); } const rootPath = openListConfig.RootPath || '/'; const client = new OpenListClient( openListConfig.URL, openListConfig.Username, openListConfig.Password ); // 读取 metainfo (从数据库或缓存) let metaInfo: MetaInfo | null = getCachedMetaInfo(rootPath); if (!metaInfo) { try { 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 不存在', list: [] }, { status: 200 } ); } } if (!metaInfo) { return NextResponse.json( { code: 0, msg: '无数据', list: [] }, { status: 200 } ); } // 搜索模式 if (wd) { const results = Object.entries(metaInfo.folders) .filter( ([key, info]) => info.folderName.toLowerCase().includes(wd.toLowerCase()) || info.title.toLowerCase().includes(wd.toLowerCase()) ) .map(([key, info]) => ({ vod_id: key, vod_name: info.title, vod_pic: getTMDBImageUrl(info.poster_path), vod_remarks: info.media_type === 'movie' ? '电影' : '剧集', vod_year: info.release_date.split('-')[0] || '', type_name: info.media_type === 'movie' ? '电影' : '电视剧', })); return NextResponse.json({ code: 1, msg: '数据列表', page: 1, pagecount: 1, limit: results.length, total: results.length, list: results, }); } // 详情模式 if (ids) { const key = ids; const info = metaInfo.folders[key]; if (!info) { return NextResponse.json( { code: 0, msg: '视频不存在', list: [] }, { status: 200 } ); } const folderName = info.folderName; // 获取视频详情 try { const detailResponse = await fetch( `${request.headers.get('x-forwarded-proto') || 'http'}://${request.headers.get('host')}/api/openlist/detail?folder=${encodeURIComponent(folderName)}` ); if (!detailResponse.ok) { throw new Error('获取视频详情失败'); } const detailData = await detailResponse.json(); if (!detailData.success) { throw new Error('获取视频详情失败'); } // 构建播放列表 const playUrls = detailData.episodes .map((ep: any) => { const title = ep.title || `第${ep.episode}集`; return `${title}$${ep.playUrl}`; }) .join('#'); return NextResponse.json({ code: 1, msg: '数据列表', page: 1, pagecount: 1, limit: 1, total: 1, list: [ { vod_id: key, vod_name: info.title, vod_pic: getTMDBImageUrl(info.poster_path), vod_remarks: info.media_type === 'movie' ? '电影' : '剧集', vod_year: info.release_date.split('-')[0] || '', vod_content: info.overview, vod_play_from: 'OpenList', vod_play_url: playUrls, type_name: info.media_type === 'movie' ? '电影' : '电视剧', }, ], }); } catch (error) { console.error('获取 OpenList 视频详情失败:', error); return NextResponse.json( { code: 0, msg: '获取详情失败', list: [] }, { status: 200 } ); } } // 默认返回所有视频 const results = Object.entries(metaInfo.folders).map( ([key, info]) => ({ vod_id: key, vod_name: info.title, vod_pic: getTMDBImageUrl(info.poster_path), vod_remarks: info.media_type === 'movie' ? '电影' : '剧集', vod_year: info.release_date.split('-')[0] || '', type_name: info.media_type === 'movie' ? '电影' : '电视剧', }) ); return NextResponse.json({ code: 1, msg: '数据列表', page: 1, pagecount: 1, limit: results.length, total: results.length, list: results, }); }