Add EPUB upload + DB integration

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.
This commit is contained in:
2026-03-05 18:02:11 +07:00
parent 112e8604e2
commit ce805adb08
13 changed files with 582 additions and 138 deletions
+120
View File
@@ -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<any>((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<string>((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 })
}
}