legado初步支持
This commit is contained in:
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { BookSource, BookSourceCapabilities } from '@/lib/book.types';
|
||||
import { db } from '@/lib/db';
|
||||
import { legadoClient } from '@/lib/legado.client';
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
@@ -10,6 +11,7 @@ export const runtime = 'nodejs';
|
||||
interface TestSourceInput {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: 'opds' | 'legado';
|
||||
url?: string;
|
||||
enabled?: boolean;
|
||||
authMode?: 'none' | 'basic' | 'header';
|
||||
@@ -20,6 +22,7 @@ interface TestSourceInput {
|
||||
searchTemplate?: string;
|
||||
preferFormat?: Array<'epub' | 'pdf'>;
|
||||
language?: string;
|
||||
legado?: BookSource['legado'];
|
||||
}
|
||||
|
||||
async function ensureAdmin(request: NextRequest) {
|
||||
@@ -39,6 +42,7 @@ async function ensureAdmin(request: NextRequest) {
|
||||
}
|
||||
|
||||
async function detectCapabilitiesFromSource(source: BookSource): Promise<BookSourceCapabilities> {
|
||||
if (source.type === 'legado') return legadoClient.detectCapabilitiesFromSource(source);
|
||||
try {
|
||||
const result = await opdsClient.getCatalogFromSource(source);
|
||||
return {
|
||||
@@ -70,7 +74,7 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const inputSources = (body?.Sources || []) as TestSourceInput[];
|
||||
if (!Array.isArray(inputSources) || inputSources.length === 0) {
|
||||
return NextResponse.json({ success: false, message: '请至少填写一个 OPDS 书源' }, { status: 400 });
|
||||
return NextResponse.json({ success: false, message: '请至少填写一个电子书源' }, { status: 400 });
|
||||
}
|
||||
|
||||
const sources: BookSource[] = inputSources
|
||||
@@ -78,6 +82,7 @@ export async function POST(request: NextRequest) {
|
||||
.map((item, index) => ({
|
||||
id: item.id?.trim() || `source_${index + 1}`,
|
||||
name: item.name?.trim() || `书源 ${index + 1}`,
|
||||
type: item.type || 'opds',
|
||||
url: (item.url || '').trim(),
|
||||
enabled: item.enabled !== false,
|
||||
authMode: item.authMode || 'none',
|
||||
@@ -88,6 +93,7 @@ export async function POST(request: NextRequest) {
|
||||
searchTemplate: item.searchTemplate?.trim() || '',
|
||||
preferFormat: item.preferFormat || ['epub', 'pdf'],
|
||||
language: item.language?.trim() || '',
|
||||
legado: item.legado,
|
||||
}));
|
||||
|
||||
if (sources.length === 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../_utils';
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
|
||||
if (!sourceId) {
|
||||
return NextResponse.json({ error: '缺少 sourceId' }, { status: 400 });
|
||||
}
|
||||
const result = await opdsClient.getCatalog(sourceId, href);
|
||||
const result = await bookProvider.getCatalog(sourceId, href);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { BookAcquisitionLink } from '@/lib/book.types';
|
||||
import { db } from '@/lib/db';
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../_utils';
|
||||
|
||||
@@ -49,7 +49,7 @@ async function resolveDetail(username: string, payload: DetailPayload) {
|
||||
}]
|
||||
: undefined;
|
||||
|
||||
const detail = await opdsClient.getBookDetail(sourceId, href, {
|
||||
const detail = await bookProvider.getBookDetail(sourceId, href, {
|
||||
id: bookId,
|
||||
title: payload.title || shelfItem?.title || readRecord?.title || undefined,
|
||||
author: payload.author || shelfItem?.author || readRecord?.author || undefined,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../_utils';
|
||||
|
||||
@@ -11,12 +11,13 @@ type FilePayload = {
|
||||
sourceId?: string;
|
||||
bookId?: string;
|
||||
href?: string;
|
||||
format?: 'epub' | 'pdf' | null;
|
||||
format?: 'epub' | 'pdf' | 'chapters' | null;
|
||||
};
|
||||
|
||||
async function resolveFileHref(username: string, payload: FilePayload): Promise<{ sourceId: string; href: string }> {
|
||||
const sourceId = payload.sourceId?.trim();
|
||||
if (!sourceId) throw new Error('缺少 sourceId');
|
||||
if (payload.format === 'chapters') throw new Error('章节型书源不提供文件下载');
|
||||
|
||||
if (payload.href?.trim()) {
|
||||
return { sourceId, href: payload.href.trim() };
|
||||
@@ -35,9 +36,10 @@ async function resolveFileHref(username: string, payload: FilePayload): Promise<
|
||||
const detailHref = shelfItem?.detailHref || readRecord?.detailHref;
|
||||
if (!detailHref) throw new Error('找不到可下载文件');
|
||||
|
||||
const preferred = await opdsClient.getPreferredAcquisition(sourceId, detailHref);
|
||||
const preferred = await bookProvider.getPreferredAcquisition(sourceId, detailHref);
|
||||
if (preferred.format === 'chapters') throw new Error('章节型书源不提供文件下载');
|
||||
if (payload.format && preferred.format !== payload.format) {
|
||||
const detail = await opdsClient.getBookDetail(sourceId, detailHref);
|
||||
const detail = await bookProvider.getBookDetail(sourceId, detailHref);
|
||||
const matched = detail.acquisitionLinks.find((item) => (payload.format === 'pdf' ? item.type.toLowerCase().includes('pdf') : item.type.toLowerCase().includes('epub')));
|
||||
if (!matched?.href) throw new Error('找不到对应格式文件');
|
||||
return { sourceId, href: matched.href };
|
||||
@@ -47,7 +49,7 @@ async function resolveFileHref(username: string, payload: FilePayload): Promise<
|
||||
}
|
||||
|
||||
async function proxyFile(request: NextRequest, sourceId: string, href: string) {
|
||||
const source = await opdsClient.getSourceById(sourceId);
|
||||
const source = await bookProvider.getSourceById(sourceId);
|
||||
const headers = new Headers();
|
||||
if (source.authMode === 'basic' && source.username) {
|
||||
headers.set('Authorization', `Basic ${Buffer.from(`${source.username}:${source.password || ''}`).toString('base64')}`);
|
||||
@@ -98,7 +100,7 @@ export async function GET(request: NextRequest) {
|
||||
sourceId: searchParams.get('sourceId') || undefined,
|
||||
bookId: searchParams.get('bookId') || undefined,
|
||||
href: searchParams.get('href') || undefined,
|
||||
format: (searchParams.get('format')?.trim() as 'epub' | 'pdf' | null) || null,
|
||||
format: (searchParams.get('format')?.trim() as 'epub' | 'pdf' | 'chapters' | null) || null,
|
||||
});
|
||||
return await proxyFile(request, resolved.sourceId, resolved.href);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { legadoClient } from '@/lib/legado.client';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedBooksUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const sourceId = searchParams.get('sourceId')?.trim();
|
||||
const href = searchParams.get('href')?.trim();
|
||||
const tocHref = searchParams.get('tocHref')?.trim() || undefined;
|
||||
if (!sourceId || !href) return NextResponse.json({ error: '缺少 sourceId 或 href' }, { status: 400 });
|
||||
const chapter = await legadoClient.getChapterContent(sourceId, href, tocHref);
|
||||
return NextResponse.json(chapter);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { legadoClient } from '@/lib/legado.client';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedBooksUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const sourceId = searchParams.get('sourceId')?.trim();
|
||||
const bookId = searchParams.get('bookId')?.trim();
|
||||
const href = searchParams.get('href')?.trim() || '';
|
||||
|
||||
if (!sourceId) return NextResponse.json({ error: '缺少 sourceId' }, { status: 400 });
|
||||
|
||||
const chapters = bookId
|
||||
? await legadoClient.getChaptersByBookId(sourceId, bookId)
|
||||
: href
|
||||
? await legadoClient.getChapters(sourceId, href)
|
||||
: null;
|
||||
if (!chapters) return NextResponse.json({ error: '缺少 bookId 或 href,无法定位章节目录' }, { status: 400 });
|
||||
return NextResponse.json({ chapters }, { headers: { 'Cache-Control': 'no-store' } });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { BookAcquisitionLink } from '@/lib/book.types';
|
||||
import { db } from '@/lib/db';
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../../_utils';
|
||||
|
||||
@@ -13,7 +13,7 @@ type ManifestPayload = {
|
||||
bookId?: string;
|
||||
href?: string;
|
||||
acquisitionHref?: string;
|
||||
format?: 'epub' | 'pdf' | null;
|
||||
format?: 'epub' | 'pdf' | 'chapters' | null;
|
||||
title?: string;
|
||||
author?: string;
|
||||
cover?: string;
|
||||
@@ -45,12 +45,12 @@ async function resolveManifest(username: string, payload: ManifestPayload) {
|
||||
const fallbackAcquisitionLinks: BookAcquisitionLink[] = resolvedAcquisitionHref
|
||||
? [{
|
||||
rel: 'http://opds-spec.org/acquisition',
|
||||
type: resolvedFormat === 'pdf' ? 'application/pdf' : 'application/epub+zip',
|
||||
type: resolvedFormat === 'chapters' ? 'application/x-legado-chapters+json' : resolvedFormat === 'pdf' ? 'application/pdf' : 'application/epub+zip',
|
||||
href: resolvedAcquisitionHref,
|
||||
}]
|
||||
: [];
|
||||
|
||||
const detail = await opdsClient.getBookDetail(sourceId, resolvedHref || '', {
|
||||
const detail = await bookProvider.getBookDetail(sourceId, resolvedHref || '', {
|
||||
id: bookId || resolvedAcquisitionHref || undefined,
|
||||
title: payload.title || existingRecord?.title || shelfItem?.title || undefined,
|
||||
author: payload.author || existingRecord?.author || shelfItem?.author || undefined,
|
||||
@@ -60,9 +60,9 @@ async function resolveManifest(username: string, payload: ManifestPayload) {
|
||||
acquisitionLinks: fallbackAcquisitionLinks,
|
||||
});
|
||||
const preferred = resolvedHref
|
||||
? await opdsClient.getPreferredAcquisition(sourceId, resolvedHref)
|
||||
? await bookProvider.getPreferredAcquisition(sourceId, resolvedHref)
|
||||
: {
|
||||
format: resolvedFormat === 'pdf' ? 'pdf' : 'epub',
|
||||
format: resolvedFormat === 'chapters' ? 'chapters' : resolvedFormat === 'pdf' ? 'pdf' : 'epub',
|
||||
href: resolvedAcquisitionHref || '',
|
||||
};
|
||||
const lastRecord = await db.getBookReadRecord(username, sourceId, detail.id);
|
||||
@@ -70,7 +70,8 @@ async function resolveManifest(username: string, payload: ManifestPayload) {
|
||||
return NextResponse.json({
|
||||
book: detail,
|
||||
format: preferred.format,
|
||||
fileUrl: `/api/books/file?sourceId=${encodeURIComponent(sourceId)}&bookId=${encodeURIComponent(detail.id)}&format=${encodeURIComponent(preferred.format)}`,
|
||||
fileUrl: preferred.format === 'chapters' ? undefined : `/api/books/file?sourceId=${encodeURIComponent(sourceId)}&bookId=${encodeURIComponent(detail.id)}&format=${encodeURIComponent(preferred.format)}`,
|
||||
chaptersUrl: preferred.format === 'chapters' ? `/api/books/read/chapters?sourceId=${encodeURIComponent(sourceId)}&bookId=${encodeURIComponent(detail.id)}` : undefined,
|
||||
acquisitionHref: preferred.href,
|
||||
cacheKey: `${sourceId}::${detail.id}::${preferred.format}`,
|
||||
coverUrl: detail.cover,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../_utils';
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
|
||||
if (!q) {
|
||||
return NextResponse.json({ results: [], failedSources: [] });
|
||||
}
|
||||
const result = await opdsClient.searchBooks(q, sourceId);
|
||||
const result = await bookProvider.searchBooks(q, sourceId);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../../_utils';
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function GET(request: NextRequest) {
|
||||
};
|
||||
|
||||
try {
|
||||
const sources = await opdsClient.getSearchSources(sourceId);
|
||||
const sources = await bookProvider.getSearchSources(sourceId);
|
||||
let completedSources = 0;
|
||||
let totalResults = 0;
|
||||
const failedSources: Array<{ sourceId: string; sourceName: string; error: string }> = [];
|
||||
@@ -47,7 +47,7 @@ export async function GET(request: NextRequest) {
|
||||
await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
try {
|
||||
const result = await opdsClient.searchBooksSource(q, source);
|
||||
const result = await bookProvider.searchBooksSource(q, source);
|
||||
completedSources += 1;
|
||||
totalResults += result.results.length;
|
||||
send({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { bookProvider } from '@/lib/book-provider';
|
||||
|
||||
import { getAuthorizedBooksUsername } from '../_utils';
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function GET(request: NextRequest) {
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const sources = await opdsClient.getSources();
|
||||
const sources = await bookProvider.getSources();
|
||||
return NextResponse.json({ sources });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
|
||||
Reference in New Issue
Block a user