← back to Designer Wallcoverings
onboarding/sangetsu-lilycolor/lily-image-manifest.cjs
88 lines
#!/usr/bin/env node
/**
* Lilycolor image MANIFEST builder (OFFLINE, disk-light).
* Reads each catalog image archive's ZIP central directory via an HTTP Range
* request on the LAST few MB only — so we enumerate every per-SKU image filename
* inside a 300MB+ archive WITHOUT downloading the archive. Maps filenames to SKUs
* and writes a manifest: { sku -> [{zipUrl, name, kind}] }.
*
* kind: C = swatch/cut, R = room/real, P = pattern-tile, SP = special.
* The actual image BYTES are deliberately NOT fetched here (gigabytes) — gated.
*
* HARD: read-only network, writes ONLY a local manifest JSON. No publish.
* node lily-image-manifest.cjs
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const HOST = 'https://www.lilycolor.co.jp/interior/catalog/';
// the wallcovering-line archives that match the 5 parsed price books (C+R+P where present)
const ZIPS = [
'will2026/Lilycolor_will_2026-29_C.zip', 'will2026/Lilycolor_will_2026-29_R.zip', 'will2026/Lilycolor_will_2026-29_P.zip', 'will2026/Lilycolor_will_2026-29_SP.zip',
'light2025/Lilycolor_LIGHT_2025-28_C.zip', 'light2025/Lilycolor_LIGHT_2025-28_R.zip', 'light2025/Lilycolor_LIGHT_2025-28_P.zip',
'v_wall2024/Lilycolor_V-wall_2024-27_C.zip', 'v_wall2024/Lilycolor_V-wall_2024-27_R.zip', 'v_wall2024/Lilycolor_V-wall_2024-27_P.zip',
'materials2024/Lilycolor_materials_2024_260213_C.zip', 'materials2024/Lilycolor_materials_2024_260213_P.zip',
'lis2023/Lilycolor_Import_Selection_231018_C.zip', 'lis2023/Lilycolor_Import_Selection_231018_R.zip', 'lis2023/Lilycolor_Import_Selection_231018_P.zip',
];
const OUT = path.join(__dirname, 'staging', 'lilycolor-image-manifest.json');
const TAIL = 4 * 1024 * 1024; // last 4MB covers the central directory of even the largest book
function getRange(url, bytesFromEnd) {
return new Promise((resolve, reject) => {
https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 Chrome/120', Range: `bytes=-${bytesFromEnd}` } }, (res) => {
if (res.statusCode !== 206 && res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks)));
}).on('error', reject);
});
}
// scan a buffer for ZIP central-directory headers (PK\x01\x02) and pull filenames
function namesFromCentralDir(buf) {
const names = [];
const SIG = 0x02014b50;
for (let i = 0; i + 46 <= buf.length; i++) {
if (buf.readUInt32LE(i) !== SIG) continue;
const nameLen = buf.readUInt16LE(i + 28);
const extraLen = buf.readUInt16LE(i + 30);
const commentLen = buf.readUInt16LE(i + 32);
if (nameLen === 0 || nameLen > 300 || i + 46 + nameLen > buf.length) continue;
const name = buf.slice(i + 46, i + 46 + nameLen).toString('utf8');
if (/\.jpe?g$/i.test(name)) names.push(name);
i += 46 + nameLen + extraLen + commentLen - 1;
}
return names;
}
const skuFromName = (n) => {
const base = n.split('/').pop();
const m = base.match(/^([A-Z]{2,4})-?(\d{3,6})/i);
return m ? `${m[1].toUpperCase()}${m[2]}` : null;
};
const kindFromName = (n) => (n.match(/_(SP|C|R|P)(?:_\d+)?\.jpe?g$/i) || [])[1]?.toUpperCase() || '?';
(async () => {
const manifest = {}; // sku -> [{zipUrl,name,kind}]
const perZip = {};
for (const z of ZIPS) {
const url = HOST + encodeURI(z);
try {
const buf = await getRange(url, TAIL);
const names = namesFromCentralDir(buf);
perZip[z] = names.length;
for (const name of names) {
const sku = skuFromName(name);
if (!sku) continue;
(manifest[sku] = manifest[sku] || []).push({ zipUrl: url, name: name.split('/').pop(), kind: kindFromName(name) });
}
process.stderr.write(`${z}: ${names.length} images\n`);
} catch (e) { process.stderr.write(`SKIP ${z}: ${e.message}\n`); }
}
fs.writeFileSync(OUT, JSON.stringify(manifest));
const skus = Object.keys(manifest);
const totalImgs = skus.reduce((a, s) => a + manifest[s].length, 0);
console.log(JSON.stringify({ skusWithImages: skus.length, totalImages: totalImgs, perZip, out: OUT }, null, 2));
})().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });