navmini/scripts/fetch-icons.mjs
eric 518410edd0 feat: 游民导航 H5、微信小程序与小红书小工具
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 12:15:00 -05:00

167 lines
6.7 KiB
JavaScript

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)
})