diff --git a/app/mod/import/import-client.tsx b/app/mod/import/import-client.tsx
index bb066ed..48c25a5 100644
--- a/app/mod/import/import-client.tsx
+++ b/app/mod/import/import-client.tsx
@@ -14,11 +14,17 @@ export function ImportClient() {
const [assetQuery, setAssetQuery] = useState("")
const [debouncedQuery, setDebouncedQuery] = useState("")
const [loading, setLoading] = useState(false)
+ const [offset, setOffset] = useState(0)
+ const [limit] = useState(50)
const [selectedJobId, setSelectedJobId] = useState("")
const [mapNovelId, setMapNovelId] = useState("")
+ const [tab, setTab] = useState<"unconverted" | "converted">("unconverted")
- const unconvertedAssets = useMemo(() => assets.filter((a) => a.status !== "completed"), [assets])
+ const visibleAssets = useMemo(
+ () => tab === "unconverted" ? assets.filter((a) => a.status !== "completed") : assets,
+ [assets, tab],
+ )
useEffect(() => {
const t = setTimeout(() => setDebouncedQuery(assetQuery.trim()), 250)
@@ -28,7 +34,9 @@ export function ImportClient() {
const refresh = async () => {
setLoading(true)
try {
- const aUrl = `/api/mod-import/assets?unconvertedOnly=true&limit=200${debouncedQuery ? `&q=${encodeURIComponent(debouncedQuery)}` : ""}`
+ const aUrl = tab === "unconverted"
+ ? `/api/mod-import/assets?unconvertedOnly=true&limit=${limit}&offset=${offset}${debouncedQuery ? `&q=${encodeURIComponent(debouncedQuery)}` : ""}`
+ : `/api/mod-import/assets?status=completed&limit=${limit}&offset=${offset}${debouncedQuery ? `&q=${encodeURIComponent(debouncedQuery)}` : ""}`
const [aRes, jRes] = await Promise.all([fetch(aUrl), fetch("/api/mod-import/review-required")])
const aData = await aRes.json().catch(() => [])
const jData = await jRes.json().catch(() => [])
@@ -45,13 +53,13 @@ export function ImportClient() {
useEffect(() => {
refresh()
- }, [debouncedQuery])
+ }, [debouncedQuery, offset, tab])
const selectItem = (assetId: string, index: number, shiftKey: boolean) => {
if (shiftKey && lastIndex !== null) {
const start = Math.min(lastIndex, index)
const end = Math.max(lastIndex, index)
- const range = unconvertedAssets.slice(start, end + 1).map((x) => x.id)
+ const range = visibleAssets.slice(start, end + 1).map((x) => x.id)
setSelectedAssetIds((prev) => Array.from(new Set([...prev, ...range])))
return
}
@@ -86,6 +94,14 @@ export function ImportClient() {
refresh()
}
+ const autoReview = async () => {
+ const res = await fetch("/api/mod-import/assets/auto-review?limit=5000", { method: "POST" })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) return toast.error((data as any)?.detail || "Auto-review thất bại")
+ toast.success(`Đã phân loại ${data.processed}: approved ${data.approved}, review ${data.reviewRequired}`)
+ refresh()
+ }
+
const mapMismatch = async () => {
if (!selectedJobId || !mapNovelId) return toast.error("Chọn mismatch job và nhập novelId")
const res = await fetch(`/api/mod-import/jobs/${selectedJobId}/apply-mapping`, {
@@ -99,6 +115,25 @@ export function ImportClient() {
refresh()
}
+ const markConverted = async (assetId: string) => {
+ if (!confirm("Đánh dấu EPUB này đã convert (ẩn khỏi danh sách chưa convert)?")) return
+ const res = await fetch(`/api/mod-import/assets/${assetId}/mark-converted`, { method: "POST" })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) return toast.error((data as any)?.detail || "Mark converted thất bại")
+ toast.success("Đã đánh dấu converted")
+ setSelectedAssetIds((prev) => prev.filter((x) => x !== assetId))
+ refresh()
+ }
+
+ const unmarkConverted = async (assetId: string) => {
+ if (!confirm("Bỏ trạng thái converted để convert lại?")) return
+ const res = await fetch(`/api/mod-import/assets/${assetId}/unmark-converted`, { method: "POST" })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) return toast.error((data as any)?.detail || "Unmark thất bại")
+ toast.success("Đã unmark converted")
+ refresh()
+ }
+
const deleteMismatchJob = async (jobId: string) => {
if (!confirm("Xoá mismatch job này và xoá luôn thư mục content trên NAS?")) return
const res = await fetch(`/api/mod-import/jobs/${jobId}?removeContent=true`, { method: "DELETE" })
@@ -117,25 +152,41 @@ export function ImportClient() {
EPUB nguồn (chưa convert)
Click để chọn, Shift + click để chọn một khoảng, sau đó bấm Convert.
-
+
+
+
+
+
+
+
+
setAssetQuery(e.target.value)} />
- {unconvertedAssets.map((a, idx) => (
-
+ {visibleAssets.map((a, idx) => (
+
+
+
+ {tab === "unconverted" ? : null}
+ {tab === "converted" ? : null}
+
+
))}
- {unconvertedAssets.length === 0 &&
Không có EPUB phù hợp
}
+ {visibleAssets.length === 0 &&
Không có EPUB phù hợp
}
Đã chọn {selectedAssetIds.length} file
-
+
+
+
+
+
diff --git a/lib/models/chapter.ts b/lib/models/chapter.ts
deleted file mode 100644
index e35b112..0000000
--- a/lib/models/chapter.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import mongoose, { Schema, Document } from "mongoose"
-
-export interface IChapter extends Document {
- novelId: string // Trỏ tới ID trong PostgreSQL
- number: number
- volumeNumber?: number
- volumeTitle?: string
- volumeChapterNumber?: number
- title: string
- content: string
- views: number
- createdAt: Date
-}
-
-const ChapterSchema: Schema = new Schema({
- novelId: { type: String, required: true, index: true },
- number: { type: Number, required: true },
- volumeNumber: { type: Number, default: null },
- volumeTitle: { type: String, default: null },
- volumeChapterNumber: { type: Number, default: null },
- title: { type: String, required: true },
- content: { type: String, required: true },
- views: { type: Number, default: 0 },
- createdAt: { type: Date, default: Date.now },
-})
-
-ChapterSchema.index({ novelId: 1, number: 1 }, { unique: true })
-ChapterSchema.index({ createdAt: -1, novelId: 1 })
-
-export const Chapter = mongoose.models.Chapter || mongoose.model("Chapter", ChapterSchema)
diff --git a/lib/models/editor-recommendation.ts b/lib/models/editor-recommendation.ts
deleted file mode 100644
index f2fe559..0000000
--- a/lib/models/editor-recommendation.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import mongoose, { Schema, Document } from "mongoose"
-
-export interface IEditorRecommendation extends Document {
- novelId: string
- editorId: string
- createdAt: Date
- updatedAt: Date
-}
-
-const EditorRecommendationSchema: Schema = new Schema(
- {
- novelId: { type: String, required: true, index: true },
- editorId: { type: String, required: true, index: true },
- },
- {
- timestamps: true,
- }
-)
-
-EditorRecommendationSchema.index({ novelId: 1, editorId: 1 }, { unique: true })
-EditorRecommendationSchema.index({ createdAt: -1 })
-
-export const EditorRecommendation =
- mongoose.models.EditorRecommendation ||
- mongoose.model("EditorRecommendation", EditorRecommendationSchema)
diff --git a/lib/models/user-recommendation.ts b/lib/models/user-recommendation.ts
deleted file mode 100644
index 9a64e1f..0000000
--- a/lib/models/user-recommendation.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import mongoose, { Document, Schema } from "mongoose"
-
-export interface IUserRecommendation extends Document {
- userId: string
- novelId: string
- createdAt: Date
- updatedAt: Date
-}
-
-const UserRecommendationSchema: Schema = new Schema(
- {
- userId: { type: String, required: true, index: true },
- novelId: { type: String, required: true, index: true },
- },
- {
- timestamps: true,
- }
-)
-
-UserRecommendationSchema.index({ userId: 1, novelId: 1 }, { unique: true })
-UserRecommendationSchema.index({ createdAt: -1 })
-
-export const UserRecommendation =
- mongoose.models.UserRecommendation ||
- mongoose.model("UserRecommendation", UserRecommendationSchema)
diff --git a/lib/mongoose.ts b/lib/mongoose.ts
deleted file mode 100644
index 8f57e3d..0000000
--- a/lib/mongoose.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import mongoose from "mongoose"
-
-let cached = (global as any).mongoose
-
-if (!cached) {
- cached = (global as any).mongoose = { conn: null, promise: null }
-}
-
-async function connectToMongoDB() {
- const mongodbUri = process.env.MONGODB_URI
- if (!mongodbUri) {
- throw new Error("Please define the MONGODB_URI environment variable")
- }
-
- if (cached.conn) {
- return cached.conn
- }
-
- if (!cached.promise) {
- const opts = {
- bufferCommands: false,
- }
-
- cached.promise = mongoose.connect(mongodbUri, opts).then((mongoose) => {
- return mongoose
- })
- }
-
- try {
- cached.conn = await cached.promise
- } catch (e) {
- cached.promise = null
- throw e
- }
-
- return cached.conn
-}
-
-export default connectToMongoDB
diff --git a/package.json b/package.json
index 56fcf95..4d5e4ae 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,6 @@
"input-otp": "1.4.2",
"lucide-react": "^0.564.0",
"mammoth": "^1.11.0",
- "mongoose": "^9.2.4",
"next": "16.1.6",
"next-auth": "^4.24.13",
"next-themes": "^0.4.6",
diff --git a/scripts/wipe_db.js b/scripts/wipe_db.js
deleted file mode 100644
index adb201c..0000000
--- a/scripts/wipe_db.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const { PrismaClient } = require('@prisma/client')
-const mongoose = require('mongoose')
-require('dotenv').config({ path: '.env.local' })
-require('dotenv').config()
-
-const prisma = new PrismaClient()
-
-async function main() {
- console.log('Connecting to MongoDB...')
- // Connect to MongoDB using MONGODB_URI
- const mongoUri = process.env.MONGODB_URI
- if (!mongoUri) {
- throw new Error('MONGODB_URI is not defined in env')
- }
- await mongoose.connect(mongoUri)
-
- // Wipe MongoDB Chapters
- console.log('Wiping chapters from MongoDB...')
- try {
- const chapterSchema = new mongoose.Schema({}, { strict: false })
- const Chapter = mongoose.models.Chapter || mongoose.model('Chapter', chapterSchema, 'chapters')
- const res = await Chapter.deleteMany({})
- console.log(`Deleted ${res.deletedCount} chapters.`)
- } catch (e) {
- console.error('Error wiping mongo chapters', e)
- }
-
- // Wipe PostgreSQL Content
- console.log('Wiping Novels, Genres, Comments, Bookmarks from PostgreSQL...')
- try {
- // Delete in order to respect foreign keys if Cascade isn't perfect, but Cascade is set on most.
- await prisma.comment.deleteMany({})
- console.log('Deleted all comments.')
-
- await prisma.bookmark.deleteMany({})
- console.log('Deleted all bookmarks.')
-
- await prisma.novelGenre.deleteMany({})
- console.log('Deleted all novel_genres.')
-
- await prisma.genre.deleteMany({})
- console.log('Deleted all genres.')
-
- await prisma.novel.deleteMany({})
- console.log('Deleted all novels.')
-
- } catch (error) {
- console.error('Error wiping postgres', error)
- }
-
- console.log('Cleanup complete.')
-}
-
-main()
- .catch(console.error)
- .finally(async () => {
- await prisma.$disconnect()
- await mongoose.disconnect()
- process.exit(0)
- })