#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); function arg(name, fallback = '') { const i = process.argv.indexOf(`--${name}`); if (i >= 0 && process.argv[i + 1]) return process.argv[i + 1]; return fallback; } (async () => { const ip = arg('ip') || process.argv[2]; if (!ip) throw new Error('Usage: download_ippure.js --ip [--outdir ]'); const outdir = arg('outdir', '/data/tmp/ippure-downloads'); fs.mkdirSync(outdir, { recursive: true }); const candidates = [ process.env.CHROMIUM_PATH, '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', ].filter(Boolean); const launchOptions = { headless: true, args: ['--no-sandbox'] }; for (const candidate of candidates) { if (fs.existsSync(candidate)) { launchOptions.executablePath = candidate; break; } } const browser = await chromium.launch(launchOptions); const context = await browser.newContext({ acceptDownloads: true, viewport: { width: 1440, height: 1200 }, deviceScaleFactor: 1, locale: 'zh-CN', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', }); const page = await context.newPage(); await page.route('**/*', route => { const url = route.request().url(); if ( url.includes('/cdn-cgi/rum') || url.includes('/cdn-cgi/speculation') || url.includes('cloudflareinsights.com') || url.includes('/api/ads') || url.includes('marker-icon.png') || url.includes('marker-shadow.png') ) return route.abort().catch(() => {}); return route.continue().catch(() => {}); }); const url = `https://ippure.com/?ip=${encodeURIComponent(ip)}`; await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 }); await page.waitForSelector('.iptable-container', { state: 'visible', timeout: 45000 }); await page.waitForSelector('button.screenshot-btn svg.lucide-camera', { state: 'visible', timeout: 45000 }); // Faster than waiting for full network idle: wait only for the official card to be populated. await page.waitForFunction((targetIp) => { const card = document.querySelector('.iptable-container'); const text = card?.innerText || ''; return text.includes(targetIp) && text.includes('IPPure系数') && !text.includes('Loading...'); }, ip, { timeout: 20000 }).catch(() => {}); // Linux headless Chrome doesn't have PingFang/SF Pro. After Playwright deps are installed, // fallback font metrics can make the IPPure score badge wrap (e.g. "40%\n中性"). // Keep the official camera export path, but stabilize fonts/nowrap inside the exported card. await page.addStyleTag({ content: ` .iptable-container, .iptable-container * { font-family: "Noto Sans CJK SC", "Noto Sans SC", "Microsoft YaHei", "PingFang SC", Arial, sans-serif !important; } .iptable-container .font-mono { font-family: "DejaVu Sans Mono", "Noto Sans Mono CJK SC", monospace !important; } .iptable-container .colormap-indicator-value { white-space: nowrap !important; min-width: max-content !important; } ` }).catch(() => {}); // Wait for web fonts/layout to settle so the export captures the stabilized layout. await page.evaluate(async () => { await document.fonts?.ready?.catch?.(() => {}); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); }).catch(() => {}); // Hide/mask the queried IP before exporting the official PNG. IPPure exposes this as // the eye button next to the camera button; click it first so downloaded images do not // leak the full IP address. const hideIpButton = page.locator('button.screenshot-btn').filter({ has: page.locator('svg.lucide-eye') }).first(); if (await hideIpButton.count()) { await hideIpButton.click({ timeout: 10000 }); await page.waitForFunction((targetIp) => { const card = document.querySelector('.iptable-container'); const text = card?.innerText || ''; return !text.includes(targetIp); }, ip, { timeout: 5000 }).catch(() => {}); await page.evaluate(async () => { await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); }).catch(() => {}); } const cameraButton = page.locator('button.screenshot-btn').filter({ has: page.locator('svg.lucide-camera') }).first(); const downloadPromise = page.waitForEvent('download', { timeout: 30000 }); await cameraButton.click({ timeout: 10000 }); const download = await downloadPromise; const suggested = await download.suggestedFilename(); const filename = suggested && suggested.toLowerCase().endsWith('.png') ? suggested : `IPPure-${ip}-${Date.now()}.png`; const out = path.join(outdir, filename); await download.saveAs(out); await browser.close(); console.log(out); })().catch(err => { console.error(err.stack || err.message || String(err)); process.exit(1); });