From ce805adb0825007e6608b8f6ad17333025a055ad Mon Sep 17 00:00:00 2001 From: fevirtus Date: Thu, 5 Mar 2026 18:02:11 +0700 Subject: [PATCH] Add EPUB upload + DB integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add server-side EPUB import and integrate Prisma + Mongo for novels/chapters. Introduces a new moderator API route (app/api/mod/epub/route.ts) that parses .epub files, creates a novel record in Prisma, and inserts chapter documents into MongoDB via the Chapter Mongoose model. Frontend: novel management UI now supports EPUB upload (app/mod/truyen/novel-client.tsx) with progress/toasts and preserves the manual 'Add novel' dialog. Convert app pages to fetch real data from Prisma and Mongo (app/page.tsx, app/truyen/[slug]/page.tsx, app/truyen/[slug]/[chapterId]/page.tsx), adapt types/props to use authorName, and adjust chapter/comment IDs to use Mongo _id strings. Minor fixes: TTS player logs playback errors, UI text fixes (e.g. "Chương"), and novel-card/other components updated for authorName. package.json updated with epub2, html-to-text and types; pnpm lock updated. Adds tsconfig.tsbuildinfo. --- app/api/mod/epub/route.ts | 120 ++++++++++++++ app/mod/truyen/novel-client.tsx | 128 ++++++++++----- app/page.tsx | 98 +++++++----- app/truyen/[slug]/[chapterId]/page.tsx | 44 +++--- app/truyen/[slug]/page.tsx | 65 +++++--- app/tu-sach/page.tsx | 2 +- components/novel-card.tsx | 19 ++- components/tts-player.tsx | 1 + lib/data.ts | 26 +-- lib/types.ts | 2 +- package.json | 3 + pnpm-lock.yaml | 211 +++++++++++++++++++++++++ tsconfig.tsbuildinfo | 1 + 13 files changed, 582 insertions(+), 138 deletions(-) create mode 100644 app/api/mod/epub/route.ts create mode 100644 tsconfig.tsbuildinfo diff --git a/app/api/mod/epub/route.ts b/app/api/mod/epub/route.ts new file mode 100644 index 0000000..6a9fde6 --- /dev/null +++ b/app/api/mod/epub/route.ts @@ -0,0 +1,120 @@ +import { NextResponse } from "next/server" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/lib/auth" +import { prisma } from "@/lib/prisma" +import connectToMongoDB from "@/lib/mongoose" +import { Chapter } from "@/lib/models/chapter" +import path from "path" +import os from "os" +import { promises as fs } from "fs" +import { convert } from "html-to-text" + +export async function POST(req: Request) { + const session = await getServerSession(authOptions) + if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + + try { + const formData = await req.formData() + const epubFile = formData.get("file") as File + if (!epubFile) { + return NextResponse.json({ error: "Thiếu file EPUB" }, { status: 400 }) + } + + const buffer = Buffer.from(await epubFile.arrayBuffer()) + const tempFilePath = path.join(os.tmpdir(), `upload-${Date.now()}.epub`) + await fs.writeFile(tempFilePath, buffer) + + // Phân tích EPUB file + const parsedData = await new Promise((resolve, reject) => { + const EPub = require("epub2").EPub || require("epub2") + const epub = new EPub(tempFilePath, "", "") + + epub.on("error", (err: any) => reject(err)) + epub.on("end", async () => { + const metadata = epub.metadata + const flow = epub.flow // TOC array + const chapters = [] + + for (let i = 0; i < flow.length; i++) { + const chapterData = flow[i] + const text = await new Promise((res) => { + epub.getChapter(chapterData.id, (err: any, d: string) => { + if (err) res("") + else res(d) + }) + }) + + if (text && text.trim().length > 0) { + const plainText = convert(text, { wordwrap: false }) + chapters.push({ + title: chapterData.title || `Chương ${i + 1}`, + content: plainText + }) + } + } + + resolve({ metadata, chapters }) + }) + + epub.parse() + }) + + // Xóa file tạm + await fs.unlink(tempFilePath).catch(() => { }) + + const { metadata, chapters } = parsedData + + let novelTitle = metadata.title || "Truyện chưa đặt tên" + let novelAuthor = metadata.creator || "Khuyết danh" + let novelDesc = metadata.description || "Chưa có giới thiệu" + + // Generate base slug + const baseSlug = novelTitle + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)+/g, "") + + let slug = baseSlug + let slugCounter = 1 + + // Đảm bảo slug là duy nhất + while (await prisma.novel.findUnique({ where: { slug } })) { + slug = `${baseSlug}-${slugCounter}` + slugCounter++ + } + + const newNovel = await prisma.novel.create({ + data: { + title: novelTitle, + slug: slug, + authorName: novelAuthor, + description: convert(novelDesc, { wordwrap: false }), // metadata metadata có thể chứa html + uploaderId: session.user.id, + totalChapters: chapters.length, + }, + }) + + // Lưu chapters xuống MongoDB + await connectToMongoDB() + const chapterDocs = chapters.map((ch: any, i: number) => ({ + novelId: newNovel.id, + number: i + 1, + title: ch.title, + content: ch.content, + views: 0 + })) + + if (chapterDocs.length > 0) { + await Chapter.insertMany(chapterDocs) + } + + return NextResponse.json(newNovel, { status: 201 }) + } catch (error: any) { + console.error("EPUB upload error:", error) + return NextResponse.json({ error: "Lỗi xử lý file EPUB", details: error.message }, { status: 500 }) + } +} diff --git a/app/mod/truyen/novel-client.tsx b/app/mod/truyen/novel-client.tsx index 5b43447..cf42668 100644 --- a/app/mod/truyen/novel-client.tsx +++ b/app/mod/truyen/novel-client.tsx @@ -13,7 +13,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" -import { BookOpen, Loader2, Plus } from "lucide-react" +import { BookOpen, Loader2, Plus, Upload } from "lucide-react" import { toast } from "sonner" import Link from "next/link" @@ -31,6 +31,7 @@ export function NovelClient() { const [loading, setLoading] = useState(true) const [openAdd, setOpenAdd] = useState(false) const [submitting, setSubmitting] = useState(false) + const [uploadingEpub, setUploadingEpub] = useState(false) // Form states const [title, setTitle] = useState("") @@ -82,6 +83,41 @@ export function NovelClient() { } } + const handleEpubUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + if (!file.name.endsWith('.epub')) { + toast.error("Vui lòng chọn file định dạng .epub") + e.target.value = "" // Reset input + return + } + + setUploadingEpub(true) + const formData = new FormData() + formData.append("file", file) + + try { + const res = await fetch("/api/mod/epub", { + method: "POST", + body: formData, + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.error || "Lỗi khi tải lên EPUB") + } + + toast.success("Phân tích và xuất bản EPUB thành công!") + fetchNovels() + } catch (err: any) { + toast.error(err.message || "Có lỗi xảy ra khi xử lý file EPUB") + } finally { + setUploadingEpub(false) + e.target.value = "" // Reset input + } + } + return (
@@ -89,41 +125,61 @@ export function NovelClient() { Quản lý truyện - - - - - - - Thêm Truyện Mới - - Nhập thông tin cơ bản cho đầu truyện mới của bạn. - - -
-
- - setTitle(e.target.value)} placeholder="Ví dụ: Phàm Nhân Tu Tiên" autoFocus /> -
-
- - setAuthorName(e.target.value)} placeholder="Ví dụ: Vong Ngữ" /> -
-
- -