mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
Init
This commit is contained in:
246
scripts/fetch-paint-data.ts
Normal file
246
scripts/fetch-paint-data.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Fetches miniature paint data from the Arcturus5404/miniature-paints GitHub repo
|
||||
* and converts Markdown tables into a single JSON file for the catalog API.
|
||||
*
|
||||
* Usage: npx tsx scripts/fetch-paint-data.ts
|
||||
*/
|
||||
|
||||
import { writeFileSync, mkdirSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
|
||||
const GITHUB_RAW =
|
||||
"https://raw.githubusercontent.com/Arcturus5404/miniature-paints/main/paints";
|
||||
|
||||
// Brands to fetch — file names from the repo (without .md)
|
||||
const BRANDS = [
|
||||
"AK",
|
||||
"Army_Painter",
|
||||
"Citadel_Colour",
|
||||
"CoatDArmes",
|
||||
"Foundry",
|
||||
"GreenStuffWorld",
|
||||
"Humbrol",
|
||||
"KimeraKolors",
|
||||
"Mig",
|
||||
"MissionModels",
|
||||
"Monument",
|
||||
"MrHobby",
|
||||
"P3",
|
||||
"Reaper",
|
||||
"Revell",
|
||||
"Scale75",
|
||||
"Tamiya",
|
||||
"TurboDork",
|
||||
"Vallejo",
|
||||
"Warcolours",
|
||||
];
|
||||
|
||||
// Display names for brands (file name → human-friendly)
|
||||
const BRAND_NAMES: Record<string, string> = {
|
||||
AK: "AK Interactive",
|
||||
Army_Painter: "The Army Painter",
|
||||
Citadel_Colour: "Citadel",
|
||||
CoatDArmes: "Coat d'Armes",
|
||||
Foundry: "Foundry",
|
||||
GreenStuffWorld: "Green Stuff World",
|
||||
Humbrol: "Humbrol",
|
||||
KimeraKolors: "Kimera Kolors",
|
||||
Mig: "AMMO by MIG",
|
||||
MissionModels: "Mission Models",
|
||||
Monument: "Monument Hobbies",
|
||||
MrHobby: "Mr. Hobby",
|
||||
P3: "P3 (Privateer Press)",
|
||||
Reaper: "Reaper",
|
||||
Revell: "Revell",
|
||||
Scale75: "Scale75",
|
||||
Tamiya: "Tamiya",
|
||||
TurboDork: "TurboDork",
|
||||
Vallejo: "Vallejo",
|
||||
Warcolours: "Warcolours",
|
||||
};
|
||||
|
||||
// Map known range/set names to paint finish types
|
||||
const FINISH_MAP: Record<string, string> = {
|
||||
// Citadel
|
||||
base: "Matte",
|
||||
layer: "Matte",
|
||||
air: "Matte",
|
||||
dry: "Matte",
|
||||
shade: "Wash",
|
||||
contrast: "Contrast",
|
||||
technical: "Other",
|
||||
"foundation (discontinued)": "Matte",
|
||||
"goblin green (discontinued)": "Matte",
|
||||
// Army Painter
|
||||
warpaints: "Matte",
|
||||
"warpaints fanatic": "Matte",
|
||||
"warpaints air": "Matte",
|
||||
speedpaint: "Contrast",
|
||||
"speedpaint set": "Contrast",
|
||||
"speedpaint set 2.0": "Contrast",
|
||||
"warpaints washes": "Wash",
|
||||
"warpaints effects": "Other",
|
||||
"warpaints metallics": "Metallic",
|
||||
"warpaints primer": "Primer",
|
||||
// Vallejo
|
||||
"model color": "Matte",
|
||||
"model air": "Matte",
|
||||
"game color": "Matte",
|
||||
"game air": "Matte",
|
||||
"game ink": "Ink",
|
||||
"game wash": "Wash",
|
||||
"metal color": "Metallic",
|
||||
"mecha color": "Matte",
|
||||
"mecha varnish": "Varnish",
|
||||
"surface primer": "Primer",
|
||||
"xpress color": "Contrast",
|
||||
panzer: "Matte",
|
||||
// Generic
|
||||
metallic: "Metallic",
|
||||
metallics: "Metallic",
|
||||
wash: "Wash",
|
||||
washes: "Wash",
|
||||
ink: "Ink",
|
||||
inks: "Ink",
|
||||
primer: "Primer",
|
||||
varnish: "Varnish",
|
||||
};
|
||||
|
||||
interface PaintEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
type: "paint";
|
||||
color: string;
|
||||
colorHex: string;
|
||||
line: string;
|
||||
finish: string;
|
||||
productCode: string | null;
|
||||
}
|
||||
|
||||
function extractHex(hexCell: string): string | null {
|
||||
// Format:  `#HEXHEX` — extract from backtick code
|
||||
const backtickMatch = hexCell.match(/`(#[0-9A-Fa-f]{6})`/);
|
||||
if (backtickMatch) return backtickMatch[1];
|
||||
|
||||
// Fallback: raw hex
|
||||
const rawMatch = hexCell.match(/#[0-9A-Fa-f]{6}/);
|
||||
if (rawMatch) return rawMatch[0];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function guessFinish(setName: string): string {
|
||||
const lower = setName.toLowerCase().trim();
|
||||
|
||||
// Direct match
|
||||
if (FINISH_MAP[lower]) return FINISH_MAP[lower];
|
||||
|
||||
// Partial match
|
||||
for (const [key, value] of Object.entries(FINISH_MAP)) {
|
||||
if (lower.includes(key)) return value;
|
||||
}
|
||||
|
||||
return "Matte"; // Default
|
||||
}
|
||||
|
||||
function parseMarkdownTable(markdown: string, brandFile: string): PaintEntry[] {
|
||||
const brandName = BRAND_NAMES[brandFile] || brandFile.replace(/_/g, " ");
|
||||
const lines = markdown.split("\n").filter((l) => l.trim().startsWith("|"));
|
||||
|
||||
if (lines.length < 2) return [];
|
||||
|
||||
// Parse header to determine column layout
|
||||
const header = lines[0]
|
||||
.split("|")
|
||||
.map((c) => c.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const hasCode = header.includes("code");
|
||||
|
||||
// Determine column indices
|
||||
const nameIdx = 0;
|
||||
const codeIdx = hasCode ? 1 : -1;
|
||||
const setIdx = hasCode ? 2 : 1;
|
||||
const rIdx = hasCode ? 3 : 2;
|
||||
const gIdx = hasCode ? 4 : 3;
|
||||
const bIdx = hasCode ? 5 : 4;
|
||||
const hexIdx = hasCode ? 6 : 5;
|
||||
|
||||
const entries: PaintEntry[] = [];
|
||||
|
||||
// Skip header and separator rows
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const cells = lines[i]
|
||||
.split("|")
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (cells.length < (hasCode ? 7 : 6)) continue;
|
||||
|
||||
const name = cells[nameIdx];
|
||||
const code = codeIdx >= 0 ? cells[codeIdx] : null;
|
||||
const set = cells[setIdx] || "";
|
||||
const hex = extractHex(cells[hexIdx]);
|
||||
|
||||
if (!name || !hex) continue;
|
||||
|
||||
const finish = guessFinish(set);
|
||||
const id = `paint-${brandFile}-${name}-${set}`.replace(/[^a-zA-Z0-9-]/g, "_").toLowerCase();
|
||||
|
||||
entries.push({
|
||||
id,
|
||||
name,
|
||||
brand: brandName,
|
||||
type: "paint",
|
||||
color: name, // For paints, the name IS the color
|
||||
colorHex: hex,
|
||||
line: set,
|
||||
finish,
|
||||
productCode: code && code !== "null" ? code : null,
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function fetchBrand(brandFile: string): Promise<PaintEntry[]> {
|
||||
const url = `${GITHUB_RAW}/${brandFile}.md`;
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
console.warn(` ⚠ Failed to fetch ${brandFile}: ${resp.status}`);
|
||||
return [];
|
||||
}
|
||||
const md = await resp.text();
|
||||
const entries = parseMarkdownTable(md, brandFile);
|
||||
console.log(` ✓ ${BRAND_NAMES[brandFile] || brandFile}: ${entries.length} paints`);
|
||||
return entries;
|
||||
} catch (err) {
|
||||
console.warn(` ⚠ Error fetching ${brandFile}:`, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Fetching paint data from GitHub...\n");
|
||||
|
||||
const allEntries: PaintEntry[] = [];
|
||||
|
||||
// Fetch in batches of 5 to avoid rate limits
|
||||
for (let i = 0; i < BRANDS.length; i += 5) {
|
||||
const batch = BRANDS.slice(i, i + 5);
|
||||
const results = await Promise.all(batch.map(fetchBrand));
|
||||
allEntries.push(...results.flat());
|
||||
}
|
||||
|
||||
console.log(`\nTotal: ${allEntries.length} paints from ${BRANDS.length} brands`);
|
||||
|
||||
// Write JSON
|
||||
const outDir = resolve(__dirname, "../src/data/catalog");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = resolve(outDir, "paints.json");
|
||||
writeFileSync(outPath, JSON.stringify(allEntries, null, 2));
|
||||
console.log(`\nWritten to: ${outPath}`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user