feat: add asset import services

This commit is contained in:
Simo committed 2026-07-12 19:12:25 +02:00
1 parent 4b596226e0
commit 3497df9dfd
46 files changed
+5383

No files matched your search

+205
View File
@@ -0,0 +1,205 @@
import { ensureFigureDirs, importSingleFigure } from '@/lib/services/figure-import'
import {
fetchOfficialFigureData,
fetchOfficialFigureMap,
type OfficialFigureData,
type OfficialLibrary,
type OfficialSet,
} from '@/lib/services/figure-official'
import { listLocalSetIds, mergePalette, mergeSet } from '@/lib/services/figuredata'
import { listFigureLibraries, mergeLibrary } from '@/lib/services/figuremap'
export interface ClothingSetItem {
setType: string
setId: number
gender: string
club: number
sellable: boolean
present: boolean
}
export interface ClothingSetListResult {
items: ClothingSetItem[]
meta: { page: number; perPage: number; total: number }
}
export interface ClothingSetStats {
total: number
present: number
missing: number
}
export interface ImportClothingSetResult {
ok: boolean
setId?: number
warnings: string[]
error?: string
}
function partKey(type: string, id: number): string {
return `${type}:${id}`
}
function buildPartIndex(officialLibs: OfficialLibrary[]): Map<string, OfficialLibrary> {
const index = new Map<string, OfficialLibrary>()
for (const lib of officialLibs) {
for (const part of lib.parts) {
const key = partKey(part.type, part.id)
if (!index.has(key)) {
index.set(key, lib)
}
}
}
return index
}
function mapOfficialSet(set: OfficialSet): Record<string, unknown> {
const result: Record<string, unknown> = {
id: set.id,
gender: set.gender,
club: set.club,
colorable: set.colorable,
selectable: set.selectable,
preselectable: set.preselectable,
sellable: set.sellable,
parts: set.parts.map((p) => ({
id: p.id,
type: p.type,
colorable: p.colorable,
index: p.index,
colorindex: p.colorindex,
})),
}
if (set.hiddenLayers?.length) {
result.hiddenLayers = set.hiddenLayers
}
return result
}
export async function importClothingSet(params: {
setType: string
setId: number
onProgress?: (status: string) => void
}): Promise<ImportClothingSetResult> {
const { setType, setId, onProgress } = params
const warnings: string[] = []
const [officialFd, officialLibs] = await Promise.all([
fetchOfficialFigureData(),
fetchOfficialFigureMap(),
])
const st = officialFd.setTypes.find((s) => s.type === setType)
if (!st) {
return { ok: false, warnings, error: `setType "${setType}" not found in official figuredata` }
}
const set = st.sets.find((s) => s.id === setId)
if (!set) {
return {
ok: false,
warnings,
error: `set id ${setId} not found in setType "${setType}" in official figuredata`,
}
}
const palette = officialFd.palettes.find((p) => p.id === st.paletteId)
const partIndex = buildPartIndex(officialLibs)
const requiredLibIds = new Set<string>()
for (const part of set.parts) {
const lib = partIndex.get(partKey(part.type, part.id))
if (lib) {
requiredLibIds.add(lib.id)
} else {
warnings.push(`No library found for part ${part.type}:${part.id}`)
}
}
const localLibs = await listFigureLibraries()
const localLibIds = new Set(localLibs.map((l) => l.id))
await ensureFigureDirs()
for (const libId of requiredLibIds) {
const officialLib = officialLibs.find((l) => l.id === libId)
if (!officialLib) continue
if (!localLibIds.has(libId)) {
onProgress?.(`importing library ${libId}`)
const result = await importSingleFigure({ lib: libId, onProgress })
if (!result.ok) {
warnings.push(`Library import failed for ${libId}: ${result.error ?? 'unknown error'}`)
}
if (result.warnings.length) {
warnings.push(...result.warnings)
}
}
await mergeLibrary(officialLib)
}
const mappedSet = mapOfficialSet(set)
await mergeSet(setType, st.paletteId, mappedSet as never)
if (palette) {
await mergePalette(palette as never)
}
return { ok: true, setId, warnings }
}
export async function getClothingSetList(params: {
search: string
page: number
perPage: number
setType?: string
}): Promise<ClothingSetListResult> {
const { search, page, perPage, setType } = params
const officialFd: OfficialFigureData = await fetchOfficialFigureData()
const allItems: ClothingSetItem[] = []
for (const st of officialFd.setTypes) {
if (setType && st.type !== setType) continue
const presentIds = await listLocalSetIds(st.type)
for (const set of st.sets) {
allItems.push({
setType: st.type,
setId: set.id,
gender: set.gender,
club: set.club,
sellable: set.sellable,
present: presentIds.has(set.id),
})
}
}
const term = search.trim().toLowerCase()
const filtered = term
? allItems.filter(
(it) => String(it.setId).includes(term) || it.setType.toLowerCase().includes(term),
)
: allItems
const total = filtered.length
const start = (Math.max(page, 1) - 1) * perPage
const items = filtered.slice(start, start + perPage)
return { items, meta: { page: Math.max(page, 1), perPage, total } }
}
export async function getClothingSetStats(): Promise<ClothingSetStats> {
const officialFd: OfficialFigureData = await fetchOfficialFigureData()
let total = 0
let present = 0
for (const st of officialFd.setTypes) {
const presentIds = await listLocalSetIds(st.type)
total += st.sets.length
present += st.sets.filter((s) => presentIds.has(s.id)).length
}
return { total, present, missing: total - present }
}