import { existsSync, promises as fs } from "node:fs"; import path from "node:path"; import { prisma } from "@/lib/prisma"; import { extractFurniIconPng } from "@/lib/services/clone-icon"; import { listSources } from "@/lib/services/clone-sources"; import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs"; import { downloadFile } from "@/lib/services/import/core/download"; import { ensureDirectories } from "./furni-import"; export interface RepairEvent { type: "item_progress" | "batch_complete" | "error"; classname?: string; status?: string; index?: number; message?: string; warnings?: string[]; succeeded?: number; failed?: number; duration?: number; } export async function repairMissingIcons( onEvent?: (evt: RepairEvent) => void, ): Promise<{ succeeded: number; failed: number }> { await ensureDirectories(); const { iconDir, nitroDir } = await getFurniAssetDirs(); const items = await prisma.$queryRaw< Array<{ id: number; item_name: string }> >`SELECT id, item_name FROM items_base ORDER BY id`; if (items.length === 0) { onEvent?.({ type: "batch_complete", succeeded: 0, failed: 0, duration: 0, }); return { succeeded: 0, failed: 0 }; } let succeeded = 0; let failed = 0; for (let i = 0; i < items.length; i++) { const { item_name: classname } = items[i]; const iconPath = path.join(iconDir, `${classname}_icon.png`); if (existsSync(iconPath)) continue; onEvent?.({ type: "item_progress", classname, status: "started", index: i, }); let ok = false; const warnings: string[] = []; const nitroPath = path.join(nitroDir, `${classname}.nitro`); if (existsSync(nitroPath)) { try { const nitroBuf = await fs.readFile(nitroPath); const icon = extractFurniIconPng(nitroBuf); if (icon) { await fs.writeFile(iconPath, icon); ok = true; } else { warnings.push("no icon asset in .nitro bundle"); } } catch (err) { warnings.push(`extract from .nitro failed: ${(err as Error).message}`); } } else { warnings.push(".nitro file not found on disk"); } if (!ok) { const sources = await listSources(); for (const source of sources) { try { const dl = await downloadFile( `${source.iconBaseUrl}/${classname}_icon.png`, iconPath, { maxRetries: 1, validate: "png" }, ); if (dl.ok) { ok = true; break; } } catch { warnings.push(`download from ${source.name} failed`); } } } if (ok) { succeeded++; onEvent?.({ type: "item_progress", classname, status: "done", index: i, warnings: warnings.length ? warnings : undefined, }); } else { failed++; onEvent?.({ type: "item_progress", classname, status: "failed", index: i, message: warnings[0] || "could not obtain icon", }); } } onEvent?.({ type: "batch_complete", succeeded, failed, }); return { succeeded, failed }; }