支持动图下载和预览

This commit is contained in:
2025-10-13 15:43:18 +08:00
parent e85f959fa6
commit 5be8ae9520
18 changed files with 909 additions and 32 deletions
+38 -1
View File
@@ -307,4 +307,41 @@ router.get('/:id/related', async (req, res) => {
}
});
module.exports = router;
/**
* 获取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;
+46 -2
View File
@@ -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=<encoded>
*/
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;
module.exports = router;
+40
View File
@@ -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;
+28
View File
@@ -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
}
+85 -16
View File
@@ -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;
}
+13 -1
View File
@@ -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);
}
+80
View File
@@ -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,
};
+4
View File
@@ -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",
+363
View File
@@ -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:
+2
View File
@@ -0,0 +1,2 @@
ignoredBuiltDependencies:
- canvas
+1
View File
@@ -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": {
+6 -3
View File
@@ -1,20 +1,22 @@
<template>
<div class="artwork-gallery">
<div class="main-image">
<div v-if="artwork.type !== 'ugoira'" class="main-image">
<img :src="getImageUrl(currentImageUrl)" :alt="artwork.title" @load="imageLoaded = true"
@error="imageError = true" :class="{ loaded: imageLoaded, error: imageError }" crossorigin="anonymous" />
@error="imageError = true" :class="{ loaded: imageLoaded, error: imageError }" crossorigin="anonymous" />
<div v-if="!imageLoaded && !imageError" class="image-placeholder">
<LoadingSpinner text="图片加载中..." />
</div>
<div v-if="imageError" class="image-error">
<span>图片加载失败</span>
</div>
<!-- 页面切换时的遮罩层 -->
<div v-if="loading" class="image-overlay">
<LoadingSpinner text="切换中..." />
</div>
</div>
<!-- Ugoira动图播放器 -->
<UgoiraPlayer v-else :artwork="artwork" />
<!-- 多页作品缩略图 -->
<div v-if="artwork.page_count > 1" class="thumbnails">
<button v-for="(page, index) in artwork.meta_pages" :key="index" @click="$emit('pageChange', index)"
@@ -40,6 +42,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getImageProxyUrl } from '@/services/api';
import type { Artwork } from '@/types';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import UgoiraPlayer from '@/components/artwork/UgoiraPlayer.vue';
interface Props {
artwork: Artwork;
@@ -740,7 +740,6 @@ input:checked+.slider:before {
margin: 0 auto;
}
/* 移动端导航优化 */
.artwork-navigation {
position: sticky;
bottom: 0;
@@ -751,13 +750,16 @@ input:checked+.slider:before {
border-radius: var(--radius-xl);
padding: var(--spacing-lg);
margin: var(--spacing-xl) 0 0 0;
display: grid;
grid-template-columns: auto 1fr auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-md);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15);
z-index: 1001;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
flex-wrap: nowrap;
min-height: 56px; /* 确保最小高度以适应按钮 */
}
.nav-back {
@@ -767,6 +769,7 @@ input:checked+.slider:before {
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
border-color: var(--color-border);
flex-shrink: 0;
}
.nav-back span {
@@ -780,6 +783,9 @@ input:checked+.slider:before {
font-weight: 600;
border-radius: var(--radius-lg);
height: 44px;
flex: 1;
flex-shrink: 1;
min-width: 100px; /* 确保按钮最小宽度 */
}
.nav-prev {
+171
View File
@@ -0,0 +1,171 @@
<template>
<div class="ugoira-player">
<div class="player-stage">
<img v-if="currentFrameUrl" :src="currentFrameUrl" class="stage-image" crossorigin="anonymous" />
<div v-else class="stage-placeholder">
<LoadingSpinner text="动图加载中..." />
</div>
<div v-if="error" class="stage-error">{{ error }}</div>
</div>
<div class="player-controls">
<button class="btn btn-primary btn-small" @click="togglePlay" :disabled="loading || !!error">
{{ playing ? '暂停' : '播放' }}
</button>
<span class="status-text" v-if="loading">预加载帧 {{ loadedCount }}/{{ frames.length }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import JSZip from 'jszip';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import artworkService from '@/services/artwork';
import { getPximgFileProxyUrl } from '@/services/api';
import type { Artwork } from '@/types';
interface Props {
artwork: Artwork;
}
const props = defineProps<Props>();
const loading = ref(true);
const error = ref<string | null>(null);
const frames = ref<{ file: string; delay: number }[]>([]);
const frameUrls = ref<string[]>([]);
const currentFrameIndex = ref(0);
const playing = ref(true);
let timer: number | null = null;
const loadedCount = ref(0);
const currentFrameUrl = ref<string>('');
const clearTimer = () => {
if (timer) {
window.clearTimeout(timer);
timer = null;
}
};
const cleanupUrls = () => {
frameUrls.value.forEach((url) => URL.revokeObjectURL(url));
frameUrls.value = [];
};
const scheduleNextFrame = () => {
clearTimer();
if (!playing.value || frames.value.length === 0) return;
const delay = frames.value[currentFrameIndex.value]?.delay || 60;
timer = window.setTimeout(() => {
currentFrameIndex.value = (currentFrameIndex.value + 1) % frames.value.length;
currentFrameUrl.value = frameUrls.value[currentFrameIndex.value] || '';
scheduleNextFrame();
}, delay);
};
const togglePlay = () => {
playing.value = !playing.value;
if (playing.value) scheduleNextFrame();
else clearTimer();
};
const loadUgoira = async () => {
try {
loading.value = true;
error.value = null;
// 获取元数据
const metaResp = await artworkService.getUgoiraMeta(props.artwork.id);
if (!metaResp.success || !metaResp.data) throw new Error(metaResp.error || '获取ugoira元数据失败');
frames.value = metaResp.data.frames || [];
// 优先使用原始zip,如果没有则用medium
const zipUrl = metaResp.data.zip_urls.original || metaResp.data.zip_urls.medium || '';
if (!zipUrl) throw new Error('缺少Ugoira ZIP地址');
const proxied = getPximgFileProxyUrl(zipUrl);
// 下载ZIP
const resp = await fetch(proxied);
if (!resp.ok) throw new Error(`下载ZIP失败: ${resp.status}`);
const buf = await resp.arrayBuffer();
const zip = await JSZip.loadAsync(buf);
// 预加载帧
const orderedFrames = frames.value.slice().sort((a, b) => a.file.localeCompare(b.file));
for (const fr of orderedFrames) {
const fileEntry = zip.file(fr.file);
if (!fileEntry) continue;
const blob = await fileEntry.async('blob');
const url = URL.createObjectURL(blob);
frameUrls.value.push(url);
loadedCount.value = frameUrls.value.length;
}
if (frameUrls.value.length === 0) throw new Error('ZIP中未找到帧图片');
currentFrameIndex.value = 0;
currentFrameUrl.value = frameUrls.value[0];
loading.value = false;
playing.value = true;
scheduleNextFrame();
} catch (e: any) {
error.value = e?.message || '加载ugoira失败';
loading.value = false;
playing.value = false;
clearTimer();
}
};
onMounted(() => {
loadUgoira();
});
onUnmounted(() => {
clearTimer();
cleanupUrls();
});
// 当artwork变化时重新加载
watch(() => props.artwork.id, () => {
clearTimer();
cleanupUrls();
loadUgoira();
});
</script>
<style scoped>
.ugoira-player {
background: var(--color-bg-primary);
border-radius: var(--radius-xl);
overflow: hidden;
}
.player-stage {
position: relative;
aspect-ratio: 1;
background: var(--color-bg-tertiary);
}
.stage-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.stage-placeholder,
.stage-error {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.player-controls {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.status-text {
color: var(--color-text-secondary);
font-size: 12px;
}
</style>
+2 -2
View File
@@ -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 {
@@ -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)
}
+11 -1
View File
@@ -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;
export default apiService;
+8 -1
View File
@@ -51,6 +51,13 @@ class ArtworkService {
return apiService.get<ArtworkImagesResponse>(`/api/artwork/${id}/images?size=${size}`);
}
/**
* 获取Ugoira元数据(包含zip_urls和frames
*/
async getUgoiraMeta(id: number): Promise<ApiResponse<{ artwork_id: number; zip_urls: { medium?: string; original?: string }; frames: { file: string; delay: number }[] }>> {
return apiService.get(`/api/artwork/${id}/ugoira`);
}
/**
* 搜索作品
*/
@@ -112,4 +119,4 @@ class ArtworkService {
}
export const artworkService = new ArtworkService();
export default artworkService;
export default artworkService;