From 89be4aab872debf7f0fec919f2b7f3f3234c4d59 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 13 Jan 2026 09:43:21 +0800 Subject: [PATCH] =?UTF-8?q?emby=E5=A2=9E=E5=8A=A0=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 74 +++++++++++++++++++++++++ src/app/api/admin/emby/export/route.ts | 44 +++++++++++++++ src/app/api/admin/emby/import/route.ts | 77 ++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 src/app/api/admin/emby/export/route.ts create mode 100644 src/app/api/admin/emby/import/route.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 5caab46..f82a746 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -3709,6 +3709,66 @@ const EmbyConfigComponent = ({ }); }; + // 导出配置 + const handleExport = async () => { + await withLoading('exportEmby', async () => { + try { + const response = await fetch('/api/admin/emby/export'); + if (!response.ok) { + const data = await response.json(); + showError(data.error || '导出失败', showAlert); + return; + } + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `emby-config-${Date.now()}.json`; + a.click(); + window.URL.revokeObjectURL(url); + showSuccess('导出成功', showAlert); + } catch (error) { + showError(error instanceof Error ? error.message : '导出失败', showAlert); + } + }); + }; + + // 导入配置 + const handleImport = async () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + await withLoading('importEmby', async () => { + try { + const text = await file.text(); + const data = JSON.parse(text); + + const response = await fetch('/api/admin/emby/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data }), + }); + + const result = await response.json(); + + if (result.success) { + showSuccess('导入成功', showAlert); + await refreshConfig(); + } else { + showError(result.error || '导入失败', showAlert); + } + } catch (error) { + showError(error instanceof Error ? error.message : '导入失败', showAlert); + } + }); + }; + input.click(); + }; + // 批量启用 const handleBatchEnable = async () => { if (selectedSources.size === 0) return; @@ -4209,6 +4269,20 @@ const EmbyConfigComponent = ({ > {isLoading('clearEmbyCache') ? '清除中...' : '清除所有缓存'} + + ); diff --git a/src/app/api/admin/emby/export/route.ts b/src/app/api/admin/emby/export/route.ts new file mode 100644 index 0000000..e631ef5 --- /dev/null +++ b/src/app/api/admin/emby/export/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { getConfig } from '@/lib/config'; + +export const runtime = 'nodejs'; + +export async function GET(request: NextRequest) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { error: '不支持本地存储进行管理员配置' }, + { status: 400 } + ); + } + + try { + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 仅站长可用 + if (authInfo.username !== process.env.USERNAME) { + return NextResponse.json({ error: '权限不足,仅站长可用' }, { status: 403 }); + } + + const adminConfig = await getConfig(); + const embyConfig = adminConfig.EmbyConfig || {}; + + const exportData = JSON.stringify(embyConfig, null, 2); + + return new NextResponse(exportData, { + headers: { + 'Content-Type': 'application/json', + 'Content-Disposition': `attachment; filename="emby-config-${Date.now()}.json"`, + }, + }); + } catch (error) { + return NextResponse.json( + { error: '导出失败: ' + (error as Error).message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/emby/import/route.ts b/src/app/api/admin/emby/import/route.ts new file mode 100644 index 0000000..ecd20c1 --- /dev/null +++ b/src/app/api/admin/emby/import/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { getConfig } from '@/lib/config'; +import { db } from '@/lib/db'; + +export const runtime = 'nodejs'; + +export async function POST(request: NextRequest) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { error: '不支持本地存储进行管理员配置' }, + { status: 400 } + ); + } + + try { + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 仅站长可用 + if (authInfo.username !== process.env.USERNAME) { + return NextResponse.json({ error: '权限不足,仅站长可用' }, { status: 403 }); + } + + const body = await request.json(); + const { data } = body; + + if (!data) { + return NextResponse.json({ error: '缺少导入数据' }, { status: 400 }); + } + + const adminConfig = await getConfig(); + + // 追加和覆盖:合并Sources数组 + if (data.Sources && Array.isArray(data.Sources)) { + const existingSources = adminConfig.EmbyConfig?.Sources || []; + const existingKeys = new Set(existingSources.map(s => s.key)); + + // 覆盖已存在的,追加新的 + const mergedSources = [...existingSources]; + for (const importSource of data.Sources) { + const existingIndex = mergedSources.findIndex(s => s.key === importSource.key); + if (existingIndex >= 0) { + mergedSources[existingIndex] = importSource; + } else { + mergedSources.push(importSource); + } + } + + adminConfig.EmbyConfig = { + ...adminConfig.EmbyConfig, + Sources: mergedSources, + }; + } else { + // 旧格式:直接覆盖 + adminConfig.EmbyConfig = { + ...adminConfig.EmbyConfig, + ...data, + }; + } + + await db.saveAdminConfig(adminConfig); + + return NextResponse.json({ + success: true, + message: '导入成功', + }); + } catch (error) { + return NextResponse.json( + { error: '导入失败: ' + (error as Error).message }, + { status: 500 } + ); + } +}