Replace entire repo content with correct code from /root/shahikitchen-website/
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Sync dish photos from ftp-images/ → public/images/dishes/
|
||||
* - Matches folders by menu item id (e.g. ftp-images/bilder-bp/chicken-biryani/bild/)
|
||||
* - Center-crops to 4:3, resizes to 1200×900, exports unified JPEG
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { menuCategories } from '../infrastructure/menu/static-menu-data';
|
||||
import { videoBaseName } from '../application/media/asset-resolver';
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const FTP_ROOT = path.join(ROOT, 'ftp-images/bilder-bp');
|
||||
const OUT_DIR = path.join(ROOT, 'public/images/dishes');
|
||||
const TARGET_W = 1200;
|
||||
const TARGET_H = 900;
|
||||
const JPEG_QUALITY = 88;
|
||||
|
||||
/** Menu ids that share another dish's ftp photo (main bilder-bp folder) */
|
||||
const FTP_ALIASES: Record<string, string> = {
|
||||
'tikka-boti': 'chicken-tikka',
|
||||
};
|
||||
|
||||
/** Menu id → folder name under ftp-images/bilder-bp/sweets/ */
|
||||
const SWEETS_FOLDERS: Record<string, string> = {
|
||||
'namakpare': 'namak-paray',
|
||||
'shakar-paray': 'Shakar paray',
|
||||
'chocolate-barfi': 'chochlate-barfi',
|
||||
'pistachio-barfi': 'pistacho-barfi',
|
||||
};
|
||||
|
||||
function pickLargestImage(dir: string): string | null {
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
|
||||
const files = fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return [];
|
||||
if (!/\.(jpe?g|png|webp)$/i.test(entry.name)) return [];
|
||||
return [{ full, size: fs.statSync(full).size }];
|
||||
})
|
||||
.sort((a, b) => b.size - a.size);
|
||||
|
||||
return files[0]?.full ?? null;
|
||||
}
|
||||
|
||||
function findSourceImage(dishId: string): string | null {
|
||||
const mainFolder = FTP_ALIASES[dishId] ?? dishId;
|
||||
const sweetsFolder = SWEETS_FOLDERS[dishId] ?? dishId;
|
||||
|
||||
const candidates = [
|
||||
path.join(FTP_ROOT, mainFolder, 'bild'),
|
||||
path.join(FTP_ROOT, 'sweets', sweetsFolder, 'bild'),
|
||||
path.join(FTP_ROOT, 'sweets', sweetsFolder),
|
||||
];
|
||||
|
||||
for (const dir of candidates) {
|
||||
const found = pickLargestImage(dir);
|
||||
if (found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function run(cmd: string) {
|
||||
execSync(cmd, { stdio: 'pipe' });
|
||||
}
|
||||
|
||||
function processToJpeg(src: string, dest: string) {
|
||||
const tmp = path.join(OUT_DIR, `.tmp-${path.basename(dest)}`);
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
// Normalize to JPEG working copy
|
||||
run(`sips -s format jpeg "${src}" --out "${tmp}"`);
|
||||
|
||||
const width = Number(
|
||||
execSync(`sips -g pixelWidth "${tmp}"`).toString().match(/pixelWidth: (\d+)/)?.[1]
|
||||
);
|
||||
const height = Number(
|
||||
execSync(`sips -g pixelHeight "${tmp}"`).toString().match(/pixelHeight: (\d+)/)?.[1]
|
||||
);
|
||||
|
||||
if (!width || !height) throw new Error(`Could not read dimensions for ${src}`);
|
||||
|
||||
const targetAspect = TARGET_W / TARGET_H;
|
||||
const sourceAspect = width / height;
|
||||
|
||||
let cropW = width;
|
||||
let cropH = height;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
if (sourceAspect > targetAspect) {
|
||||
cropW = Math.round(height * targetAspect);
|
||||
offsetX = Math.round((width - cropW) / 2);
|
||||
} else if (sourceAspect < targetAspect) {
|
||||
cropH = Math.round(width / targetAspect);
|
||||
offsetY = Math.round((height - cropH) / 2);
|
||||
}
|
||||
|
||||
run(
|
||||
`sips --cropToHeightWidth ${cropH} ${cropW} --cropOffset ${offsetY} ${offsetX} "${tmp}"`
|
||||
);
|
||||
run(
|
||||
`sips -z ${TARGET_H} ${TARGET_W} -s format jpeg -s formatOptions ${JPEG_QUALITY} "${tmp}" --out "${dest}"`
|
||||
);
|
||||
|
||||
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const written = new Set<string>();
|
||||
let synced = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const category of menuCategories) {
|
||||
for (const item of category.items) {
|
||||
const src = findSourceImage(item.id);
|
||||
if (!src) {
|
||||
skipped++;
|
||||
console.log(` skip (no ftp image): ${item.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const outputs = new Set<string>();
|
||||
if (item.image) outputs.add(path.join(OUT_DIR, item.image));
|
||||
|
||||
if (item.video) {
|
||||
const base = videoBaseName(item.video);
|
||||
outputs.add(path.join(OUT_DIR, `${base}-poster.jpg`));
|
||||
outputs.add(path.join(OUT_DIR, `${base}-optimized-poster.jpg`));
|
||||
}
|
||||
|
||||
for (const out of outputs) {
|
||||
if (written.has(out)) continue;
|
||||
processToJpeg(src, out);
|
||||
written.add(out);
|
||||
console.log(` ✓ ${path.basename(out)} ← ${item.id}`);
|
||||
}
|
||||
|
||||
synced++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nDone: ${synced} dishes synced, ${skipped} without ftp source, ${written.size} files written.`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user