feat: 添加 TVBox 配置接口,支持视频源导入及解析功能

This commit is contained in:
katelya
2025-09-03 19:32:24 +08:00
parent b4ebe89292
commit 2294f1b066
6 changed files with 864 additions and 1 deletions
+165
View File
@@ -0,0 +1,165 @@
import { NextRequest, NextResponse } from 'next/server';
// 常用的视频解析接口列表
const PARSE_APIS = [
{
name: '无名小站',
url: 'https://jx.aidouer.net/?url=',
support: ['qq', 'iqiyi', 'youku', 'mgtv', 'bilibili']
},
{
name: '虾米解析',
url: 'https://jx.xmflv.com/?url=',
support: ['qq', 'iqiyi', 'youku', 'mgtv', 'bilibili', 'sohu']
},
{
name: '爱豆解析',
url: 'https://jx.aidouer.net/?url=',
support: ['qq', 'iqiyi', 'youku', 'mgtv', 'bilibili']
},
{
name: '8090解析',
url: 'https://www.8090g.cn/?url=',
support: ['qq', 'iqiyi', 'youku', 'mgtv', 'bilibili']
},
{
name: 'OK解析',
url: 'https://okjx.cc/?url=',
support: ['qq', 'iqiyi', 'youku', 'mgtv', 'bilibili']
}
];
// 检测视频URL的平台类型
function detectPlatform(url: string): string {
if (url.includes('qq.com') || url.includes('v.qq.com')) return 'qq';
if (url.includes('iqiyi.com') || url.includes('qiyi.com')) return 'iqiyi';
if (url.includes('youku.com')) return 'youku';
if (url.includes('mgtv.com')) return 'mgtv';
if (url.includes('bilibili.com')) return 'bilibili';
if (url.includes('sohu.com')) return 'sohu';
if (url.includes('letv.com') || url.includes('le.com')) return 'letv';
if (url.includes('tudou.com')) return 'tudou';
if (url.includes('pptv.com')) return 'pptv';
if (url.includes('1905.com')) return '1905';
return 'unknown';
}
// 获取适用的解析接口
function getCompatibleParsers(platform: string) {
return PARSE_APIS.filter(api =>
api.support.includes(platform) || platform === 'unknown'
);
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const url = searchParams.get('url');
const parser = searchParams.get('parser'); // 指定解析器
const format = searchParams.get('format') || 'json'; // 返回格式
if (!url) {
return NextResponse.json(
{ error: '缺少url参数' },
{ status: 400 }
);
}
// 检测平台类型
const platform = detectPlatform(url);
const compatibleParsers = getCompatibleParsers(platform);
if (compatibleParsers.length === 0) {
return NextResponse.json(
{
error: '暂不支持该平台的视频解析',
platform,
url
},
{ status: 400 }
);
}
// 如果指定了解析器,优先使用
let selectedParser = compatibleParsers[0];
if (parser) {
const customParser = PARSE_APIS.find(api =>
api.name.toLowerCase().includes(parser.toLowerCase())
);
if (customParser && compatibleParsers.includes(customParser)) {
selectedParser = customParser;
}
}
const parseUrl = selectedParser.url + encodeURIComponent(url);
// 根据format返回不同格式
if (format === 'redirect') {
// 直接重定向到解析页面
return NextResponse.redirect(parseUrl);
} else if (format === 'iframe') {
// 返回可嵌入的HTML页面
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>视频播放</title>
<style>
body { margin: 0; padding: 0; background: #000; }
iframe { width: 100%; height: 100vh; border: none; }
</style>
</head>
<body>
<iframe src="${parseUrl}" allowfullscreen></iframe>
</body>
</html>`;
return new NextResponse(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Access-Control-Allow-Origin': '*'
}
});
} else {
// 返回JSON格式的解析信息
return NextResponse.json({
success: true,
data: {
original_url: url,
platform,
parse_url: parseUrl,
parser_name: selectedParser.name,
available_parsers: compatibleParsers.map(p => p.name)
}
}, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'public, max-age=300' // 5分钟缓存
}
});
}
} catch (error) {
return NextResponse.json(
{
error: '视频解析失败',
details: error instanceof Error ? error.message : String(error)
},
{ status: 500 }
);
}
}
// 支持CORS预检请求
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
});
}
+249
View File
@@ -0,0 +1,249 @@
import fs from 'fs';
import { NextRequest, NextResponse } from 'next/server';
import path from 'path';
// 定义配置文件结构
interface ApiSite {
api: string;
name: string;
detail?: string;
}
interface ConfigFileStruct {
cache_time?: number;
api_site: {
[key: string]: ApiSite;
};
}
// TVBox源格式接口
interface TVBoxSource {
key: string;
name: string;
type: number; // 0=影视源, 1=直播源, 3=解析源
api: string;
searchable?: number; // 0=不可搜索, 1=可搜索
quickSearch?: number; // 0=不支持快速搜索, 1=支持快速搜索
filterable?: number; // 0=不支持分类筛选, 1=支持分类筛选
ext?: string; // 扩展参数
jar?: string; // jar包地址
playUrl?: string; // 播放解析地址
categories?: string[]; // 分类
timeout?: number; // 超时时间(秒)
}
interface TVBoxConfig {
spider?: string; // 爬虫jar包地址
wallpaper?: string; // 壁纸地址
lives?: Array<{
name: string;
type: number;
url: string;
epg?: string;
logo?: string;
}>; // 直播源
sites: TVBoxSource[]; // 影视源
parses?: Array<{
name: string;
type: number;
url: string;
ext?: Record<string, unknown>;
header?: Record<string, string>;
}>; // 解析源
flags?: string[]; // 播放标识
ijk?: Record<string, unknown>; // IJK播放器配置
ads?: string[]; // 广告过滤规则
}
// 读取配置文件
function readConfigFile(): ConfigFileStruct {
const configPath = path.join(process.cwd(), 'config.json');
const raw = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(raw) as ConfigFileStruct;
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const format = searchParams.get('format') || 'json'; // 支持json和txt格式
const host = request.headers.get('host') || 'localhost:3000';
const protocol = request.headers.get('x-forwarded-proto') || 'http';
const baseUrl = `${protocol}://${host}`;
// 读取当前配置
const config = readConfigFile();
if (!config?.api_site) {
return NextResponse.json({ error: '配置文件读取失败' }, { status: 500 });
}
// 转换为TVBox格式
const tvboxConfig: TVBoxConfig = {
// 基础配置
spider: '', // 可以根据需要添加爬虫jar包
wallpaper: `${baseUrl}/screenshot1.png`, // 使用项目截图作为壁纸
// 影视源配置
sites: Object.entries(config.api_site).map(([key, source]) => ({
key: key,
name: source.name,
type: 0, // 影视源
api: source.api,
searchable: 1, // 可搜索
quickSearch: 1, // 支持快速搜索
filterable: 1, // 支持分类筛选
ext: source.detail || '', // 详情页地址作为扩展参数
timeout: 30, // 30秒超时
categories: [
"电影", "电视剧", "综艺", "动漫", "纪录片", "短剧"
]
})),
// 解析源配置(添加一些常用的解析源)
parses: [
{
name: "Json并发",
type: 2,
url: "Parallel"
},
{
name: "Json轮询",
type: 2,
url: "Sequence"
},
{
name: "KatelyaTV内置解析",
type: 1,
url: `${baseUrl}/api/parse?url=`,
ext: {
flag: ["qiyi", "qq", "letv", "sohu", "youku", "mgtv", "bilibili", "wasu", "xigua", "1905"]
}
}
],
// 播放标识
flags: [
"youku", "qq", "iqiyi", "qiyi", "letv", "sohu", "tudou", "pptv",
"mgtv", "wasu", "bilibili", "le", "duoduozy", "renrenmi", "xigua",
"优酷", "腾讯", "爱奇艺", "奇艺", "乐视", "搜狐", "土豆", "PPTV",
"芒果", "华数", "哔哩", "1905"
],
// 直播源(可选)
lives: [
{
name: "KatelyaTV直播",
type: 0,
url: `${baseUrl}/api/live/channels`,
epg: "",
logo: ""
}
],
// 广告过滤规则
ads: [
"mimg.0c1q0l.cn",
"www.googletagmanager.com",
"www.google-analytics.com",
"mc.usihnbcq.cn",
"mg.g1mm3d.cn",
"mscs.svaeuzh.cn",
"cnzz.hhurm.com",
"tp.vinuxhome.com",
"cnzz.mmstat.com",
"www.baihuillq.com",
"s23.cnzz.com",
"z3.cnzz.com",
"c.cnzz.com",
"stj.v1vo.top",
"z12.cnzz.com",
"img.mosflower.cn",
"tips.gamevvip.com",
"ehwe.yhdtns.com",
"xdn.cqqc3.com",
"www.jixunkyy.cn",
"sp.chemacid.cn",
"hm.baidu.com",
"s9.cnzz.com",
"z6.cnzz.com",
"um.cavuc.com",
"mav.mavuz.com",
"wofwk.aoidf3.com",
"z5.cnzz.com",
"xc.hubeijieshikj.cn",
"tj.tianwenhu.com",
"xg.gars57.cn",
"k.jinxiuzhilv.com",
"cdn.bootcss.com",
"ppl.xunzhuo123.com",
"xomk.jiangjunmh.top",
"img.xunzhuo123.com",
"z1.cnzz.com",
"s13.cnzz.com",
"xg.huataisangao.cn",
"z7.cnzz.com",
"xg.huataisangao.cn",
"z2.cnzz.com",
"s96.cnzz.com",
"q11.cnzz.com",
"thy.dacedsfa.cn",
"xg.whsbpw.cn",
"s19.cnzz.com",
"z8.cnzz.com",
"s4.cnzz.com",
"f5w.as12df.top",
"ae01.alicdn.com",
"www.92424.cn",
"k.wudejia.com",
"vivovip.mmszxc.top",
"qiu.xixiqiu.com",
"cdnjs.hnfenxun.com",
"cms.qdwght.com"
]
};
// 根据format参数返回不同格式
if (format === 'txt') {
// 返回base64编码的配置(TVBox常用格式)
const configStr = JSON.stringify(tvboxConfig, null, 2);
const base64Config = Buffer.from(configStr).toString('base64');
return new NextResponse(base64Config, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'public, max-age=3600'
}
});
} else {
// 返回JSON格式
return NextResponse.json(tvboxConfig, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'public, max-age=3600'
}
});
}
} catch (error) {
return NextResponse.json(
{ error: 'TVBox配置生成失败', details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// 支持CORS预检请求
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
});
}
+242
View File
@@ -0,0 +1,242 @@
'use client';
import { useEffect, useState } from 'react';
import PageLayout from '@/components/PageLayout';
export default function TVBoxPage() {
const [baseUrl, setBaseUrl] = useState('');
const [copySuccess, setCopySuccess] = useState<string | null>(null);
useEffect(() => {
// 获取当前域名
setBaseUrl(window.location.origin);
}, []);
const handleCopy = async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
} catch (err) {
// 降级方案:使用document.execCommand
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
}
};
const configs = [
{
name: 'TVBox JSON配置',
description: '直接返回JSON格式的配置文件,适用于支持在线配置的TVBox应用',
url: `${baseUrl}/api/tvbox`,
type: 'json'
},
{
name: 'TVBox Base64配置',
description: '返回Base64编码的配置文件,适用于大部分TVBox应用',
url: `${baseUrl}/api/tvbox?format=txt`,
type: 'base64'
}
];
return (
<PageLayout>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* 页面标题 */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
📺 TVBox配置接口
</h1>
<p className="text-lg text-gray-600 dark:text-gray-300">
KatelyaTV的视频源导入到TVBox应用中使用
</p>
</div>
{/* 功能介绍 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
🎯
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-3">
<div className="flex items-start space-x-3">
<span className="text-green-500 text-sm"></span>
<span className="text-gray-700 dark:text-gray-300">
KatelyaTV的所有视频源
</span>
</div>
<div className="flex items-start space-x-3">
<span className="text-green-500 text-sm"></span>
<span className="text-gray-700 dark:text-gray-300">
TVBox标准JSON格式
</span>
</div>
<div className="flex items-start space-x-3">
<span className="text-green-500 text-sm"></span>
<span className="text-gray-700 dark:text-gray-300">
</span>
</div>
</div>
<div className="space-y-3">
<div className="flex items-start space-x-3">
<span className="text-green-500 text-sm"></span>
<span className="text-gray-700 dark:text-gray-300">
Base64编码格式
</span>
</div>
<div className="flex items-start space-x-3">
<span className="text-green-500 text-sm"></span>
<span className="text-gray-700 dark:text-gray-300">
CORS跨域支持
</span>
</div>
<div className="flex items-start space-x-3">
<span className="text-green-500 text-sm"></span>
<span className="text-gray-700 dark:text-gray-300">
</span>
</div>
</div>
</div>
</div>
{/* 配置链接 */}
<div className="space-y-6">
{configs.map((config) => (
<div
key={config.type}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{config.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{config.description}
</p>
</div>
<button
onClick={() => handleCopy(config.url, config.type)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
copySuccess === config.type
? 'bg-green-500 text-white'
: 'bg-blue-500 hover:bg-blue-600 text-white'
}`}
>
{copySuccess === config.type ? '已复制!' : '复制链接'}
</button>
</div>
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<code className="text-sm text-gray-800 dark:text-gray-200 break-all">
{config.url}
</code>
</div>
</div>
))}
</div>
{/* 使用说明 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mt-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
📖 使
</h2>
<div className="space-y-4 text-gray-700 dark:text-gray-300">
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
1.
</h3>
<p className="text-sm ml-4">
"复制链接"
</p>
</div>
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
2. TVBox
</h3>
<p className="text-sm ml-4">
TVBox应用
</p>
</div>
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
3.
</h3>
<p className="text-sm ml-4">
KatelyaTV添加新的视频源时TVBox中刷新配置即可同步最新源站
</p>
</div>
</div>
</div>
{/* API参数说明 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mt-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
🔧 API参数说明
</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
format
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
json() txt(base64编码)
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
?format=txt
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* 解析接口说明 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mt-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
🎬
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
KatelyaTV提供内置的视频解析接口
</p>
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<code className="text-sm text-gray-800 dark:text-gray-200">
{baseUrl}/api/parse?url=
</code>
</div>
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
<p>TV</p>
</div>
</div>
</div>
</div>
</PageLayout>
);
}
+6 -1
View File
@@ -1,6 +1,6 @@
'use client';
import { Clover, Film, Home, Menu, Search, Tv } from 'lucide-react';
import { Clover, Film, Home, Menu, Search, Settings, Tv } from 'lucide-react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import {
@@ -138,6 +138,11 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
label: '综艺',
href: '/douban?type=show',
},
{
icon: Settings,
label: 'TVBox配置',
href: '/tvbox',
},
];
return (