增加漫画展馆功能
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function getAuthorizedUsername(request: NextRequest): Promise<string | NextResponse> {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (authInfo.username !== process.env.USERNAME) {
|
||||
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||
if (!userInfoV2) {
|
||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||
}
|
||||
if (userInfoV2.banned) {
|
||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
return authInfo.username;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { suwayomiClient } from '@/lib/suwayomi.client';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mangaId = searchParams.get('mangaId')?.trim();
|
||||
const sourceId = searchParams.get('sourceId')?.trim();
|
||||
|
||||
if (!mangaId || !sourceId) {
|
||||
return NextResponse.json({ error: '缺少 mangaId 或 sourceId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const detail = await suwayomiClient.getMangaDetail({
|
||||
mangaId,
|
||||
sourceId,
|
||||
title: searchParams.get('title') || undefined,
|
||||
cover: searchParams.get('cover') || undefined,
|
||||
sourceName: searchParams.get('sourceName') || undefined,
|
||||
description: searchParams.get('description') || undefined,
|
||||
author: searchParams.get('author') || undefined,
|
||||
status: searchParams.get('status') || undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(detail);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { MangaReadRecord } from '@/lib/manga.types';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, mangaId] = key.split('+');
|
||||
if (!sourceId || !mangaId) {
|
||||
return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
}
|
||||
const record = await db.getMangaReadRecord(username, sourceId, mangaId);
|
||||
return NextResponse.json(record, { status: 200 });
|
||||
}
|
||||
|
||||
const records = await db.getAllMangaReadRecords(username);
|
||||
return NextResponse.json(records, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { key, record }: { key: string; record: MangaReadRecord } = await request.json();
|
||||
if (!key || !record?.chapterId) {
|
||||
return NextResponse.json({ error: 'Missing key or record' }, { status: 400 });
|
||||
}
|
||||
|
||||
const [sourceId, mangaId] = key.split('+');
|
||||
if (!sourceId || !mangaId) {
|
||||
return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.saveMangaReadRecord(username, sourceId, mangaId, {
|
||||
...record,
|
||||
saveTime: record.saveTime ?? Date.now(),
|
||||
});
|
||||
|
||||
if ((db as any).storage.cleanupOldMangaReadRecords) {
|
||||
(db as any).storage.cleanupOldMangaReadRecords(username).catch((err: Error) => {
|
||||
console.error('异步清理漫画阅读历史失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, mangaId] = key.split('+');
|
||||
if (!sourceId || !mangaId) {
|
||||
return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
}
|
||||
await db.deleteMangaReadRecord(username, sourceId, mangaId);
|
||||
} else {
|
||||
const all = await db.getAllMangaReadRecords(username);
|
||||
await Promise.all(Object.keys(all).map(async (itemKey) => {
|
||||
const [sourceId, mangaId] = itemKey.split('+');
|
||||
if (sourceId && mangaId) {
|
||||
await db.deleteMangaReadRecord(username, sourceId, mangaId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
import { getSuwayomiConfig } from '@/lib/suwayomi.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
function resolveUpstreamUrl(serverBaseUrl: string, pathOrUrl: string): string {
|
||||
if (/^https?:\/\//i.test(pathOrUrl)) {
|
||||
const target = new URL(pathOrUrl);
|
||||
const base = new URL(serverBaseUrl);
|
||||
if (target.origin !== base.origin) {
|
||||
throw new Error('不允许代理非当前 Suwayomi 服务的地址');
|
||||
}
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
if (!pathOrUrl.startsWith('/')) {
|
||||
pathOrUrl = `/${pathOrUrl}`;
|
||||
}
|
||||
|
||||
return `${serverBaseUrl}${pathOrUrl}`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const pathOrUrl = new URL(request.url).searchParams.get('path')?.trim();
|
||||
if (!pathOrUrl) {
|
||||
return NextResponse.json({ error: '缺少 path 参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const config = await getSuwayomiConfig();
|
||||
const upstreamUrl = resolveUpstreamUrl(config.serverBaseUrl, pathOrUrl);
|
||||
const response = await fetch(upstreamUrl, {
|
||||
headers: config.token ? { Authorization: `Bearer ${config.token}` } : undefined,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Suwayomi 图片请求失败: ${response.status}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const contentType = response.headers.get('content-type');
|
||||
const cacheControl = response.headers.get('cache-control');
|
||||
if (contentType) headers.set('content-type', contentType);
|
||||
headers.set('cache-control', cacheControl || 'public, max-age=300');
|
||||
|
||||
return new NextResponse(response.body, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : '图片代理失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { suwayomiClient } from '@/lib/suwayomi.client';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const chapterId = new URL(request.url).searchParams.get('chapterId')?.trim();
|
||||
if (!chapterId) {
|
||||
return NextResponse.json({ error: '缺少 chapterId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const pages = await suwayomiClient.getChapterPages(chapterId);
|
||||
return NextResponse.json({ pages });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { suwayomiClient } from '@/lib/suwayomi.client';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(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;
|
||||
const page = Number(searchParams.get('page') || '1');
|
||||
|
||||
if (!q) {
|
||||
return NextResponse.json({ results: [] });
|
||||
}
|
||||
|
||||
const results = await suwayomiClient.searchManga(q, sourceId, page);
|
||||
return NextResponse.json({ results });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { MangaShelfItem } from '@/lib/manga.types';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, mangaId] = key.split('+');
|
||||
if (!sourceId || !mangaId) {
|
||||
return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
}
|
||||
const item = await db.getMangaShelf(username, sourceId, mangaId);
|
||||
return NextResponse.json(item, { status: 200 });
|
||||
}
|
||||
|
||||
const records = await db.getAllMangaShelf(username);
|
||||
return NextResponse.json(records, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const { key, item }: { key: string; item: MangaShelfItem } = await request.json();
|
||||
if (!key || !item?.title) {
|
||||
return NextResponse.json({ error: 'Missing key or item' }, { status: 400 });
|
||||
}
|
||||
|
||||
const [sourceId, mangaId] = key.split('+');
|
||||
if (!sourceId || !mangaId) {
|
||||
return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.saveMangaShelf(username, sourceId, mangaId, {
|
||||
...item,
|
||||
saveTime: item.saveTime ?? Date.now(),
|
||||
});
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const key = new URL(request.url).searchParams.get('key');
|
||||
if (key) {
|
||||
const [sourceId, mangaId] = key.split('+');
|
||||
if (!sourceId || !mangaId) {
|
||||
return NextResponse.json({ error: 'Invalid key format' }, { status: 400 });
|
||||
}
|
||||
await db.deleteMangaShelf(username, sourceId, mangaId);
|
||||
} else {
|
||||
const all = await db.getAllMangaShelf(username);
|
||||
await Promise.all(Object.keys(all).map(async (itemKey) => {
|
||||
const [sourceId, mangaId] = itemKey.split('+');
|
||||
if (sourceId && mangaId) {
|
||||
await db.deleteMangaShelf(username, sourceId, mangaId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { suwayomiClient } from '@/lib/suwayomi.client';
|
||||
|
||||
import { getAuthorizedUsername } from '../_utils';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const username = await getAuthorizedUsername(request);
|
||||
if (username instanceof NextResponse) return username;
|
||||
|
||||
try {
|
||||
const lang = new URL(request.url).searchParams.get('lang') || process.env.SUWAYOMI_DEFAULT_LANG || 'zh';
|
||||
const sources = await suwayomiClient.getSources(lang);
|
||||
return NextResponse.json({ sources });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user