116 lines
4.2 KiB
JavaScript
116 lines
4.2 KiB
JavaScript
/**
|
||
* Assets IO — lecture des métadonnées PNG, copie des fichiers ingrédients
|
||
* et branding vers `apps/web/public`, sélection d'une cover recette.
|
||
*
|
||
* Aucun parsing : ces fonctions touchent le système de fichiers. Le parsing
|
||
* bentext / sprite vit dans `parse-bentext.mjs` et `sprites.mjs`.
|
||
*/
|
||
|
||
import { promises as fs } from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||
|
||
/** Fichiers d’ingrédients copiés tels quels vers `apps/web/public`. */
|
||
const INGREDIENT_ASSET_FILES = ["ingredients.png", "ingredient-sprites.csv", "ingredient-unknown.png"];
|
||
|
||
/** Fichiers de branding copiés tels quels vers `apps/web/public/branding`. */
|
||
export const BRANDING_FILES = ["onigiri-logo.png", "og.jpg"];
|
||
|
||
export const PATHS = {
|
||
repoRoot,
|
||
recipesDir: path.join(repoRoot, "ressources/bentext"),
|
||
outDir: path.join(repoRoot, "ressources/generated"),
|
||
webPublicDir: path.join(repoRoot, "apps/web/public/recipes"),
|
||
webCoverDir: path.join(repoRoot, "apps/web/public/recipe-covers"),
|
||
webBrandingDir: path.join(repoRoot, "apps/web/public/branding"),
|
||
webPublicRoot: path.join(repoRoot, "apps/web/public"),
|
||
publicAssetsDir: path.join(repoRoot, "ressources/public"),
|
||
placeholderCoversDir: path.join(repoRoot, "ressources/recipe-placeholder-covers"),
|
||
/** Grille des tuiles : coordonnées et alias listés dans ce fichier (plus d’API). */
|
||
ingredientSpritesBentextPath: path.join(repoRoot, "ressources/public/ingredient-sprites.bentext"),
|
||
ingredientsPngPath: path.join(repoRoot, "ressources/public/ingredients.png")
|
||
};
|
||
|
||
const COVER_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"];
|
||
|
||
const readPngDimensions = async (filePath) => {
|
||
try {
|
||
const buf = await fs.readFile(filePath);
|
||
if (buf.length < 24 || buf[0] !== 0x89) return null;
|
||
const width = buf.readUInt32BE(16);
|
||
const height = buf.readUInt32BE(20);
|
||
return { width, height };
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const copyIngredientAssetsToWeb = async () => {
|
||
await fs.mkdir(PATHS.webPublicRoot, { recursive: true });
|
||
for (const name of INGREDIENT_ASSET_FILES) {
|
||
const src = path.join(PATHS.publicAssetsDir, name);
|
||
const dest = path.join(PATHS.webPublicRoot, name);
|
||
try {
|
||
await fs.copyFile(src, dest);
|
||
} catch (error) {
|
||
if (error && error.code === "ENOENT") continue;
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
const copyBrandingAssets = async () => {
|
||
await fs.mkdir(PATHS.webBrandingDir, { recursive: true });
|
||
for (const name of BRANDING_FILES) {
|
||
const src = path.join(PATHS.publicAssetsDir, name);
|
||
const dest = path.join(PATHS.webBrandingDir, name);
|
||
try {
|
||
await fs.copyFile(src, dest);
|
||
} catch (error) {
|
||
if (error && error.code === "ENOENT") {
|
||
console.warn(`Branding asset missing, skip: ${name}`);
|
||
continue;
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Sélectionne une cover pour `slug` :
|
||
* 1. Image HD dans `ressources/recipe-placeholder-covers/<slug>.png`.
|
||
* 2. Sinon, image dans `ressources/public/<slug>.<ext>`.
|
||
* Copie le fichier dans `apps/web/public/recipe-covers/` et retourne l’URL publique.
|
||
*/
|
||
const pickCoverImage = async (slug) => {
|
||
await fs.mkdir(PATHS.webCoverDir, { recursive: true });
|
||
/* Priorité aux visuels HD du dépôt ; sinon ressources/public (override legacy). */
|
||
const placeholderPng = path.join(PATHS.placeholderCoversDir, `${slug}.png`);
|
||
try {
|
||
await fs.access(placeholderPng);
|
||
const publicName = `${slug}.png`;
|
||
const target = path.join(PATHS.webCoverDir, publicName);
|
||
await fs.copyFile(placeholderPng, target);
|
||
return `/recipe-covers/${publicName}`;
|
||
} catch {
|
||
// continue
|
||
}
|
||
for (const extension of COVER_EXTENSIONS) {
|
||
const candidate = path.join(PATHS.publicAssetsDir, `${slug}${extension}`);
|
||
try {
|
||
await fs.access(candidate);
|
||
const publicName = `${slug}${extension}`;
|
||
const target = path.join(PATHS.webCoverDir, publicName);
|
||
await fs.copyFile(candidate, target);
|
||
return `/recipe-covers/${publicName}`;
|
||
} catch {
|
||
// continue
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
export { readPngDimensions, copyIngredientAssetsToWeb, copyBrandingAssets, pickCoverImage };
|