"use server"; import { revalidatePath } from "next/cache"; import { rawDb, uniqueSlug } from "@/lib/db/client"; const PALETTE = ["#fbbf24", "#22d3ee", "#a78bfa", "#f472b6", "#34d399", "#fb7185", "#f97316", "#60a5fa"]; export interface ImportPreviewLine { raw: string; name: string; altNames: string | null; categories: string[]; status: "new" | "exists" | "blank" | "error"; reason?: string; } export interface ImportResult { lines: ImportPreviewLine[]; added: number; skipped: number; errors: number; newCategories: string[]; } function parseLines(text: string): Array<{ raw: string; name: string; altNames: string | null; categories: string[] }> { const out: Array<{ raw: string; name: string; altNames: string | null; categories: string[] }> = []; for (const raw of text.split(/\r?\n/)) { const trimmed = raw.trim(); if (!trimmed) { out.push({ raw, name: "", altNames: null, categories: [] }); continue; } const parts = trimmed.split("|").map((s) => s.trim()); const [name, alt, cats] = [parts[0] ?? "", parts[1] ?? "", parts[2] ?? ""]; const categories = cats ? cats.split(/[,、,]/).map((s) => s.trim()).filter(Boolean) : []; out.push({ raw: trimmed, name, altNames: alt || null, categories }); } return out; } /** * Dry run — parse + classify lines without writing. Drives the preview UI. */ export async function previewActressImport(text: string): Promise { const parsed = parseLines(text); const lines: ImportPreviewLine[] = []; const existingCats = new Set( (rawDb.prepare(`SELECT name FROM actress_categories`).all() as Array<{ name: string }>) .map((r) => r.name.toLowerCase()), ); // Dedupe new-category collection by lowercased key to match the // commit path, which keys its catCache by toLowerCase(). Otherwise // "Action" and "ACTION" in the input would be reported as 2 new // categories in the preview but commit would create only 1. const newCategoriesByKey = new Map(); // Track within-input new actresses too: a name appearing twice should // be counted once in the preview, matching the commit (which inserts // the first and rejects/sees the second as existing). const seenNewNames = new Set(); let added = 0, skipped = 0, errors = 0; for (const p of parsed) { if (!p.name) { lines.push({ ...p, status: "blank" }); continue; } const existing = rawDb.prepare(`SELECT id FROM actresses WHERE name = ? COLLATE NOCASE`).get(p.name) as { id: number } | undefined; const nameKey = p.name.toLowerCase(); if (existing || seenNewNames.has(nameKey)) { lines.push({ ...p, status: "exists" }); skipped++; continue; } for (const c of p.categories) { const key = c.toLowerCase(); if (!existingCats.has(key) && !newCategoriesByKey.has(key)) { newCategoriesByKey.set(key, c); } } seenNewNames.add(nameKey); lines.push({ ...p, status: "new" }); added++; } return { lines, added, skipped, errors, newCategories: Array.from(newCategoriesByKey.values()) }; } /** * Commit — actually write. Auto-creates unknown categories with palette colors. */ export async function commitActressImport(text: string, defaultCategoryIds: number[] = []): Promise { const parsed = parseLines(text); const lines: ImportPreviewLine[] = []; let added = 0, skipped = 0, errors = 0; const newCategoriesSet = new Set(); // Cache existing categories by lowercased name → id. const catCache = new Map(); for (const row of rawDb.prepare(`SELECT id, name FROM actress_categories`).all() as Array<{ id: number; name: string }>) { catCache.set(row.name.toLowerCase(), row.id); } // Track NEW categories with their own counter so palette colors are // assigned in import order — using catCache.size meant the first new // category's color depended on the existing-category count and often // collided with an existing category's chosen color. let newCategoryCount = 0; // Helper to get or create a category. function getOrCreateCategory(name: string): number { const key = name.toLowerCase(); const hit = catCache.get(key); if (hit != null) return hit; const slug = uniqueSlug(rawDb, "actress_categories", name); const color = PALETTE[newCategoryCount++ % PALETTE.length]; const row = rawDb.prepare(` INSERT INTO actress_categories (name, slug, color, icon, priority, builtin) VALUES (?, ?, ?, 'tag', 50, 0) RETURNING id `).get(name, slug, color) as { id: number }; catCache.set(key, row.id); newCategoriesSet.add(name); return row.id; } const tx = rawDb.transaction(() => { for (const p of parsed) { if (!p.name) { lines.push({ ...p, status: "blank" }); continue; } const existing = rawDb.prepare(`SELECT id FROM actresses WHERE name = ? COLLATE NOCASE`).get(p.name) as { id: number } | undefined; if (existing) { lines.push({ ...p, status: "exists" }); skipped++; continue; } try { const slug = uniqueSlug(rawDb, "actresses", p.name); const row = rawDb.prepare(` INSERT INTO actresses (name, slug, alt_names) VALUES (?, ?, ?) RETURNING id `).get(p.name, slug, p.altNames) as { id: number }; const assignedCatIds = new Set(); for (const cat of p.categories) { const catId = getOrCreateCategory(cat); assignedCatIds.add(catId); } for (const id of defaultCategoryIds) assignedCatIds.add(id); for (const id of assignedCatIds) { rawDb.prepare(`INSERT OR IGNORE INTO actress_categories_map (actress_id, category_id) VALUES (?, ?)`).run(row.id, id); } lines.push({ ...p, status: "new" }); added++; } catch (e) { lines.push({ ...p, status: "error", reason: (e as Error).message }); errors++; } } }); tx(); revalidatePath("/actress"); return { lines, added, skipped, errors, newCategories: Array.from(newCategoriesSet) }; }