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) - })