feat: 添加视频源配置管理功能,包括导入和导出配置的支持
This commit is contained in:
@@ -1031,6 +1031,46 @@ KatelyaTV 支持标准的苹果 CMS V10 API 格式。
|
|||||||
|
|
||||||
站长或管理员访问 `/admin` 即可进行管理员配置
|
站长或管理员访问 `/admin` 即可进行管理员配置
|
||||||
|
|
||||||
|
### 🔧 视频源配置管理
|
||||||
|
|
||||||
|
管理员界面提供了完整的视频源配置管理功能:
|
||||||
|
|
||||||
|
#### 📤 导出配置
|
||||||
|
|
||||||
|
- **一键导出**:点击"📤 导出配置"按钮,系统会自动生成符合标准格式的 `config.json` 文件
|
||||||
|
- **自动格式化**:导出的配置文件包含所有已启用的视频源,格式完全符合项目要求
|
||||||
|
- **本地保存**:配置文件会自动下载到浏览器的下载文件夹,文件名包含日期标记
|
||||||
|
|
||||||
|
#### 📂 导入配置
|
||||||
|
|
||||||
|
- **文件选择**:点击"📂 导入配置"按钮,选择本地的 `.json` 配置文件
|
||||||
|
- **格式验证**:系统会自动验证配置文件格式,确保数据正确性
|
||||||
|
- **批量导入**:支持一次性导入多个视频源,显示详细的导入结果
|
||||||
|
- **错误提示**:如果导入过程中出现错误,会显示具体的错误信息
|
||||||
|
|
||||||
|
#### 📋 支持的配置格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache_time": 7200,
|
||||||
|
"api_site": {
|
||||||
|
"source_key": {
|
||||||
|
"api": "https://example.com/api.php/provide/vod",
|
||||||
|
"name": "视频源名称",
|
||||||
|
"detail": "https://example.com" // 可选
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✨ 其他管理功能
|
||||||
|
|
||||||
|
- **拖拽排序**:支持通过拖拽调整视频源的优先级顺序
|
||||||
|
- **启用/禁用**:可以临时禁用某个视频源而不删除配置
|
||||||
|
- **实时生效**:所有配置修改都会立即生效,无需重启服务
|
||||||
|
|
||||||
|
> **💡 提示**:导入的配置会永久保存在数据库中,不会因为浏览器刷新而丢失。这比直接修改 `config.json` 文件更加可靠和方便。
|
||||||
|
|
||||||
## 📱 AndroidTV 使用
|
## 📱 AndroidTV 使用
|
||||||
|
|
||||||
目前该项目可以配合 [OrionTV](https://github.com/zimplexing/OrionTV) 在 Android TV 上使用,可以直接作为 OrionTV 后端
|
目前该项目可以配合 [OrionTV](https://github.com/zimplexing/OrionTV) 在 Android TV 上使用,可以直接作为 OrionTV 后端
|
||||||
|
|||||||
+179
-7
@@ -721,6 +721,154 @@ const VideoSourceConfig = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 导出配置
|
||||||
|
const handleExportConfig = () => {
|
||||||
|
try {
|
||||||
|
// 构建符合要求的配置格式
|
||||||
|
const exportConfig = {
|
||||||
|
cache_time: config?.SiteConfig?.SiteInterfaceCacheTime || 7200,
|
||||||
|
api_site: {} as Record<string, any>
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将视频源转换为config.json格式
|
||||||
|
sources.forEach(source => {
|
||||||
|
if (!source.disabled) {
|
||||||
|
exportConfig.api_site[source.key] = {
|
||||||
|
api: source.api,
|
||||||
|
name: source.name,
|
||||||
|
...(source.detail && { detail: source.detail })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成JSON文件并下载
|
||||||
|
const dataStr = JSON.stringify(exportConfig, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `config_${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showSuccess('配置文件已导出到下载文件夹');
|
||||||
|
} catch (error) {
|
||||||
|
showError('导出失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入配置
|
||||||
|
const handleImportConfig = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 检查文件类型
|
||||||
|
if (!file.name.toLowerCase().endsWith('.json')) {
|
||||||
|
showError('请选择JSON文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
const content = e.target?.result as string;
|
||||||
|
const importConfig = JSON.parse(content);
|
||||||
|
|
||||||
|
// 验证配置格式
|
||||||
|
if (!importConfig.api_site || typeof importConfig.api_site !== 'object') {
|
||||||
|
showError('配置文件格式错误:缺少 api_site 字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认导入
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: '确认导入',
|
||||||
|
text: `检测到 ${Object.keys(importConfig.api_site).length} 个视频源,是否继续导入?`,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: '确认导入',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
confirmButtonColor: '#059669',
|
||||||
|
cancelButtonColor: '#6b7280'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
// 批量导入视频源
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const [key, source] of Object.entries(importConfig.api_site)) {
|
||||||
|
try {
|
||||||
|
// 类型检查和验证
|
||||||
|
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
||||||
|
throw new Error(`${key}: 无效的配置对象`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceObj = source as { api?: string; name?: string; detail?: string };
|
||||||
|
|
||||||
|
if (!sourceObj.api || !sourceObj.name) {
|
||||||
|
throw new Error(`${key}: 缺少必要字段 api 或 name`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await callSourceApi({
|
||||||
|
action: 'add',
|
||||||
|
key: key,
|
||||||
|
name: sourceObj.name,
|
||||||
|
api: sourceObj.api,
|
||||||
|
detail: sourceObj.detail || ''
|
||||||
|
});
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
errors.push(`${key}: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示导入结果
|
||||||
|
if (errorCount === 0) {
|
||||||
|
showSuccess(`成功导入 ${successCount} 个视频源`);
|
||||||
|
} else {
|
||||||
|
await Swal.fire({
|
||||||
|
title: '导入完成',
|
||||||
|
html: `
|
||||||
|
<div class="text-left">
|
||||||
|
<p class="text-green-600 mb-2">✅ 成功导入: ${successCount} 个</p>
|
||||||
|
<p class="text-red-600 mb-2">❌ 导入失败: ${errorCount} 个</p>
|
||||||
|
${errors.length > 0 ? `
|
||||||
|
<details class="mt-3">
|
||||||
|
<summary class="cursor-pointer text-gray-600">查看错误详情</summary>
|
||||||
|
<div class="mt-2 text-sm text-gray-500 max-h-32 overflow-y-auto">
|
||||||
|
${errors.map(err => `<div class="py-1">${err}</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
icon: successCount > 0 ? 'warning' : 'error',
|
||||||
|
confirmButtonText: '确定'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showError('配置文件解析失败: ' + (error instanceof Error ? error.message : '文件格式错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
showError('文件读取失败');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
|
||||||
|
// 清空input,允许重复选择同一文件
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
const handleDragEnd = (event: any) => {
|
const handleDragEnd = (event: any) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
@@ -829,16 +977,40 @@ const VideoSourceConfig = ({
|
|||||||
return (
|
return (
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
{/* 添加视频源表单 */}
|
{/* 添加视频源表单 */}
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between flex-wrap gap-2'>
|
||||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||||
视频源列表
|
视频源列表
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
onClick={() => setShowAddForm(!showAddForm)}
|
{/* 导入按钮 */}
|
||||||
className='px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors'
|
<label className='relative'>
|
||||||
>
|
<input
|
||||||
{showAddForm ? '取消' : '添加视频源'}
|
type='file'
|
||||||
</button>
|
accept='.json'
|
||||||
|
onChange={handleImportConfig}
|
||||||
|
className='absolute inset-0 w-full h-full opacity-0 cursor-pointer'
|
||||||
|
/>
|
||||||
|
<span className='inline-flex items-center px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors cursor-pointer'>
|
||||||
|
📂 导入配置
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* 导出按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={handleExportConfig}
|
||||||
|
className='inline-flex items-center px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
📤 导出配置
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 添加视频源按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
|
className='px-3 py-1 bg-orange-600 hover:bg-orange-700 text-white text-sm rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
{showAddForm ? '取消' : '➕ 添加视频源'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"cache_time": 7200,
|
||||||
|
"api_site": {
|
||||||
|
"test_source": {
|
||||||
|
"api": "https://test.example.com/api.php/provide/vod",
|
||||||
|
"name": "测试视频源",
|
||||||
|
"detail": "https://test.example.com"
|
||||||
|
},
|
||||||
|
"another_test": {
|
||||||
|
"api": "https://another.example.com/api.php/provide/vod",
|
||||||
|
"name": "另一个测试源"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user