#!/usr/bin/env node /** * 下载 Epic 免费游戏图片到本地 * 运行方式: node scripts/download-epic-images.mjs */ import fs from 'fs' import path from 'path' import https from 'https' import http from 'http' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const EPIC_API_URL = 'https://uapis.cn/api/v1/game/epic-free' const OUTPUT_DIR = path.join(__dirname, '../public/epic-images') const MANIFEST_PATH = path.join(OUTPUT_DIR, 'manifest.json') // 发起 HTTPS GET 请求获取 JSON function fetchJson(url) { return new Promise((resolve, reject) => { https.get(url, (response) => { if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { fetchJson(response.headers.location).then(resolve).catch(reject) return } if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}`)) return } let data = '' response.on('data', chunk => data += chunk) response.on('end', () => { try { resolve(JSON.parse(data)) } catch (e) { reject(e) } }) }).on('error', reject) }) } // 确保输出目录存在 function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) console.log(`📁 Created directory: ${dir}`) } } // 下载文件 function downloadFile(url, destPath) { return new Promise((resolve, reject) => { const protocol = url.startsWith('https') ? https : http const request = protocol.get(url, (response) => { // 处理重定向 if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { downloadFile(response.headers.location, destPath).then(resolve).catch(reject) return } if (response.statusCode !== 200) { reject(new Error(`Failed to download ${url}: ${response.statusCode}`)) return } const fileStream = fs.createWriteStream(destPath) response.pipe(fileStream) fileStream.on('finish', () => { fileStream.close() resolve() }) fileStream.on('error', (err) => { fs.unlink(destPath, () => { }) // 删除失败的文件 reject(err) }) }) request.on('error', reject) request.setTimeout(30000, () => { request.destroy() reject(new Error(`Timeout downloading ${url}`)) }) }) } // 获取文件扩展名 function getExtension(url) { const match = url.match(/\.(\w+)(?:\?|$)/) return match ? `.${match[1]}` : '.jpg' } // 生成安全的文件名 function safeFileName(id, url) { const ext = getExtension(url) return `${id}${ext}` } // 主函数 async function main() { console.log('🎮 开始下载 Epic 免费游戏图片...\n') // 获取 Epic API 数据 console.log('📡 正在获取 Epic API 数据...') const result = await fetchJson(EPIC_API_URL) if (!result.data || !Array.isArray(result.data)) { console.error('❌ API 返回数据格式错误') process.exit(1) } // 过滤掉神秘游戏 const games = result.data.filter(game => !game.title.includes('神秘游戏')) console.log(`📋 找到 ${games.length} 个游戏\n`) // 确保输出目录存在 ensureDir(OUTPUT_DIR) // 下载每个游戏的封面图片 const manifest = {} for (const game of games) { if (!game.cover) { console.log(`⚠️ 跳过 ${game.title}: 无封面图片`) continue } const fileName = safeFileName(game.id, game.cover) const destPath = path.join(OUTPUT_DIR, fileName) // 检查文件是否已存在 if (fs.existsSync(destPath)) { console.log(`✅ 已存在: ${game.title} (${fileName})`) manifest[game.id] = `/epic-images/${fileName}` continue } try { console.log(`⬇️ 下载中: ${game.title}...`) await downloadFile(game.cover, destPath) console.log(`✅ 完成: ${game.title} (${fileName})`) manifest[game.id] = `/epic-images/${fileName}` } catch (error) { console.error(`❌ 失败: ${game.title} - ${error.message}`) // 失败时保留原始 URL manifest[game.id] = game.cover } } // 保存 manifest 文件 fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2)) console.log(`\n📄 Manifest 已保存到: ${MANIFEST_PATH}`) console.log('🎉 完成!') } main().catch(console.error)