import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import sharp from 'sharp' import { decodeIco, isIco } from 'icojs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const root = path.resolve(__dirname, '..') const outDir = path.join(root, 'public', 'icons') const linksPath = path.join(root, 'src', 'data', 'links.json') const SIZE = 96 const WEBP_QUALITY = 72 const SOURCE = { nomadro: 'https://nav.hackrobot.cn/assets/images/nomadro2.png', nomadlist: 'https://nav.hackrobot.cn/assets/images/nomadlist.png', nomadcna: 'https://nav.hackrobot.cn/assets/images/nomadcna.jpg', chatgpt: 'https://nav.hackrobot.cn/assets/images/logos/chatgpt.png', grok: 'https://nav.hackrobot.cn/assets/favicon_1759046289851.png', '1psim': 'https://nav.hackrobot.cn/assets/images/1psim.png', yesim: 'https://nav.hackrobot.cn/assets/images/yesim.png', tello: 'https://nav.hackrobot.cn/assets/images/tello.png', '9esim': 'https://nav.hackrobot.cn/assets/images/9esim.png', wise: 'https://nav.hackrobot.cn/assets/images/logos/wise.png', okx: 'https://nav.hackrobot.cn/assets/images/logos/okx.png', safepal: 'https://nav.hackrobot.cn/assets/images/logos/safepal.png', paypal: 'https://nav.hackrobot.cn/assets/images/paypal.png', bmc: 'https://nav.hackrobot.cn/assets/images/buymecoffee.png', jms: 'https://nav.hackrobot.cn/assets/images/logos/proxy.png', ykk: 'https://nav.hackrobot.cn/assets/images/logos/ykk.jpg', vultr: 'https://nav.hackrobot.cn/assets/images/logos/vultr.png', bwh: 'https://nav.hackrobot.cn/assets/images/logos/bandwagonhost.jpg', racknerd: 'https://nav.hackrobot.cn/assets/images/logos/RackNerd.jpg', qcloud: 'https://nav.hackrobot.cn/assets/images/tengxunyun.png', aliyun: 'https://nav.hackrobot.cn/assets/images/aliyun.jpg', colocrossing: 'https://nav.hackrobot.cn/assets/images/logos/colocrossing.png', iproyal: 'https://nav.hackrobot.cn/assets/images/logos/iproyal.jpg', 'proxy-cheap': 'https://nav.hackrobot.cn/assets/images/logos/proxy-cheap.jpg', smartproxy: 'https://nav.hackrobot.cn/assets/images/logos/smartproxy.jpg', lisa: 'https://nav.hackrobot.cn/assets/images/lisa.png', voyracloud: 'https://nav.hackrobot.cn/assets/images/voyracloud.jpg', rpi: 'https://nav.hackrobot.cn/assets/images/logos/raspberrypi.png', 'mini-pc': 'https://nav.hackrobot.cn/assets/images/logos/lingke.jpg', gpu: 'https://nav.hackrobot.cn/assets/images/logos/nvidia.png', '3x-ui': 'https://nav.hackrobot.cn/assets/images/logos/3x.jpg', mihomo: 'https://nav.hackrobot.cn/assets/images/mihomo.jpg', frp: 'https://nav.hackrobot.cn/assets/images/logos/frp.png', wireguard: 'https://nav.hackrobot.cn/assets/images/wireguard.png', lineageos: 'https://nav.hackrobot.cn/assets/images/logos/lineageos.webp', 'ai-glasses': 'https://nav.hackrobot.cn/assets/images/logos/glass.jpg', 'action-cam': 'https://nav.hackrobot.cn/assets/images/logos/action.jpg', drone: 'https://nav.hackrobot.cn/assets/images/logos/neo2.jpg', tiktok: 'https://nav.hackrobot.cn/assets/images/logos/tiktok.png', 'cloud-phone': 'https://nav.hackrobot.cn/assets/images/logos/phone.png', vcam: 'https://nav.hackrobot.cn/assets/images/vcam.jpg', youtube: 'https://nav.hackrobot.cn/assets/images/logos/youtube.png', smmfollows: 'https://nav.hackrobot.cn/assets/images/logos/qqtube.png', 'yt-auto-dismiss': 'https://nav.hackrobot.cn/assets/images/greasyfork.png', namesilo: 'https://nav.hackrobot.cn/assets/images/logos/namesilo.jpg', aapanel: 'https://nav.hackrobot.cn/assets/images/logos/aapanel.png', cloudflare: 'https://nav.hackrobot.cn/assets/images/logos/cloudflar.ico', ga: 'https://nav.hackrobot.cn/assets/images/logos/analytics.ico', adsense: 'https://nav.hackrobot.cn/assets/images/logos/gads.ico', obs: 'https://nav.hackrobot.cn/assets/images/obs.jpg', ffmpeg: 'https://nav.hackrobot.cn/assets/images/ffmpeg.jpg', srs: 'https://nav.hackrobot.cn/assets/images/srs.png', nowpayments: 'https://nav.hackrobot.cn/assets/images/nowpayments.ico', creem: 'https://nav.hackrobot.cn/assets/images/creem.ico', stripe: 'https://nav.hackrobot.cn/assets/images/stripe.ico', zpay: 'https://nav.hackrobot.cn/assets/images/zpay.ico', xorpay: 'https://nav.hackrobot.cn/assets/images/xorpay.ico', yidu: 'https://nav.hackrobot.cn/assets/images/blog.jpg', dsx: 'https://nav.hackrobot.cn/assets/images/dsx.jpg', _brand: 'https://nav.hackrobot.cn/assets/images/nomadro.png', } async function fetchBuffer(url) { const res = await fetch(url) if (!res.ok) throw new Error(`${res.status} ${url}`) return Buffer.from(await res.arrayBuffer()) } async function normalizeInput(buf) { if (isIco(buf)) { const images = await decodeIco(buf) let best = images[0] let bestArea = 0 for (const img of images) { const meta = await sharp(Buffer.from(img.buffer), { failOn: 'none' }).metadata() const area = (meta.width || 0) * (meta.height || 0) if (area >= bestArea) { bestArea = area best = img } } return Buffer.from(best.buffer) } return buf } async function toWebp(buf) { const input = await normalizeInput(buf) return sharp(input, { animated: false, failOn: 'none' }) .rotate() .resize(SIZE, SIZE, { fit: 'cover', withoutEnlargement: false, background: { r: 255, g: 255, b: 255, alpha: 0 }, }) .webp({ quality: WEBP_QUALITY, effort: 6 }) .toBuffer() } async function main() { await mkdir(outDir, { recursive: true }) const data = JSON.parse(await readFile(linksPath, 'utf8')) const ids = new Set(['_brand']) for (const cat of data.categories) { for (const group of cat.groups) { for (const link of group.links) ids.add(link.id) } } let ok = 0 let fail = 0 let bytes = 0 for (const id of ids) { const url = SOURCE[id] if (!url) { console.error(`✗ ${id}: missing source URL`) fail += 1 continue } try { const raw = await fetchBuffer(url) const webp = await toWebp(raw) await writeFile(path.join(outDir, `${id}.webp`), webp) bytes += webp.length ok += 1 console.log(`✓ ${id}.webp ${(webp.length / 1024).toFixed(1)}KB`) } catch (err) { fail += 1 console.error(`✗ ${id}: ${err.message}`) } } for (const cat of data.categories) { for (const group of cat.groups) { for (const link of group.links) { link.icon = `/icons/${link.id}.webp` } } } await writeFile(linksPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8') console.log( `\nDone: ${ok} ok, ${fail} fail, total ${(bytes / 1024).toFixed(1)}KB → public/icons/`, ) } main().catch((err) => { console.error(err) process.exit(1) })