新增电子书架
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { BookSource, BookSourceCapabilities } from '@/lib/book.types';
|
||||
import { db } from '@/lib/db';
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
interface TestSourceInput {
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
enabled?: boolean;
|
||||
authMode?: 'none' | 'basic' | 'header';
|
||||
username?: string;
|
||||
password?: string;
|
||||
headerName?: string;
|
||||
headerValue?: string;
|
||||
searchTemplate?: string;
|
||||
preferFormat?: Array<'epub' | 'pdf'>;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
async function ensureAdmin(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo?.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (authInfo.username !== process.env.USERNAME) {
|
||||
const userInfo = await db.getUserInfoV2(authInfo.username);
|
||||
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner') || userInfo.banned) {
|
||||
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
return authInfo.username;
|
||||
}
|
||||
|
||||
async function detectCapabilitiesFromSource(source: BookSource): Promise<BookSourceCapabilities> {
|
||||
try {
|
||||
const result = await opdsClient.getCatalogFromSource(source);
|
||||
return {
|
||||
searchSupported: !!source.searchTemplate || result.searchHref !== undefined,
|
||||
catalogSupported: result.navigation.length > 0 || result.entries.length > 0,
|
||||
searchMode: result.searchHref ? 'opds' : source.searchTemplate ? 'template' : 'disabled',
|
||||
catalogMode: result.navigation.length > 0 ? 'navigation' : result.entries.length > 0 ? 'flat' : 'disabled',
|
||||
acquisitionTypes: Array.from(new Set(result.entries.flatMap((item) => item.acquisitionLinks.map((link) => link.type)))),
|
||||
lastCheckedAt: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
searchSupported: !!source.searchTemplate,
|
||||
catalogSupported: false,
|
||||
searchMode: source.searchTemplate ? 'template' : 'disabled',
|
||||
catalogMode: 'disabled',
|
||||
acquisitionTypes: [],
|
||||
lastCheckedAt: Date.now(),
|
||||
lastError: error instanceof Error ? error.message : '测试失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const ensured = await ensureAdmin(request);
|
||||
if (ensured instanceof NextResponse) return ensured;
|
||||
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
|
||||
const sources: BookSource[] = inputSources
|
||||
.filter((item) => item?.url?.trim())
|
||||
.map((item, index) => ({
|
||||
id: item.id?.trim() || `source_${index + 1}`,
|
||||
name: item.name?.trim() || `书源 ${index + 1}`,
|
||||
url: (item.url || '').trim(),
|
||||
enabled: item.enabled !== false,
|
||||
authMode: item.authMode || 'none',
|
||||
username: item.username?.trim() || '',
|
||||
password: item.password || '',
|
||||
headerName: item.headerName?.trim() || '',
|
||||
headerValue: item.headerValue || '',
|
||||
searchTemplate: item.searchTemplate?.trim() || '',
|
||||
preferFormat: item.preferFormat || ['epub', 'pdf'],
|
||||
language: item.language?.trim() || '',
|
||||
}));
|
||||
|
||||
if (sources.length === 0) {
|
||||
return NextResponse.json({ success: false, message: '没有可测试的有效书源地址' }, { status: 400 });
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
sources.map(async (source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
url: source.url,
|
||||
capability: await detectCapabilitiesFromSource(source),
|
||||
}))
|
||||
);
|
||||
|
||||
const successCount = results.filter((item) => item.capability.catalogSupported || item.capability.searchSupported).length;
|
||||
return NextResponse.json({
|
||||
success: successCount > 0,
|
||||
message: `测试完成,${successCount}/${results.length} 个书源可用`,
|
||||
results,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '测试连接失败',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { hasFeaturePermission } from '@/lib/permissions';
|
||||
|
||||
export async function getAuthorizedBooksUsername(request: NextRequest): Promise<string | NextResponse> {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo?.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (authInfo.username !== process.env.USERNAME) {
|
||||
const user = await db.getUserInfoV2(authInfo.username);
|
||||
if (!user || user.banned) {
|
||||
return NextResponse.json({ error: '用户不存在或已被封禁' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
const allowed = await hasFeaturePermission(authInfo.username, 'books');
|
||||
if (!allowed) {
|
||||
return NextResponse.json({ error: '无权限访问电子书功能' }, { status: 403 });
|
||||
}
|
||||
|
||||
return authInfo.username;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.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() || undefined;
|
||||
if (!sourceId) {
|
||||
return NextResponse.json({ error: '缺少 sourceId' }, { status: 400 });
|
||||
}
|
||||
const result = await opdsClient.getCatalog(sourceId, href);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { BookAcquisitionLink } from '@/lib/book.types';
|
||||
import { opdsClient } from '@/lib/opds.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() || '';
|
||||
|
||||
if (!sourceId) {
|
||||
return NextResponse.json({ error: '缺少 sourceId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const acquisitionLinksRaw = searchParams.get('acquisitionLinks');
|
||||
let acquisitionLinks: BookAcquisitionLink[] | undefined;
|
||||
if (acquisitionLinksRaw) {
|
||||
try {
|
||||
acquisitionLinks = JSON.parse(acquisitionLinksRaw) as BookAcquisitionLink[];
|
||||
} catch {
|
||||
acquisitionLinks = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const detail = await opdsClient.getBookDetail(sourceId, href, {
|
||||
id: searchParams.get('bookId') || undefined,
|
||||
title: searchParams.get('title') || undefined,
|
||||
author: searchParams.get('author') || undefined,
|
||||
cover: searchParams.get('cover') || undefined,
|
||||
summary: searchParams.get('summary') || undefined,
|
||||
detailHref: href || undefined,
|
||||
acquisitionLinks,
|
||||
});
|
||||
return NextResponse.json(detail);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.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();
|
||||
if (!sourceId || !href) {
|
||||
return NextResponse.json({ error: '缺少 sourceId 或 href' }, { status: 400 });
|
||||
}
|
||||
|
||||
const source = await opdsClient.getSourceById(sourceId);
|
||||
const headers = new Headers();
|
||||
if (source.authMode === 'basic' && source.username) {
|
||||
headers.set('Authorization', `Basic ${Buffer.from(`${source.username}:${source.password || ''}`).toString('base64')}`);
|
||||
} else if (source.authMode === 'header' && source.headerName && source.headerValue) {
|
||||
headers.set(source.headerName, source.headerValue);
|
||||
}
|
||||
const range = request.headers.get('range');
|
||||
if (range) headers.set('Range', range);
|
||||
|
||||
const response = await fetch(href, {
|
||||
headers,
|
||||
redirect: 'follow',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json({ error: `文件代理失败: ${response.status}` }, { status: response.status });
|
||||
}
|
||||
|
||||
const outHeaders = new Headers();
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const acceptRanges = response.headers.get('accept-ranges');
|
||||
const contentRange = response.headers.get('content-range');
|
||||
const disposition = response.headers.get('content-disposition');
|
||||
if (contentType) outHeaders.set('Content-Type', contentType);
|
||||
if (contentLength) outHeaders.set('Content-Length', contentLength);
|
||||
if (acceptRanges) outHeaders.set('Accept-Ranges', acceptRanges);
|
||||
if (contentRange) outHeaders.set('Content-Range', contentRange);
|
||||
if (disposition) outHeaders.set('Content-Disposition', disposition);
|
||||
outHeaders.set('Cache-Control', 'private, max-age=300');
|
||||
|
||||
return new NextResponse(response.body, {
|
||||
status: response.status,
|
||||
headers: outHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { BookReadRecord } from '@/lib/book.types';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
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 key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, bookId] = key.split('+');
|
||||
if (!sourceId || !bookId) return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
const record = await db.getBookReadRecord(username, sourceId, bookId);
|
||||
return NextResponse.json(record, { status: 200 });
|
||||
}
|
||||
|
||||
const records = await db.getAllBookReadRecords(username);
|
||||
return NextResponse.json(records, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const username = await getAuthorizedBooksUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { key, record }: { key: string; record: BookReadRecord } = await request.json();
|
||||
if (!key || !record?.locator?.value) {
|
||||
return NextResponse.json({ error: 'Missing key or record' }, { status: 400 });
|
||||
}
|
||||
const [sourceId, bookId] = key.split('+');
|
||||
if (!sourceId || !bookId) return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
|
||||
const shelfItem = await db.getBookShelf(username, sourceId, bookId);
|
||||
const existingRecord = await db.getBookReadRecord(username, sourceId, bookId);
|
||||
const normalizedRecord: BookReadRecord = {
|
||||
...record,
|
||||
sourceId: record.sourceId || sourceId,
|
||||
bookId: record.bookId || bookId,
|
||||
sourceName: record.sourceName || shelfItem?.sourceName || existingRecord?.sourceName || '',
|
||||
detailHref: record.detailHref || shelfItem?.detailHref || existingRecord?.detailHref,
|
||||
acquisitionHref: record.acquisitionHref || shelfItem?.acquisitionHref || existingRecord?.acquisitionHref,
|
||||
author: record.author || shelfItem?.author || existingRecord?.author,
|
||||
cover: record.cover || shelfItem?.cover || existingRecord?.cover,
|
||||
saveTime: record.saveTime ?? Date.now(),
|
||||
};
|
||||
await db.saveBookReadRecord(username, sourceId, bookId, normalizedRecord);
|
||||
|
||||
if (shelfItem) {
|
||||
await db.saveBookShelf(username, sourceId, bookId, {
|
||||
...shelfItem,
|
||||
format: normalizedRecord.format,
|
||||
progressPercent: normalizedRecord.progressPercent,
|
||||
lastReadTime: normalizedRecord.saveTime,
|
||||
lastLocatorType: normalizedRecord.locator.type,
|
||||
lastLocatorValue: normalizedRecord.locator.value,
|
||||
lastChapterTitle: normalizedRecord.chapterTitle || normalizedRecord.locator.chapterTitle,
|
||||
});
|
||||
}
|
||||
|
||||
if ((db as any).storage.cleanupOldBookReadRecords) {
|
||||
(db as any).storage.cleanupOldBookReadRecords(username).catch((err: Error) => {
|
||||
console.error('异步清理电子书阅读历史失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const username = await getAuthorizedBooksUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, bookId] = key.split('+');
|
||||
if (!sourceId || !bookId) return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
await db.deleteBookReadRecord(username, sourceId, bookId);
|
||||
} else {
|
||||
const all = await db.getAllBookReadRecords(username);
|
||||
await Promise.all(Object.keys(all).map(async (itemKey) => {
|
||||
const [sourceId, bookId] = itemKey.split('+');
|
||||
if (sourceId && bookId) await db.deleteBookReadRecord(username, sourceId, bookId);
|
||||
}));
|
||||
}
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { BookAcquisitionLink } from '@/lib/book.types';
|
||||
import { opdsClient } from '@/lib/opds.client';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
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 acquisitionHref = searchParams.get('acquisitionHref')?.trim();
|
||||
const format = searchParams.get('format')?.trim() as 'epub' | 'pdf' | null;
|
||||
const bookId = searchParams.get('bookId')?.trim();
|
||||
|
||||
if (!sourceId) {
|
||||
return NextResponse.json({ error: '缺少 sourceId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const existingRecord = bookId ? await db.getBookReadRecord(username, sourceId, bookId) : null;
|
||||
const shelfItem = bookId ? await db.getBookShelf(username, sourceId, bookId) : null;
|
||||
const resolvedHref = href || existingRecord?.detailHref || shelfItem?.detailHref || '';
|
||||
const resolvedAcquisitionHref = acquisitionHref || existingRecord?.acquisitionHref || shelfItem?.acquisitionHref || '';
|
||||
const resolvedFormat = format || existingRecord?.format || shelfItem?.format || 'epub';
|
||||
|
||||
if (!resolvedHref && !resolvedAcquisitionHref) {
|
||||
return NextResponse.json({ error: '缺少 href / acquisitionHref,且历史记录中也没有可恢复的下载链接' }, { status: 400 });
|
||||
}
|
||||
|
||||
const fallbackAcquisitionLinks: BookAcquisitionLink[] = resolvedAcquisitionHref
|
||||
? [{
|
||||
rel: 'http://opds-spec.org/acquisition',
|
||||
type: resolvedFormat === 'pdf' ? 'application/pdf' : 'application/epub+zip',
|
||||
href: resolvedAcquisitionHref,
|
||||
}]
|
||||
: [];
|
||||
|
||||
const detail = await opdsClient.getBookDetail(sourceId, resolvedHref || '', {
|
||||
id: bookId || resolvedAcquisitionHref || undefined,
|
||||
title: searchParams.get('title') || existingRecord?.title || shelfItem?.title || undefined,
|
||||
author: searchParams.get('author') || existingRecord?.author || shelfItem?.author || undefined,
|
||||
cover: searchParams.get('cover') || existingRecord?.cover || shelfItem?.cover || undefined,
|
||||
summary: searchParams.get('summary') || undefined,
|
||||
detailHref: resolvedHref || undefined,
|
||||
acquisitionLinks: fallbackAcquisitionLinks,
|
||||
});
|
||||
const preferred = resolvedHref
|
||||
? await opdsClient.getPreferredAcquisition(sourceId, resolvedHref)
|
||||
: {
|
||||
format: resolvedFormat === 'pdf' ? 'pdf' : 'epub',
|
||||
href: resolvedAcquisitionHref || '',
|
||||
};
|
||||
const lastRecord = await db.getBookReadRecord(username, sourceId, detail.id);
|
||||
|
||||
return NextResponse.json({
|
||||
book: detail,
|
||||
format: preferred.format,
|
||||
fileUrl: `/api/books/file?sourceId=${encodeURIComponent(sourceId)}&href=${encodeURIComponent(preferred.href)}`,
|
||||
acquisitionHref: preferred.href,
|
||||
cacheKey: `${sourceId}::${detail.id}::${preferred.href}`,
|
||||
coverUrl: detail.cover,
|
||||
lastRecord,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.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 q = searchParams.get('q')?.trim();
|
||||
const sourceId = searchParams.get('sourceId')?.trim() || undefined;
|
||||
if (!q) {
|
||||
return NextResponse.json({ results: [], failedSources: [] });
|
||||
}
|
||||
const result = await opdsClient.searchBooks(q, sourceId);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { BookShelfItem } from '@/lib/book.types';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
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 key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, bookId] = key.split('+');
|
||||
if (!sourceId || !bookId) return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
const item = await db.getBookShelf(username, sourceId, bookId);
|
||||
return NextResponse.json(item, { status: 200 });
|
||||
}
|
||||
|
||||
const records = await db.getAllBookShelf(username);
|
||||
return NextResponse.json(records, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const username = await getAuthorizedBooksUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { key, item }: { key: string; item: BookShelfItem } = await request.json();
|
||||
if (!key || !item?.title) return NextResponse.json({ error: 'Missing key or item' }, { status: 400 });
|
||||
const [sourceId, bookId] = key.split('+');
|
||||
if (!sourceId || !bookId) return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
|
||||
await db.saveBookShelf(username, sourceId, bookId, {
|
||||
...item,
|
||||
sourceId: item.sourceId || sourceId,
|
||||
bookId: item.bookId || bookId,
|
||||
saveTime: item.saveTime ?? Date.now(),
|
||||
});
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const username = await getAuthorizedBooksUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, bookId] = key.split('+');
|
||||
if (!sourceId || !bookId) return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
await db.deleteBookShelf(username, sourceId, bookId);
|
||||
} else {
|
||||
const all = await db.getAllBookShelf(username);
|
||||
await Promise.all(Object.keys(all).map(async (itemKey) => {
|
||||
const [sourceId, bookId] = itemKey.split('+');
|
||||
if (sourceId && bookId) await db.deleteBookShelf(username, sourceId, bookId);
|
||||
}));
|
||||
}
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { opdsClient } from '@/lib/opds.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 sources = await opdsClient.getSources();
|
||||
return NextResponse.json({ sources });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user