Initial commit
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"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<ImportResult> {
|
||||
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<string, string>();
|
||||
// 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<string>();
|
||||
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<ImportResult> {
|
||||
const parsed = parseLines(text);
|
||||
const lines: ImportPreviewLine[] = [];
|
||||
let added = 0, skipped = 0, errors = 0;
|
||||
const newCategoriesSet = new Set<string>();
|
||||
|
||||
// Cache existing categories by lowercased name → id.
|
||||
const catCache = new Map<string, number>();
|
||||
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<number>();
|
||||
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) };
|
||||
}
|
||||
Reference in New Issue
Block a user