diff --git a/backend/routes/artwork.js b/backend/routes/artwork.js index 281d581..ff8914a 100644 --- a/backend/routes/artwork.js +++ b/backend/routes/artwork.js @@ -307,4 +307,41 @@ router.get('/:id/related', async (req, res) => { } }); -module.exports = router; \ No newline at end of file +/** + * 获取Ugoira动画的ZIP文件URL + * GET /api/artwork/:id/ugoira + */ +router.get('/:id/ugoira', async (req, res) => { + try { + const { id } = req.params; + + if (!id || isNaN(parseInt(id))) { + return res.status(400).json({ + success: false, + error: 'Invalid artwork ID' + }); + } + + const artworkService = new ArtworkService(req.backend.getAuth()); + const result = await artworkService.getUgoiraZipUrl(parseInt(id)); + + if (result.success) { + res.json({ + success: true, + data: result.data + }); + } else { + res.status(404).json({ + success: false, + error: result.error + }); + } + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/proxy.js b/backend/routes/proxy.js index c5edf69..3dcfcfa 100644 --- a/backend/routes/proxy.js +++ b/backend/routes/proxy.js @@ -3,6 +3,7 @@ const router = express.Router(); const ImageCacheService = require('../services/image-cache'); const ApiCacheService = require('../services/api-cache'); const { defaultLogger } = require('../utils/logger'); +const axios = require('axios'); // 创建logger实例 const logger = defaultLogger.child('ProxyRouter'); @@ -53,6 +54,46 @@ router.get('/image', async (req, res) => { } }); +/** + * 通用文件代理(支持ZIP等二进制资源) + * GET /api/proxy/file?url= + */ +router.get('/file', async (req, res) => { + try { + const { url } = req.query; + if (!url) { + return res.status(400).json({ success: false, error: 'File URL is required' }); + } + + const decodedUrl = decodeURIComponent(url); + + // 发起请求到源站(例如 i.pximg.net),设置必要头以通过防盗链 + const response = await axios.get(decodedUrl, { + responseType: 'arraybuffer', + headers: { + Referer: 'https://www.pixiv.net/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Accept: '*/*' + }, + timeout: 60000 + }); + + const contentType = getContentType(decodedUrl); + res.set({ + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=600', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Allow-Headers': 'Content-Type' + }); + + res.send(response.data); + } catch (error) { + logger.error('File proxy error:', { message: error.message, status: error.response?.status }); + res.status(500).json({ success: false, error: 'Failed to proxy file' }); + } +}); + /** * 缓存管理 - 获取缓存统计信息 * GET /api/proxy/cache/stats @@ -301,10 +342,13 @@ function getContentType(url) { 'gif': 'image/gif', 'webp': 'image/webp', 'bmp': 'image/bmp', - 'svg': 'image/svg+xml' + 'svg': 'image/svg+xml', + 'zip': 'application/zip', + 'mp4': 'video/mp4', + 'webm': 'video/webm' }; return contentTypeMap[ext] || 'image/jpeg'; } -module.exports = router; \ No newline at end of file +module.exports = router; \ No newline at end of file diff --git a/backend/services/artwork.js b/backend/services/artwork.js index cfed071..54c48a6 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -487,6 +487,46 @@ class ArtworkService { throw error; } } + + /** + * 获取Ugoira动画的ZIP文件URL + */ + async getUgoiraZipUrl(artworkId) { + try { + // 首先获取作品详情以确认是否为ugoira类型 + const detailResponse = await this.makeRequest('GET', '/v1/illust/detail', { illust_id: artworkId }); + const artwork = detailResponse.illust; + + // 检查作品类型是否为ugoira + if (artwork.type !== 'ugoira') { + return { + success: false, + error: 'This artwork is not an ugoira animation' + }; + } + + // 获取ugoira元数据 + const metadataResponse = await this.makeRequest('GET', '/v1/ugoira/metadata', { illust_id: artworkId }); + + // 返回ZIP文件URL和其他元数据 + return { + success: true, + data: { + artwork_id: artworkId, + zip_urls: metadataResponse.ugoira_metadata.zip_urls, + frames: metadataResponse.ugoira_metadata.frames, + } + }; + } catch (error) { + logger.error('Get ugoira zip URL error:', error.message); + logger.error('Get ugoira zip URL error details:', error.response?.data); + + return { + success: false, + error: error.message || 'Failed to get ugoira zip URL' + }; + } + } } module.exports = ArtworkService; diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index dc6b09b..a5b4843 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs-extra'); +const { generatePreviewGifFromUgoira } = require('./ugoira-gif'); const { defaultLogger } = require('../utils/logger'); const abortControllerManager = require('../utils/abort-controller-manager'); @@ -172,6 +173,31 @@ class DownloadExecutor { const infoPath = path.join(artworkDir, 'artwork_info.json'); await fs.writeJson(infoPath, artwork, { spaces: 2 }); + // 若为ugoira,基于已下载的ZIP生成预览GIF(不影响注册表判定) + try { + if (artwork && artwork.type === 'ugoira' && Array.isArray(artwork.ugoira_frames) && artwork.ugoira_frames.length) { + const files = await fs.readdir(artworkDir); + const zipFile = files.find((f) => f.toLowerCase().endsWith('.zip')); + if (zipFile) { + const zipPath = path.join(artworkDir, zipFile); + const previewGifPath = path.join(artworkDir, 'preview.gif'); + await generatePreviewGifFromUgoira(zipPath, artwork.ugoira_frames, previewGifPath); + + // 完整性校验预览GIF(不计入任务下载计数) + const integrity = await this.fileManager.checkFileIntegrity(previewGifPath, null, 'image/gif'); + if (!integrity.valid) { + // 如果GIF生成不完整,尝试删除以避免残留 + await this.fileManager.safeDeleteFile(previewGifPath).catch(() => {}); + logger.warn('预览GIF完整性校验失败,已清理', { previewGifPath, reason: integrity.reason }); + } else { + logger.info('已生成ugoira预览GIF', { previewGifPath }); + } + } + } + } catch (gifError) { + logger.warn('生成ugoira预览GIF失败(继续任务流程)', { error: gifError.message }); + } + // 更新任务状态 - 确保所有文件都处理完成后再更新 task.status = task.failed_files === 0 ? 'completed' : 'partial'; task.end_time = new Date(); @@ -501,6 +527,8 @@ class DownloadExecutor { return 'image/webp'; case 'bmp': return 'image/bmp'; + case 'zip': + return 'application/zip'; default: return 'image/jpeg'; // 默认为JPEG } diff --git a/backend/services/download.js b/backend/services/download.js index 110d508..5c3bd59 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -7,6 +7,7 @@ const ProgressManager = require('./progress-manager'); const HistoryManager = require('./history-manager'); const DownloadExecutor = require('./download-executor'); const DownloadRegistry = require('./download-registry'); +const { generatePreviewGifFromUgoira } = require('./ugoira-gif'); const CacheConfigManager = require('../config/cache-config'); const fs = require('fs-extra'); // Added for fs-extra const { defaultLogger } = require('../utils/logger'); @@ -655,6 +656,8 @@ class DownloadService { return 'image/webp'; case 'bmp': return 'image/bmp'; + case 'zip': + return 'application/zip'; default: return 'image/jpeg'; // 默认为JPEG } @@ -959,14 +962,36 @@ class DownloadService { await this.fileManager.ensureDirectory(artworkDir); - // 获取图片URL - const imagesResult = await this.artworkService.getArtworkImages(artworkId, size); - if (!imagesResult.success) { - throw new Error(`获取图片URL失败: ${imagesResult.error}`); + // 根据作品类型获取下载资源 + let images = []; + if (artwork.type === 'ugoira') { + // Ugoira动图:仅下载ZIP,预览将由本地生成GIF + const zipResult = await this.artworkService.getUgoiraZipUrl(artworkId); + if (!zipResult.success) { + throw new Error(`获取ugoira ZIP失败: ${zipResult.error}`); + } + const zipUrls = zipResult.data?.zip_urls || {}; + const zipUrl = zipUrls.medium || zipUrls.large || zipUrls.small || Object.values(zipUrls)[0]; + if (!zipUrl) { + throw new Error('未找到ugoira ZIP地址'); + } + // 仅添加ZIP到下载列表 + images = [ + { original: zipUrl, large: zipUrl, medium: zipUrl, square_medium: zipUrl }, + ]; + // 保存ugoira帧元数据,供执行器生成预览GIF + if (Array.isArray(zipResult.data?.frames)) { + artwork.ugoira_frames = zipResult.data.frames; + } + } else { + // 普通插画/漫画:按原有逻辑获取图片URL + const imagesResult = await this.artworkService.getArtworkImages(artworkId, size); + if (!imagesResult.success) { + throw new Error(`获取图片URL失败: ${imagesResult.error}`); + } + images = imagesResult.data.images; } - const images = imagesResult.data.images; - // 创建任务记录 const task = this.taskManager.createTask('artwork', { artwork_id: artworkId, @@ -1094,14 +1119,33 @@ class DownloadService { await this.fileManager.ensureDirectory(artworkDir); - // 获取图片URL - const imagesResult = await this.artworkService.getArtworkImages(artworkId, size); - if (!imagesResult.success) { - throw new Error(`获取图片URL失败: ${imagesResult.error}`); + // 根据作品类型获取下载资源(批量下载场景) + let images = []; + if (artwork.type === 'ugoira') { + const zipResult = await this.artworkService.getUgoiraZipUrl(artworkId); + if (!zipResult.success) { + throw new Error(`获取ugoira ZIP失败: ${zipResult.error}`); + } + const zipUrls = zipResult.data?.zip_urls || {}; + const zipUrl = zipUrls.medium || zipUrls.large || zipUrls.small || Object.values(zipUrls)[0]; + if (!zipUrl) { + throw new Error('未找到ugoira ZIP地址'); + } + // 仅添加ZIP到下载列表;预览将由本地生成GIF + images = [ + { original: zipUrl, large: zipUrl, medium: zipUrl, square_medium: zipUrl }, + ]; + if (Array.isArray(zipResult.data?.frames)) { + artwork.ugoira_frames = zipResult.data.frames; + } + } else { + const imagesResult = await this.artworkService.getArtworkImages(artworkId, size); + if (!imagesResult.success) { + throw new Error(`获取图片URL失败: ${imagesResult.error}`); + } + images = imagesResult.data.images; } - const images = imagesResult.data.images; - // 直接下载,不创建新任务 const results = []; for (let index = 0; index < images.length; index++) { @@ -1165,6 +1209,30 @@ class DownloadService { const infoPath = path.join(artworkDir, 'artwork_info.json'); await fs.writeJson(infoPath, artwork, { spaces: 2 }); + // 若为ugoira,生成预览GIF(不影响注册表判定) + try { + if (artwork && artwork.type === 'ugoira' && Array.isArray(artwork.ugoira_frames) && artwork.ugoira_frames.length) { + const files = await fs.readdir(artworkDir); + const zipFile = files.find((f) => f.toLowerCase().endsWith('.zip')); + if (zipFile) { + const zipPath = path.join(artworkDir, zipFile); + const previewGifPath = path.join(artworkDir, 'preview.gif'); + await generatePreviewGifFromUgoira(zipPath, artwork.ugoira_frames, previewGifPath); + + // 校验GIF完整性(不计入images数量) + const headerCheck = await this.fileManager.checkFileHeader(previewGifPath, 'image/gif'); + if (!headerCheck.valid) { + await this.fileManager.safeDeleteFile(previewGifPath).catch(() => {}); + logger.warn('批量生成的预览GIF完整性校验失败,已清理', { previewGifPath, reason: headerCheck.reason }); + } else { + logger.info('批量已生成ugoira预览GIF', { previewGifPath }); + } + } + } + } catch (gifError) { + logger.warn('批量生成ugoira预览GIF失败(继续流程)', { error: gifError.message }); + } + // 检查下载结果 const failedCount = results.filter(r => !r.success).length; const successCount = results.filter(r => r.success && !r.skipped).length; @@ -1190,10 +1258,11 @@ class DownloadService { break; } - // 检查MIME类型 - 使用checkFileHeader方法来检测文件类型 - const headerCheck = await this.fileManager.checkFileHeader(filePath); - if (!headerCheck.valid || !headerCheck.detectedType || !headerCheck.detectedType.startsWith('image/')) { - logger.warn(`文件MIME类型检查失败: ${filePath}, 检测结果: ${JSON.stringify(headerCheck)}`); + // 检查MIME类型 - 依据扩展名设置期望类型,支持ZIP + const expectedMimeType = this.getMimeTypeFromExtension(fileName); + const headerCheck = await this.fileManager.checkFileHeader(filePath, expectedMimeType); + if (!headerCheck.valid) { + logger.warn(`文件MIME类型或头部检查失败: ${filePath}, 检测结果: ${JSON.stringify(headerCheck)}`); integrityCheckPassed = false; break; } diff --git a/backend/services/file-manager.js b/backend/services/file-manager.js index 2255035..67af4f4 100644 --- a/backend/services/file-manager.js +++ b/backend/services/file-manager.js @@ -168,6 +168,8 @@ class FileManager { if (expectedMimeType) { if (expectedMimeType.startsWith('image/')) { return 1024; // 图片文件至少1KB + } else if (expectedMimeType.includes('zip')) { + return 1024; // ZIP文件至少1KB } } @@ -243,6 +245,16 @@ class FileManager { return { valid: true, detectedType: 'image/webp' }; } + // 非图片类型的文件头检查(例如ZIP) + if (expectedMimeType && expectedMimeType.includes('zip')) { + // ZIP文件头常见为 504B0304 或 504B0506 等,以 504B 开头 + const headerHex = buffer.toString('hex', 0, Math.min(bytesRead, 4)); + if (headerHex.startsWith('504b')) { + return { valid: true, detectedType: 'application/zip' }; + } + return { valid: false, reason: '文件格式不匹配:期望ZIP但未检测到ZIP头部' }; + } + // 如果没有明确的期望类型,且检测到了有效的图片头部,则认为有效 if (!expectedMimeType) { return { valid: true, detectedType: 'unknown' }; @@ -250,7 +262,7 @@ class FileManager { // 如果有期望类型但未匹配到已知格式,可能是损坏的文件 return { valid: false, reason: '无法识别的文件格式或文件头部损坏' }; - + } finally { await fs.close(fd); } diff --git a/backend/services/ugoira-gif.js b/backend/services/ugoira-gif.js new file mode 100644 index 0000000..8f3a389 --- /dev/null +++ b/backend/services/ugoira-gif.js @@ -0,0 +1,80 @@ +const fs = require('fs'); +const path = require('path'); +const fsExtra = require('fs-extra'); +// These dependencies are expected in project dependencies +const AdmZip = require('adm-zip'); +const jpeg = require('jpeg-js'); +const GIFEncoder = require('gifencoder'); + +/** + * Generate an animated GIF preview from a Pixiv ugoira ZIP and frame metadata. + * - Extracts frames from ZIP into a temp directory + * - Encodes frames into GIF honoring per-frame delays + * - Writes GIF to outPath + * + * @param {string} zipPath - Path to the downloaded ugoira ZIP file + * @param {Array<{file:string, delay:number}>} frames - Frame metadata from Pixiv API + * @param {string} outPath - Destination path for the generated GIF (e.g., preview.gif) + */ +async function generatePreviewGifFromUgoira(zipPath, frames, outPath) { + if (!zipPath || !Array.isArray(frames) || frames.length === 0 || !outPath) { + throw new Error('Invalid parameters for generating ugoira preview GIF'); + } + + const tmpDir = path.join(path.dirname(outPath), '.ugoira_tmp'); + + await fsExtra.ensureDir(tmpDir); + + try { + // Extract all frames + const zip = new AdmZip(zipPath); + zip.extractAllTo(tmpDir, true); + + // Determine size from first frame + const firstFramePath = path.join(tmpDir, frames[0].file); + const firstBuf = fs.readFileSync(firstFramePath); + const firstDecoded = jpeg.decode(firstBuf, { useTArray: true }); + + const width = firstDecoded.width; + const height = firstDecoded.height; + + // Initialize encoder and output stream + const encoder = new GIFEncoder(width, height); + const ws = fs.createWriteStream(outPath); + encoder.createReadStream().pipe(ws); + + encoder.start(); + encoder.setRepeat(0); // loop forever + encoder.setQuality(10); + + // Add frames honoring per-frame delay + for (const f of frames) { + const framePath = path.join(tmpDir, f.file); + const buf = fs.readFileSync(framePath); + const decoded = jpeg.decode(buf, { useTArray: true }); + + // Ensure dimensions match; if not, skip or resize (skip for simplicity) + if (decoded.width !== width || decoded.height !== height) { + // Skip mismatched frames to keep encoder stable + continue; + } + + encoder.setDelay(typeof f.delay === 'number' ? f.delay : 0); + encoder.addFrame(decoded.data); + } + + encoder.finish(); + + await new Promise((resolve, reject) => { + ws.on('finish', resolve); + ws.on('error', reject); + }); + } finally { + // Cleanup extracted frames + try { await fsExtra.remove(tmpDir); } catch (_) {} + } +} + +module.exports = { + generatePreviewGifFromUgoira, +}; \ No newline at end of file diff --git a/package.json b/package.json index b9fc61e..31b05ee 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,16 @@ "bp": "npm run build && node scripts/create-portable.js" }, "dependencies": { + "adm-zip": "^0.5.16", "appdata-path": "^1.0.0", "axios": "0.27.0", "cors": "^2.8.5", "express": "^5.1.0", "fs-extra": "^11.3.2", + "gifencoder": "^2.0.1", + "jpeg-js": "^0.4.4", "js-base64": "^3.7.8", + "jszip": "^3.10.1", "moment": "^2.30.1", "mysql2": "^3.15.2", "proxy-agent": "^6.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbdb038..90e052b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 appdata-path: specifier: ^1.0.0 version: 1.0.0 @@ -23,9 +26,18 @@ importers: fs-extra: specifier: ^11.3.2 version: 11.3.2 + gifencoder: + specifier: ^2.0.1 + version: 2.0.1 + jpeg-js: + specifier: ^0.4.4 + version: 0.4.4 js-base64: specifier: ^3.7.8 version: 3.7.8 + jszip: + specifier: ^3.10.1 + version: 3.10.1 moment: specifier: ^2.30.1 version: 2.30.1 @@ -91,6 +103,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -106,10 +122,17 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -129,6 +152,14 @@ packages: appdata-path@1.0.0: resolution: {integrity: sha512-ZbH3ezXfnT/YE3NdqduIt4lBV+H0ybvA2Qx3K76gIjQvh8gROpDFdDLpx6B1QJtW7zxisCbpTlCLhKqoR8cDBw==} + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -152,6 +183,9 @@ packages: resolution: {integrity: sha512-XV/WrPxXfzgZ8j4lcB5i6LyaXmi90yetmV/Fem0kmglGx+mpY06CiweL3YxU6wOTNLmqLUePW4G8h45nGZ/+pA==} deprecated: Formdata complete broken, incorrect build size + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -166,6 +200,9 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -185,6 +222,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + canvas@2.11.2: + resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} + engines: {node: '>=6'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -192,6 +233,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -202,10 +247,20 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -242,6 +297,10 @@ packages: supports-color: optional: true + decompress-response@4.2.1: + resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} + engines: {node: '>=8'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -258,6 +317,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -394,9 +456,21 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -416,6 +490,9 @@ packages: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} + gifencoder@2.0.1: + resolution: {integrity: sha512-x19DcyWY10SkshBpokqFOo/HBht9GB75evRYvaLMbez9p+yB/o+kt0fK9AwW59nFiAMs2UUQsjv1lX/hvu9Ong==} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -423,6 +500,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -446,6 +527,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + has@1.0.4: resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} engines: {node: '>= 0.4.0'} @@ -485,6 +569,13 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -535,6 +626,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -546,6 +640,12 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -557,6 +657,10 @@ packages: resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -593,16 +697,40 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} + mimic-response@2.1.0: + resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} + engines: {node: '>=8'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} @@ -620,6 +748,9 @@ packages: resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} engines: {node: '>=12.0.0'} + nan@2.23.0: + resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} @@ -644,6 +775,15 @@ packages: encoding: optional: true + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -671,10 +811,17 @@ packages: resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} engines: {node: '>= 14'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -779,6 +926,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -795,6 +947,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -811,6 +967,12 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -830,9 +992,15 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + simple-get@3.1.1: + resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} @@ -904,6 +1072,10 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -954,6 +1126,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -965,6 +1140,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -1009,6 +1187,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.1.1 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.2 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1023,11 +1216,15 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} + abbrev@1.1.1: {} + accepts@2.0.0: dependencies: mime-types: 3.0.1 negotiator: 1.0.0 + adm-zip@0.5.16: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -1044,6 +1241,13 @@ snapshots: appdata-path@1.0.0: {} + aproba@2.1.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + array-union@2.1.0: {} ast-types@0.13.4: @@ -1063,6 +1267,8 @@ snapshots: transitivePeerDependencies: - debug + balanced-match@1.0.2: {} + base64-js@1.5.1: {} basic-ftp@5.0.5: {} @@ -1087,6 +1293,11 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -1108,6 +1319,15 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + canvas@2.11.2: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + nan: 2.23.0 + simple-get: 3.1.1 + transitivePeerDependencies: + - encoding + - supports-color + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -1115,6 +1335,8 @@ snapshots: chownr@1.1.4: {} + chownr@2.0.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -1127,10 +1349,16 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + concat-map@0.0.1: {} + + console-control-strings@1.1.0: {} + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -1154,6 +1382,10 @@ snapshots: dependencies: ms: 2.1.3 + decompress-response@4.2.1: + dependencies: + mimic-response: 2.1.0 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -1168,6 +1400,8 @@ snapshots: delayed-stream@1.0.0: {} + delegates@1.0.0: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -1324,8 +1558,26 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + function-bind@1.1.2: {} + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + generate-function@2.3.1: dependencies: is-property: 1.0.2 @@ -1358,12 +1610,28 @@ snapshots: transitivePeerDependencies: - supports-color + gifencoder@2.0.1: + dependencies: + canvas: 2.11.2 + transitivePeerDependencies: + - encoding + - supports-color + github-from-package@0.0.0: {} glob-parent@5.1.2: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -1385,6 +1653,8 @@ snapshots: dependencies: has-symbols: 1.1.0 + has-unicode@2.0.1: {} + has@1.0.4: {} hasown@2.0.2: @@ -1432,6 +1702,13 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ini@1.3.8: {} @@ -1469,6 +1746,8 @@ snapshots: isarray@1.0.0: {} + jpeg-js@0.4.4: {} + js-base64@3.7.8: {} jsesc@2.5.2: {} @@ -1479,12 +1758,27 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + long@5.3.2: {} lru-cache@7.18.3: {} lru.min@1.1.2: {} + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -1510,12 +1804,31 @@ snapshots: dependencies: mime-db: 1.54.0 + mimic-response@2.1.0: {} + mimic-response@3.1.0: {} + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + minimist@1.2.8: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} + moment@2.30.1: {} ms@2.1.3: {} @@ -1541,6 +1854,8 @@ snapshots: dependencies: lru-cache: 7.18.3 + nan@2.23.0: {} + napi-build-utils@1.0.2: {} negotiator@1.0.0: {} @@ -1555,6 +1870,17 @@ snapshots: dependencies: whatwg-url: 5.0.0 + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -1587,8 +1913,12 @@ snapshots: degenerator: 5.0.1 netmask: 2.0.2 + pako@1.0.11: {} + parseurl@1.3.3: {} + path-is-absolute@1.0.1: {} + path-parse@1.0.7: {} path-to-regexp@8.3.0: {} @@ -1731,6 +2061,10 @@ snapshots: reusify@1.1.0: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -1751,6 +2085,8 @@ snapshots: safer-buffer@2.1.2: {} + semver@6.3.1: {} + semver@7.7.2: {} send@1.2.0: @@ -1780,6 +2116,10 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} side-channel-list@1.0.0: @@ -1810,8 +2150,16 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + simple-concat@1.0.1: {} + simple-get@3.1.1: + dependencies: + decompress-response: 4.2.1 + once: 1.4.0 + simple-concat: 1.0.1 + simple-get@4.0.1: dependencies: decompress-response: 6.0.0 @@ -1889,6 +2237,15 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -1928,6 +2285,10 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -1938,6 +2299,8 @@ snapshots: y18n@5.0.8: {} + yallist@4.0.0: {} + yargs-parser@20.2.9: {} yargs@16.2.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..141cffc --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +ignoredBuiltDependencies: + - canvas diff --git a/ui/package.json b/ui/package.json index cc36e77..1e95e9f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "axios": "^1.11.0", "pinia": "^3.0.3", "vue": "^3.5.18", + "jszip": "^3.10.1", "vue-router": "^4.5.1" }, "devDependencies": { diff --git a/ui/src/components/artwork/ArtworkGallery.vue b/ui/src/components/artwork/ArtworkGallery.vue index 24dc9db..3b6f675 100644 --- a/ui/src/components/artwork/ArtworkGallery.vue +++ b/ui/src/components/artwork/ArtworkGallery.vue @@ -1,20 +1,22 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/common/RegistryWidget.vue b/ui/src/components/common/RegistryWidget.vue index 221cfa0..6827ace 100644 --- a/ui/src/components/common/RegistryWidget.vue +++ b/ui/src/components/common/RegistryWidget.vue @@ -494,10 +494,10 @@ const rebuildRegistry = async () => { // 开始轮询进度 const startProgressPolling = () => { if (progressPollingInterval.value) { - clearInterval(progressPollingInterval.value); + window.clearInterval(progressPollingInterval.value); } - progressPollingInterval.value = setInterval(async () => { + progressPollingInterval.value = window.setInterval(async () => { if (!rebuildTaskId.value) return; try { diff --git a/ui/src/components/repository/components/SearchPanel.vue b/ui/src/components/repository/components/SearchPanel.vue index 15bcba1..104c3f4 100644 --- a/ui/src/components/repository/components/SearchPanel.vue +++ b/ui/src/components/repository/components/SearchPanel.vue @@ -58,8 +58,8 @@ const filterBy = ref(props.initialFilter) // 防抖搜索 let searchTimeout: number const debounceSearch = () => { - clearTimeout(searchTimeout) - searchTimeout = setTimeout(() => { + window.clearTimeout(searchTimeout) + searchTimeout = window.setTimeout(() => { emit('search', searchQuery.value) }, 300) } diff --git a/ui/src/services/api.ts b/ui/src/services/api.ts index a733752..22415f5 100644 --- a/ui/src/services/api.ts +++ b/ui/src/services/api.ts @@ -20,6 +20,16 @@ export const getImageProxyUrl = (originalUrl: string) => { return originalUrl; }; +// 获取Pximg资源(包括ZIP等文件)的代理URL +export const getPximgFileProxyUrl = (originalUrl: string) => { + if (!originalUrl) return ''; + if (originalUrl.includes('i.pximg.net')) { + const encodedUrl = encodeURIComponent(originalUrl); + return `${getApiBaseUrl()}/api/proxy/file?url=${encodedUrl}`; + } + return originalUrl; +}; + class ApiService { private client: AxiosInstance; @@ -104,4 +114,4 @@ class ApiService { } export const apiService = new ApiService(); -export default apiService; \ No newline at end of file +export default apiService; \ No newline at end of file diff --git a/ui/src/services/artwork.ts b/ui/src/services/artwork.ts index c59c584..4b2fce3 100644 --- a/ui/src/services/artwork.ts +++ b/ui/src/services/artwork.ts @@ -51,6 +51,13 @@ class ArtworkService { return apiService.get(`/api/artwork/${id}/images?size=${size}`); } + /** + * 获取Ugoira元数据(包含zip_urls和frames) + */ + async getUgoiraMeta(id: number): Promise> { + return apiService.get(`/api/artwork/${id}/ugoira`); + } + /** * 搜索作品 */ @@ -112,4 +119,4 @@ class ArtworkService { } export const artworkService = new ArtworkService(); -export default artworkService; \ No newline at end of file +export default artworkService; \ No newline at end of file