From 87c3da046b62c09e08480d5f840020b2c232aaaf Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 19 May 2026 19:25:56 +0800 Subject: [PATCH] =?UTF-8?q?legado=E5=88=9D=E6=AD=A5=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 252 ++++++++- src/app/api/admin/opds/route.ts | 8 +- src/app/api/books/catalog/route.ts | 4 +- src/app/api/books/detail/route.ts | 4 +- src/app/api/books/file/route.ts | 14 +- src/app/api/books/read/chapter/route.ts | 24 + src/app/api/books/read/chapters/route.ts | 31 + src/app/api/books/read/manifest/route.ts | 15 +- src/app/api/books/search/route.ts | 4 +- src/app/api/books/search/ws/route.ts | 6 +- src/app/api/books/sources/route.ts | 4 +- src/app/books/detail/page.tsx | 90 ++- src/app/books/page.tsx | 5 +- src/app/books/read/page.tsx | 155 ++++- src/components/books/BooksLayout.tsx | 2 +- src/lib/admin.types.ts | 2 + src/lib/book-provider.ts | 84 +++ src/lib/book-route-cache.client.ts | 18 +- src/lib/book.types.ts | 87 ++- src/lib/config.ts | 1 + src/lib/legado.client.ts | 687 +++++++++++++++++++++++ src/lib/opds.client.ts | 2 +- 22 files changed, 1433 insertions(+), 66 deletions(-) create mode 100644 src/app/api/books/read/chapter/route.ts create mode 100644 src/app/api/books/read/chapters/route.ts create mode 100644 src/lib/book-provider.ts create mode 100644 src/lib/legado.client.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8dda348..0d106cd 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -12217,6 +12217,8 @@ const OPDSConfigComponent = ({ const [cacheTTL, setCacheTTL] = useState(10 * 60 * 1000); const [sources, setSources] = useState([]); const [editingIndex, setEditingIndex] = useState(null); + const [legadoImportText, setLegadoImportText] = useState(''); + const [legadoRuleDrafts, setLegadoRuleDrafts] = useState>({}); useEffect(() => { if (config?.OPDSConfig) { @@ -12226,6 +12228,7 @@ const OPDSConfigComponent = ({ (config.OPDSConfig.Sources || []).map((item, index) => ({ id: item.id || `source_${index + 1}`, name: item.name || `书源 ${index + 1}`, + type: item.type || 'opds', url: item.url || '', enabled: item.enabled !== false, authMode: item.authMode || 'none', @@ -12236,9 +12239,11 @@ const OPDSConfigComponent = ({ searchTemplate: item.searchTemplate || '', preferFormat: item.preferFormat || ['epub', 'pdf'], language: item.language || '', + legado: item.legado, })) ); setEditingIndex(null); + setLegadoRuleDrafts({}); } }, [config]); @@ -12264,6 +12269,7 @@ const OPDSConfigComponent = ({ { id: `source_${prev.length + 1}`, name: `书源 ${prev.length + 1}`, + type: 'opds', url: '', enabled: true, authMode: 'none', @@ -12274,13 +12280,76 @@ const OPDSConfigComponent = ({ searchTemplate: '', preferFormat: ['epub', 'pdf'], language: '', + legado: undefined, }, ]; }); }; + const makeLegadoSourceId = (name: string, url: string, index: number) => { + const raw = `${name}|${url}|${index}`; + let hash = 0; + for (let i = 0; i < raw.length; i += 1) { + hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0; + } + return `legado_${Math.abs(hash).toString(36)}`; + }; + + const importLegadoSources = () => { + try { + const parsed = JSON.parse(legadoImportText); + const list = Array.isArray(parsed) ? parsed : [parsed]; + const imported = list + .filter((item) => item && typeof item === 'object') + .map((rule: any, index) => { + const name = rule.bookSourceName || `Legado 书源 ${index + 1}`; + const url = rule.bookSourceUrl || ''; + return { + id: makeLegadoSourceId(name, url, index), + name, + type: 'legado' as const, + url, + enabled: rule.enabled !== false, + authMode: 'none' as const, + username: '', + password: '', + headerName: '', + headerValue: '', + searchTemplate: '', + preferFormat: ['epub' as const], + language: '', + legado: rule, + } satisfies BookSource; + }) + .filter((source) => !!source.url); + + if (imported.length === 0) { + throw new Error('没有识别到有效 Legado 书源,请确认 JSON 内含 bookSourceUrl'); + } + + setSources((prev) => { + const existed = new Set(prev.map((item) => `${item.type || 'opds'}|${item.url}|${item.name}`)); + const next = imported.filter((item) => !existed.has(`${item.type}|${item.url}|${item.name}`)); + return [...prev, ...next]; + }); + setLegadoImportText(''); + showSuccess(`已导入 ${imported.length} 个 Legado 书源`, showAlert); + } catch (error) { + showError(error instanceof Error ? error.message : 'Legado JSON 解析失败', showAlert); + } + }; + const removeSource = (index: number) => { setSources((prev) => prev.filter((_, idx) => idx !== index)); + setLegadoRuleDrafts((prev) => { + const next: Record = {}; + Object.entries(prev).forEach(([key, value]) => { + const numericKey = Number(key); + if (numericKey < index) next[numericKey] = value; + if (numericKey > index) next[numericKey - 1] = value; + }); + return next; + }); setEditingIndex((prev) => { if (prev === null) return prev; if (prev === index) return null; @@ -12291,6 +12360,7 @@ const OPDSConfigComponent = ({ const normalizeSource = (source: BookSource, index: number) => ({ id: source.id?.trim() || `source_${index + 1}`, name: source.name?.trim() || `书源 ${index + 1}`, + type: source.type || 'opds', url: source.url?.trim() || '', enabled: source.enabled !== false, authMode: source.authMode || 'none', @@ -12299,13 +12369,28 @@ const OPDSConfigComponent = ({ headerName: source.authMode === 'header' ? source.headerName?.trim() || '' : '', headerValue: source.authMode === 'header' ? source.headerValue || '' : '', - searchTemplate: source.searchTemplate?.trim() || '', + searchTemplate: source.type === 'legado' ? '' : source.searchTemplate?.trim() || '', preferFormat: source.preferFormat?.length ? source.preferFormat : ['epub', 'pdf'], language: source.language?.trim() || '', + legado: source.type === 'legado' ? source.legado : undefined, }); + const updateLegadoRuleJson = (index: number, value: string) => { + setLegadoRuleDrafts((prev) => ({ ...prev, [index]: value })); + try { + const rule = JSON.parse(value); + updateSource(index, { + legado: rule, + name: rule.bookSourceName || sources[index]?.name, + url: rule.bookSourceUrl || sources[index]?.url, + }); + } catch { + // 允许用户继续编辑尚未完成的 JSON,保存前需修正为合法 JSON + } + }; + const buildConfig = () => ({ Enabled: enabled, CacheTTL: Math.max(60_000, cacheTTL || 10 * 60 * 1000), @@ -12316,6 +12401,14 @@ const OPDSConfigComponent = ({ await withLoading('saveOPDSConfig', async () => { try { if (!config) throw new Error('配置未加载'); + for (const [index, draft] of Object.entries(legadoRuleDrafts)) { + if (!draft.trim()) continue; + try { + JSON.parse(draft); + } catch { + throw new Error(`第 ${Number(index) + 1} 个 Legado 书源 JSON 格式不正确`); + } + } const response = await fetch('/api/admin/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -12385,10 +12478,10 @@ const OPDSConfigComponent = ({

- 关于电子书馆 / OPDS + 关于电子书馆 / OPDS / Legado

-

• 支持多书源,每个源可独立配置认证、搜索模板与默认格式偏好。

+

• 支持多书源,每个源可选择 OPDS 或 Legado,并独立配置规则。

• 有些源只支持分类浏览,有些源只支持搜索,测试连接会自动探测能力。

@@ -12449,6 +12542,34 @@ const OPDSConfigComponent = ({
+
+
+
+

+ 导入 Legado 书源 +

+

+ 粘贴阅读/Legado 导出的单个书源 JSON 或书源数组,导入后可逐个测试。 +

+
+ +
+