增加下载同步功能,可以导出已下载作品。避免另一个设备的重复下载,修复日志bug
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
# 环境
|
# 环境
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
|
||||||
|
test
|
||||||
|
|
||||||
# 下载文件夹
|
# 下载文件夹
|
||||||
downloads/
|
downloads/
|
||||||
data/
|
data/
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ class CacheConfigManager {
|
|||||||
chunkSize: 1024 * 1024, // 1MB块大小
|
chunkSize: 1024 * 1024, // 1MB块大小
|
||||||
retryAttempts: 3, // 重试次数
|
retryAttempts: 3, // 重试次数
|
||||||
retryDelay: 2000, // 重试延迟
|
retryDelay: 2000, // 重试延迟
|
||||||
|
// 下载检测配置
|
||||||
|
useRegistryCheck: true, // 是否使用注册表检测(默认启用)
|
||||||
|
fallbackToScan: false, // 检测失败时是否回退到扫盘检测
|
||||||
maxFileSize: 50 * 1024 * 1024, // 最大文件大小 50MB
|
maxFileSize: 50 * 1024 * 1024, // 最大文件大小 50MB
|
||||||
},
|
},
|
||||||
// 新增Windows特定配置
|
// 新增Windows特定配置
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ const CONFIG_FILE_DIR = require('appdata-path').getAppDataPath('pmanager');
|
|||||||
const CONFIG_FILE = Path.resolve(CONFIG_FILE_DIR, 'config.json');
|
const CONFIG_FILE = Path.resolve(CONFIG_FILE_DIR, 'config.json');
|
||||||
|
|
||||||
// 创建logger实例
|
// 创建logger实例
|
||||||
const logger = defaultLogger.child('PixivBackend');
|
const logger = defaultLogger.child('PixivCore');
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function loggerMiddleware(req, res, next) {
|
|||||||
const isArtistArtworksQuery = /^\/api\/artist\/\d+\/artworks/.test(req.path);
|
const isArtistArtworksQuery = /^\/api\/artist\/\d+\/artworks/.test(req.path);
|
||||||
|
|
||||||
// 过滤掉作品详情请求
|
// 过滤掉作品详情请求
|
||||||
const isArtworkDetailQuery = /^\/api\/artwork\/\d+/.test(req.path);
|
const isArtworkDetailQuery = /^\/(?:api\/)?artwork\/\d+/.test(req.path);
|
||||||
|
|
||||||
// 过滤掉仓库下载检查请求
|
// 过滤掉仓库下载检查请求
|
||||||
const isRepositoryCheckDownloadedQuery = /^\/api\/repository\/check-downloaded\/\d+/.test(req.path);
|
const isRepositoryCheckDownloadedQuery = /^\/api\/repository\/check-downloaded\/\d+/.test(req.path);
|
||||||
|
|||||||
+190
-1
@@ -753,4 +753,193 @@ router.get('/stats', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
/**
|
||||||
|
* 导出下载注册表
|
||||||
|
* GET /api/download/registry/export
|
||||||
|
*/
|
||||||
|
router.get('/registry/export', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
const registryData = await downloadService.downloadRegistry.exportRegistry();
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="download-registry.json"');
|
||||||
|
res.json(registryData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('导出下载注册表失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入下载注册表
|
||||||
|
* POST /api/download/registry/import
|
||||||
|
*/
|
||||||
|
router.post('/registry/import', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { registryData } = req.body;
|
||||||
|
|
||||||
|
if (!registryData) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少注册表数据'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
const result = await downloadService.downloadRegistry.importRegistry(registryData);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('导入下载注册表失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动全盘扫描更新注册表
|
||||||
|
* POST /api/download/registry/rebuild
|
||||||
|
*/
|
||||||
|
router.post('/registry/rebuild', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
const fileManager = downloadService.fileManager;
|
||||||
|
const result = await downloadService.downloadRegistry.rebuildFromFileSystem(fileManager);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('重建下载注册表失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载注册表统计信息
|
||||||
|
* GET /api/download/registry/stats
|
||||||
|
*/
|
||||||
|
router.get('/registry/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
const stats = await downloadService.downloadRegistry.getStats();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: stats
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('获取下载注册表统计信息失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理下载注册表
|
||||||
|
* POST /api/download/registry/cleanup
|
||||||
|
*/
|
||||||
|
router.post('/registry/cleanup', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
const fileManager = downloadService.fileManager;
|
||||||
|
const result = await downloadService.downloadRegistry.cleanupRegistry(fileManager);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('清理下载注册表失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载检测配置
|
||||||
|
* GET /api/download/registry/config
|
||||||
|
*/
|
||||||
|
router.get('/registry/config', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
const config = await downloadService.cacheConfigManager.loadConfig();
|
||||||
|
|
||||||
|
// 提取下载相关的配置
|
||||||
|
const downloadConfig = {
|
||||||
|
useRegistryCheck: config.download?.useRegistryCheck !== false, // 默认启用
|
||||||
|
fallbackToScan: config.download?.fallbackToScan === true // 默认不启用
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: downloadConfig
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('获取下载检测配置失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新下载检测配置
|
||||||
|
* PUT /api/download/registry/config
|
||||||
|
*/
|
||||||
|
router.put('/registry/config', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { useRegistryCheck, fallbackToScan } = req.body;
|
||||||
|
|
||||||
|
if (typeof useRegistryCheck !== 'boolean' || typeof fallbackToScan !== 'boolean') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '配置参数必须是布尔值'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadService = req.backend.getDownloadService();
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
const updatedConfig = await downloadService.cacheConfigManager.updateConfig({
|
||||||
|
download: {
|
||||||
|
useRegistryCheck,
|
||||||
|
fallbackToScan
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
useRegistryCheck: updatedConfig.download?.useRegistryCheck !== false,
|
||||||
|
fallbackToScan: updatedConfig.download?.fallbackToScan === true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('更新下载检测配置失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -141,7 +141,7 @@ router.get('/artworks/:artworkId', async (req, res) => {
|
|||||||
router.delete('/artworks/:artworkId', async (req, res) => {
|
router.delete('/artworks/:artworkId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { artworkId } = req.params
|
const { artworkId } = req.params
|
||||||
const result = await repositoryService.deleteArtwork(artworkId)
|
const result = await repositoryService.deleteArtwork(artworkId, req)
|
||||||
res.json(ResponseUtil.success(result))
|
res.json(ResponseUtil.success(result))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json(ResponseUtil.error(error.message))
|
res.status(500).json(ResponseUtil.error(error.message))
|
||||||
@@ -380,4 +380,4 @@ function getContentType(extension) {
|
|||||||
return contentTypes[extension.toLowerCase()] || 'application/octet-stream'
|
return contentTypes[extension.toLowerCase()] || 'application/octet-stream'
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
@@ -28,6 +28,8 @@ class PixivServer {
|
|||||||
this.app = express();
|
this.app = express();
|
||||||
this.backend = null;
|
this.backend = null;
|
||||||
this.port = 3000; // 默认端口,会在init时重新设置
|
this.port = 3000; // 默认端口,会在init时重新设置
|
||||||
|
this.logLevel = process.env.LOG_LEVEL || 'info'; // 获取日志级别
|
||||||
|
this.isVerboseMode = ['debug', 'trace'].includes(this.logLevel.toLowerCase()); // 检查是否为详细模式
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +41,17 @@ class PixivServer {
|
|||||||
// 重新设置端口(从环境变量获取)
|
// 重新设置端口(从环境变量获取)
|
||||||
this.port = process.env.PORT || 3000;
|
this.port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// 如果启用了详细模式,输出调试信息
|
||||||
|
if (this.isVerboseMode) {
|
||||||
|
logger.info(`详细模式已启用 (日志级别: ${this.logLevel.toUpperCase()})`);
|
||||||
|
logger.debug('环境变量:', {
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
PORT: process.env.PORT,
|
||||||
|
PROXY_PORT: process.env.PROXY_PORT,
|
||||||
|
LOG_LEVEL: process.env.LOG_LEVEL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 设置代理
|
// 设置代理
|
||||||
proxyConfig.setEnvironmentVariables();
|
proxyConfig.setEnvironmentVariables();
|
||||||
|
|
||||||
@@ -106,6 +119,11 @@ class PixivServer {
|
|||||||
if (this.backend.isLoggedIn) {
|
if (this.backend.isLoggedIn) {
|
||||||
logger.info(`用户: ${this.backend.config.user?.account}`);
|
logger.info(`用户: ${this.backend.config.user?.account}`);
|
||||||
}
|
}
|
||||||
|
if (this.isVerboseMode) {
|
||||||
|
logger.info(`日志级别: ${this.logLevel.toUpperCase()}`);
|
||||||
|
logger.debug(`服务器端口: ${this.port}`);
|
||||||
|
logger.debug(`代理端口: ${process.env.PROXY_PORT || '未设置'}`);
|
||||||
|
}
|
||||||
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,19 @@ class DownloadExecutor {
|
|||||||
await this.taskManager.saveTasks();
|
await this.taskManager.saveTasks();
|
||||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||||
|
|
||||||
|
// 如果下载成功,更新下载注册表
|
||||||
|
if (task.status === 'completed') {
|
||||||
|
try {
|
||||||
|
await this.downloadService.downloadRegistry.addArtwork(task.artist_name, task.artwork_id);
|
||||||
|
logger.debug('已更新下载注册表', {
|
||||||
|
artistName: task.artist_name,
|
||||||
|
artworkId: task.artwork_id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('更新下载注册表失败:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加到历史记录
|
// 添加到历史记录
|
||||||
const historyItem = {
|
const historyItem = {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
|
|||||||
@@ -0,0 +1,557 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const { defaultLogger } = require('../utils/logger');
|
||||||
|
|
||||||
|
// 创建logger实例
|
||||||
|
const logger = defaultLogger.child('DownloadRegistry');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载记录管理器 - 维护已下载作品的JSON记录
|
||||||
|
* 用于快速检测作品是否已下载,支持导入导出和多设备同步
|
||||||
|
*/
|
||||||
|
class DownloadRegistry {
|
||||||
|
constructor(dataPath) {
|
||||||
|
this.dataPath = dataPath;
|
||||||
|
this.registryPath = path.join(dataPath, 'download_registry.json');
|
||||||
|
this.registry = {
|
||||||
|
version: '1.0.5',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
artists: {} // 格式: { artistName: { artworks: [artworkId1, artworkId2, ...] } }
|
||||||
|
};
|
||||||
|
this.loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化注册表
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// 确保数据目录存在
|
||||||
|
await fs.ensureDir(this.dataPath);
|
||||||
|
|
||||||
|
// 加载现有注册表
|
||||||
|
await this.loadRegistry();
|
||||||
|
|
||||||
|
logger.info(`下载记录注册表初始化完成,总共包含${Object.keys(this.registry.artists).length}个作者,${this.getTotalArtworkCount()}个工作品`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('下载记录注册表初始化失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载注册表文件
|
||||||
|
*/
|
||||||
|
async loadRegistry() {
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(this.registryPath)) {
|
||||||
|
const data = await fs.readJson(this.registryPath);
|
||||||
|
|
||||||
|
// 验证数据格式
|
||||||
|
if (data && typeof data === 'object' && data.artists) {
|
||||||
|
this.registry = {
|
||||||
|
version: data.version || '1.0.0',
|
||||||
|
created_at: data.created_at || new Date().toISOString(),
|
||||||
|
updated_at: data.updated_at || new Date().toISOString(),
|
||||||
|
artists: data.artists || {}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
logger.warn('注册表文件格式不正确,使用默认格式');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('注册表文件不存在,将创建新的注册表');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('加载注册表文件失败:', error);
|
||||||
|
// 使用默认注册表
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存注册表到文件
|
||||||
|
*/
|
||||||
|
async saveRegistry() {
|
||||||
|
try {
|
||||||
|
this.registry.updated_at = new Date().toISOString();
|
||||||
|
await fs.writeJson(this.registryPath, this.registry, { spaces: 2 });
|
||||||
|
logger.debug('注册表已保存到文件', { path: this.registryPath });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('保存注册表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加已下载的作品记录
|
||||||
|
* @param {string} artistName - 作者名称
|
||||||
|
* @param {number|string} artworkId - 作品ID
|
||||||
|
*/
|
||||||
|
async addArtwork(artistName, artworkId) {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedArtistName = this.normalizeArtistName(artistName);
|
||||||
|
const normalizedArtworkId = parseInt(artworkId);
|
||||||
|
|
||||||
|
if (!this.registry.artists[normalizedArtistName]) {
|
||||||
|
this.registry.artists[normalizedArtistName] = {
|
||||||
|
artworks: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
if (!this.registry.artists[normalizedArtistName].artworks.includes(normalizedArtworkId)) {
|
||||||
|
this.registry.artists[normalizedArtistName].artworks.push(normalizedArtworkId);
|
||||||
|
this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a); // 按ID倒序排列
|
||||||
|
|
||||||
|
await this.saveRegistry();
|
||||||
|
logger.debug('添加作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除作品记录
|
||||||
|
* @param {string} artistName - 作者名称
|
||||||
|
* @param {number|string} artworkId - 作品ID
|
||||||
|
*/
|
||||||
|
async removeArtwork(artistName, artworkId) {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedArtistName = this.normalizeArtistName(artistName);
|
||||||
|
const normalizedArtworkId = parseInt(artworkId);
|
||||||
|
|
||||||
|
logger.debug('开始移除作品记录', {
|
||||||
|
originalArtistName: artistName,
|
||||||
|
normalizedArtistName: normalizedArtistName,
|
||||||
|
artworkId: normalizedArtworkId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.registry.artists[normalizedArtistName]) {
|
||||||
|
const artworks = this.registry.artists[normalizedArtistName].artworks;
|
||||||
|
const index = artworks.indexOf(normalizedArtworkId);
|
||||||
|
|
||||||
|
logger.debug('查找作品在注册表中的位置', {
|
||||||
|
artistName: normalizedArtistName,
|
||||||
|
artworkId: normalizedArtworkId,
|
||||||
|
index: index,
|
||||||
|
artworks: artworks
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
artworks.splice(index, 1);
|
||||||
|
|
||||||
|
// 如果作者下没有作品了,删除作者记录
|
||||||
|
if (artworks.length === 0) {
|
||||||
|
delete this.registry.artists[normalizedArtistName];
|
||||||
|
logger.info('作者下无作品,删除作者记录', { artistName: normalizedArtistName });
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveRegistry();
|
||||||
|
logger.debug('成功移除作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId });
|
||||||
|
} else {
|
||||||
|
logger.warn('作品在注册表中未找到', { artistName: normalizedArtistName, artworkId: normalizedArtworkId });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('作者在注册表中未找到', { artistName: normalizedArtistName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查作品是否已下载
|
||||||
|
* @param {number|string} artworkId - 作品ID
|
||||||
|
* @returns {boolean} 是否已下载
|
||||||
|
*/
|
||||||
|
async isArtworkDownloaded(artworkId) {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedArtworkId = parseInt(artworkId);
|
||||||
|
|
||||||
|
// 遍历所有作者查找作品
|
||||||
|
for (const artistName in this.registry.artists) {
|
||||||
|
if (this.registry.artists[artistName].artworks.includes(normalizedArtworkId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已下载的作品ID列表
|
||||||
|
* @returns {number[]} 作品ID数组
|
||||||
|
*/
|
||||||
|
async getDownloadedArtworkIds() {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const artworkIds = new Set();
|
||||||
|
|
||||||
|
for (const artistName in this.registry.artists) {
|
||||||
|
for (const artworkId of this.registry.artists[artistName].artworks) {
|
||||||
|
artworkIds.add(artworkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(artworkIds).sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定作者的已下载作品
|
||||||
|
* @param {string} artistName - 作者名称
|
||||||
|
* @returns {number[]} 作品ID数组
|
||||||
|
*/
|
||||||
|
async getArtistArtworks(artistName) {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedArtistName = this.normalizeArtistName(artistName);
|
||||||
|
|
||||||
|
if (this.registry.artists[normalizedArtistName]) {
|
||||||
|
return [...this.registry.artists[normalizedArtistName].artworks];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已下载的作者列表
|
||||||
|
* @returns {string[]} 作者名称数组
|
||||||
|
*/
|
||||||
|
async getDownloadedArtists() {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(this.registry.artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*/
|
||||||
|
async getStats() {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const artists = Object.keys(this.registry.artists);
|
||||||
|
const totalArtworks = this.getTotalArtworkCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
artistCount: artists.length,
|
||||||
|
artworkCount: totalArtworks,
|
||||||
|
version: this.registry.version,
|
||||||
|
created_at: this.registry.created_at,
|
||||||
|
updated_at: this.registry.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出注册表数据
|
||||||
|
* @returns {Object} 注册表数据
|
||||||
|
*/
|
||||||
|
async exportRegistry() {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this.registry,
|
||||||
|
exported_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入注册表数据(增量导入,不覆盖现有数据)
|
||||||
|
* @param {Object} importData - 要导入的数据
|
||||||
|
* @returns {Object} 导入结果统计
|
||||||
|
*/
|
||||||
|
async importRegistry(importData) {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!importData || !importData.artists) {
|
||||||
|
throw new Error('导入数据格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
let addedArtists = 0;
|
||||||
|
let addedArtworks = 0;
|
||||||
|
let skippedArtworks = 0;
|
||||||
|
|
||||||
|
for (const artistName in importData.artists) {
|
||||||
|
const normalizedArtistName = this.normalizeArtistName(artistName);
|
||||||
|
const importArtworks = importData.artists[artistName].artworks || [];
|
||||||
|
|
||||||
|
if (!this.registry.artists[normalizedArtistName]) {
|
||||||
|
this.registry.artists[normalizedArtistName] = { artworks: [] };
|
||||||
|
addedArtists++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingArtworks = new Set(this.registry.artists[normalizedArtistName].artworks);
|
||||||
|
|
||||||
|
for (const artworkId of importArtworks) {
|
||||||
|
const normalizedArtworkId = parseInt(artworkId);
|
||||||
|
if (!existingArtworks.has(normalizedArtworkId)) {
|
||||||
|
this.registry.artists[normalizedArtistName].artworks.push(normalizedArtworkId);
|
||||||
|
addedArtworks++;
|
||||||
|
} else {
|
||||||
|
skippedArtworks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveRegistry();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
addedArtists,
|
||||||
|
addedArtworks,
|
||||||
|
skippedArtworks,
|
||||||
|
totalArtists: Object.keys(this.registry.artists).length,
|
||||||
|
totalArtworks: this.getTotalArtworkCount()
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('注册表导入完成', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件系统扫描并重建注册表
|
||||||
|
* @param {Object} fileManager - 文件管理器实例
|
||||||
|
* @returns {Object} 扫描结果统计
|
||||||
|
*/
|
||||||
|
async rebuildFromFileSystem(fileManager) {
|
||||||
|
try {
|
||||||
|
logger.info('开始从文件系统扫描并添加新作品到注册表...');
|
||||||
|
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
let scannedArtists = 0;
|
||||||
|
let scannedArtworks = 0;
|
||||||
|
let addedArtworks = 0;
|
||||||
|
let skippedArtworks = 0;
|
||||||
|
|
||||||
|
const downloadPath = await fileManager.getDownloadPath();
|
||||||
|
logger.debug(`扫描下载路径: ${downloadPath}`);
|
||||||
|
|
||||||
|
const artists = await fileManager.listDirectory(downloadPath);
|
||||||
|
logger.debug(`找到 ${artists.length} 个作者目录`);
|
||||||
|
|
||||||
|
for (const artist of artists) {
|
||||||
|
try {
|
||||||
|
const artistPath = path.join(downloadPath, artist);
|
||||||
|
const artistStat = await fileManager.getFileInfo(artistPath);
|
||||||
|
|
||||||
|
if (artistStat.exists && artistStat.isDirectory) {
|
||||||
|
scannedArtists++;
|
||||||
|
logger.debug(`扫描作者: ${artist}`);
|
||||||
|
|
||||||
|
const artworks = await fileManager.listDirectory(artistPath);
|
||||||
|
|
||||||
|
for (const artwork of artworks) {
|
||||||
|
try {
|
||||||
|
const artworkPath = path.join(artistPath, artwork);
|
||||||
|
const artworkStat = await fileManager.getFileInfo(artworkPath);
|
||||||
|
|
||||||
|
if (artworkStat.exists && artworkStat.isDirectory) {
|
||||||
|
scannedArtworks++;
|
||||||
|
|
||||||
|
// 检查是否是作品目录(包含数字ID)
|
||||||
|
const artworkMatch = artwork.match(/^(\d+)_(.+)$/);
|
||||||
|
if (artworkMatch) {
|
||||||
|
const artworkId = parseInt(artworkMatch[1]);
|
||||||
|
|
||||||
|
// 检查作品是否已经在注册表中
|
||||||
|
const isAlreadyRegistered = await this.isArtworkDownloaded(artworkId);
|
||||||
|
if (isAlreadyRegistered) {
|
||||||
|
skippedArtworks++;
|
||||||
|
continue; // 跳过已注册的作品
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查作品信息文件和图片文件
|
||||||
|
const infoPath = path.join(artworkPath, 'artwork_info.json');
|
||||||
|
let artworkInfo;
|
||||||
|
try {
|
||||||
|
const infoContent = await fs.readFile(infoPath, 'utf8');
|
||||||
|
artworkInfo = JSON.parse(infoContent);
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`读取作品信息文件失败: ${infoPath}`, error);
|
||||||
|
continue; // 跳过没有信息文件的目录
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有图片文件
|
||||||
|
const files = await fileManager.listDirectory(artworkPath);
|
||||||
|
const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file));
|
||||||
|
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
// 检查图片数量是否与artwork_info.json中记录的一致
|
||||||
|
const expectedImageCount = artworkInfo.page_count || 1;
|
||||||
|
if (imageFiles.length >= expectedImageCount) {
|
||||||
|
// 添加到注册表(只添加新的)
|
||||||
|
await this.addArtwork(artist, artworkId);
|
||||||
|
addedArtworks++;
|
||||||
|
logger.debug(`添加作品到注册表: ${artist} - ${artworkId}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`作品图片数量不足: ${artist} - ${artworkId}, 期望: ${expectedImageCount}, 实际: ${imageFiles.length}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug(`作品目录无图片文件: ${artworkPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`处理作品目录 ${artwork} 时出错:`, error);
|
||||||
|
continue; // 跳过有问题的作品目录
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`处理作者目录 ${artist} 时出错:`, error);
|
||||||
|
continue; // 跳过有问题的作者目录
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
scannedArtists,
|
||||||
|
scannedArtworks,
|
||||||
|
addedArtworks,
|
||||||
|
skippedArtworks,
|
||||||
|
totalRegisteredArtists: Object.keys(this.registry.artists).length,
|
||||||
|
totalRegisteredArtworks: this.getTotalArtworkCount()
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('注册表扫描完成', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('注册表扫描失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理注册表(移除不存在的记录)
|
||||||
|
* @param {Object} fileManager - 文件管理器实例
|
||||||
|
* @returns {Object} 清理结果统计
|
||||||
|
*/
|
||||||
|
async cleanupRegistry(fileManager) {
|
||||||
|
try {
|
||||||
|
if (!this.loaded) {
|
||||||
|
await this.loadRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('开始清理注册表...');
|
||||||
|
|
||||||
|
let removedArtists = 0;
|
||||||
|
let removedArtworks = 0;
|
||||||
|
const downloadPath = await fileManager.getDownloadPath();
|
||||||
|
|
||||||
|
for (const artistName in this.registry.artists) {
|
||||||
|
const artworks = [...this.registry.artists[artistName].artworks];
|
||||||
|
let validArtworks = [];
|
||||||
|
|
||||||
|
for (const artworkId of artworks) {
|
||||||
|
// 检查作品目录是否存在
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const artistPath = path.join(downloadPath, artistName);
|
||||||
|
if (await fileManager.directoryExists(artistPath)) {
|
||||||
|
const artworkEntries = await fileManager.listDirectory(artistPath);
|
||||||
|
|
||||||
|
for (const entry of artworkEntries) {
|
||||||
|
const match = entry.match(/^(\d+)_(.+)$/);
|
||||||
|
if (match && parseInt(match[1]) === artworkId) {
|
||||||
|
const artworkPath = path.join(artistPath, entry);
|
||||||
|
const infoPath = path.join(artworkPath, 'artwork_info.json');
|
||||||
|
|
||||||
|
// 检查信息文件是否存在
|
||||||
|
if (await fs.pathExists(infoPath)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`检查作品 ${artworkId} 时出错:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
validArtworks.push(artworkId);
|
||||||
|
} else {
|
||||||
|
removedArtworks++;
|
||||||
|
logger.debug(`移除无效作品记录: ${artistName} - ${artworkId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validArtworks.length > 0) {
|
||||||
|
this.registry.artists[artistName].artworks = validArtworks;
|
||||||
|
} else {
|
||||||
|
delete this.registry.artists[artistName];
|
||||||
|
removedArtists++;
|
||||||
|
logger.debug(`移除空作者记录: ${artistName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveRegistry();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
removedArtists,
|
||||||
|
removedArtworks,
|
||||||
|
remainingArtists: Object.keys(this.registry.artists).length,
|
||||||
|
remainingArtworks: this.getTotalArtworkCount()
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('注册表清理完成', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('注册表清理失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化作者名称(处理特殊字符)
|
||||||
|
*/
|
||||||
|
normalizeArtistName(artistName) {
|
||||||
|
if (!artistName || typeof artistName !== 'string') {
|
||||||
|
return 'Unknown Artist';
|
||||||
|
}
|
||||||
|
return artistName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总作品数量
|
||||||
|
*/
|
||||||
|
getTotalArtworkCount() {
|
||||||
|
let total = 0;
|
||||||
|
for (const artistName in this.registry.artists) {
|
||||||
|
total += this.registry.artists[artistName].artworks.length;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取注册表文件路径
|
||||||
|
*/
|
||||||
|
getRegistryPath() {
|
||||||
|
return this.registryPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DownloadRegistry;
|
||||||
@@ -6,6 +6,7 @@ const FileManager = require('./file-manager');
|
|||||||
const ProgressManager = require('./progress-manager');
|
const ProgressManager = require('./progress-manager');
|
||||||
const HistoryManager = require('./history-manager');
|
const HistoryManager = require('./history-manager');
|
||||||
const DownloadExecutor = require('./download-executor');
|
const DownloadExecutor = require('./download-executor');
|
||||||
|
const DownloadRegistry = require('./download-registry');
|
||||||
const CacheConfigManager = require('../config/cache-config');
|
const CacheConfigManager = require('../config/cache-config');
|
||||||
const fs = require('fs-extra'); // Added for fs-extra
|
const fs = require('fs-extra'); // Added for fs-extra
|
||||||
const { defaultLogger } = require('../utils/logger');
|
const { defaultLogger } = require('../utils/logger');
|
||||||
@@ -40,6 +41,7 @@ class DownloadService {
|
|||||||
this.taskManager = new TaskManager(this.dataPath);
|
this.taskManager = new TaskManager(this.dataPath);
|
||||||
this.progressManager = new ProgressManager();
|
this.progressManager = new ProgressManager();
|
||||||
this.historyManager = new HistoryManager(this.dataPath);
|
this.historyManager = new HistoryManager(this.dataPath);
|
||||||
|
this.downloadRegistry = new DownloadRegistry(this.dataPath);
|
||||||
// 先创建下载执行器,稍后在init方法中设置downloadService引用
|
// 先创建下载执行器,稍后在init方法中设置downloadService引用
|
||||||
this.downloadExecutor = new DownloadExecutor(this.fileManager, this.taskManager, this.progressManager, this.historyManager, this);
|
this.downloadExecutor = new DownloadExecutor(this.fileManager, this.taskManager, this.progressManager, this.historyManager, this);
|
||||||
|
|
||||||
@@ -78,6 +80,7 @@ class DownloadService {
|
|||||||
// 初始化各个管理器
|
// 初始化各个管理器
|
||||||
await this.taskManager.init();
|
await this.taskManager.init();
|
||||||
await this.historyManager.init();
|
await this.historyManager.init();
|
||||||
|
await this.downloadRegistry.init();
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
// 下载服务初始化完成
|
// 下载服务初始化完成
|
||||||
@@ -463,6 +466,40 @@ class DownloadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isArtworkDownloaded(artworkId) {
|
async isArtworkDownloaded(artworkId) {
|
||||||
|
try {
|
||||||
|
// 获取配置,决定使用哪种检测方式
|
||||||
|
const cacheConfig = await this.cacheConfigManager.loadConfig();
|
||||||
|
const useRegistryCheck = cacheConfig.download?.useRegistryCheck !== false; // 默认启用
|
||||||
|
const fallbackToScan = cacheConfig.download?.fallbackToScan === true; // 默认不启用
|
||||||
|
|
||||||
|
// 优先使用注册表检测(如果启用)
|
||||||
|
if (useRegistryCheck) {
|
||||||
|
try {
|
||||||
|
const isDownloaded = await this.downloadRegistry.isArtworkDownloaded(artworkId);
|
||||||
|
if (isDownloaded || !fallbackToScan) {
|
||||||
|
return isDownloaded;
|
||||||
|
}
|
||||||
|
// 如果注册表显示未下载但启用了回退,继续使用扫盘检测
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('注册表检测失败,使用扫盘检测:', error.message);
|
||||||
|
if (!fallbackToScan) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用原有的扫盘检测逻辑
|
||||||
|
return await this.isArtworkDownloadedByScan(artworkId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('检查作品下载状态失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过扫描文件系统检测作品是否已下载(原有逻辑)
|
||||||
|
*/
|
||||||
|
async isArtworkDownloadedByScan(artworkId) {
|
||||||
try {
|
try {
|
||||||
const downloadPath = await this.fileManager.getDownloadPath();
|
const downloadPath = await this.fileManager.getDownloadPath();
|
||||||
|
|
||||||
@@ -520,7 +557,7 @@ class DownloadService {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('检查作品下载状态失败:', error);
|
logger.error('扫盘检查作品下载状态失败:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1114,79 @@ class DownloadService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除已下载的文件
|
||||||
|
* @param {string} artist - 作者名称
|
||||||
|
* @param {string} artwork - 作品目录名称
|
||||||
|
* @returns {Object} 删除结果
|
||||||
|
*/
|
||||||
|
async deleteDownloadedFiles(artist, artwork) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const downloadPath = await this.fileManager.getDownloadPath();
|
||||||
|
const artworkPath = path.join(downloadPath, artist, artwork);
|
||||||
|
|
||||||
|
// 检查作品目录是否存在
|
||||||
|
const artworkStat = await this.fileManager.getFileInfo(artworkPath);
|
||||||
|
if (!artworkStat.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '作品目录不存在'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从作品目录名称中提取作品ID
|
||||||
|
const artworkMatch = artwork.match(/^(\d+)_(.+)$/);
|
||||||
|
if (!artworkMatch) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '无效的作品目录格式'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const artworkId = parseInt(artworkMatch[1]);
|
||||||
|
|
||||||
|
// 删除作品目录
|
||||||
|
await this.fileManager.removeDirectory(artworkPath);
|
||||||
|
|
||||||
|
// 从注册表中移除作品记录
|
||||||
|
try {
|
||||||
|
await this.downloadRegistry.removeArtwork(artist, artworkId);
|
||||||
|
logger.debug('已从下载注册表中移除作品', {
|
||||||
|
artistName: artist,
|
||||||
|
artworkId: artworkId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('从下载注册表中移除作品失败:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查作者目录是否为空,如果为空则删除
|
||||||
|
const artistPath = path.join(downloadPath, artist);
|
||||||
|
try {
|
||||||
|
const artistEntries = await this.fileManager.listDirectory(artistPath);
|
||||||
|
const hasArtworks = artistEntries.some(entry => entry.match(/^\d+_/));
|
||||||
|
|
||||||
|
if (!hasArtworks) {
|
||||||
|
await this.fileManager.removeDirectory(artistPath);
|
||||||
|
logger.debug('已删除空的作者目录', { artistName: artist });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`检查作者目录失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '作品删除成功'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('删除作品失败:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = DownloadService;
|
module.exports = DownloadService;
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ class RepositoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除作品
|
// 删除作品
|
||||||
async deleteArtwork(artworkId) {
|
async deleteArtwork(artworkId, req) {
|
||||||
try {
|
try {
|
||||||
// 优化:直接通过文件系统查找,避免全仓库扫描
|
// 优化:直接通过文件系统查找,避免全仓库扫描
|
||||||
const artwork = await this.findArtworkByIdOptimized(artworkId)
|
const artwork = await this.findArtworkByIdOptimized(artworkId)
|
||||||
@@ -746,6 +746,23 @@ class RepositoryService {
|
|||||||
throw new Error('作品不存在')
|
throw new Error('作品不存在')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从注册表中移除作品记录
|
||||||
|
try {
|
||||||
|
// 使用共享的下载服务实例,而不是创建新实例
|
||||||
|
const downloadService = req.backend?.getDownloadService();
|
||||||
|
if (downloadService) {
|
||||||
|
await downloadService.downloadRegistry.removeArtwork(artwork.artist, artworkId);
|
||||||
|
logger.debug('已从下载注册表中移除作品', {
|
||||||
|
artistName: artwork.artist,
|
||||||
|
artworkId: artworkId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn('无法获取下载服务实例,跳过注册表更新');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('从下载注册表中移除作品失败:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
await fs.rm(artwork.path, { recursive: true, force: true })
|
await fs.rm(artwork.path, { recursive: true, force: true })
|
||||||
|
|
||||||
// 优化:直接检查作者目录是否为空,避免重复扫描
|
// 优化:直接检查作者目录是否为空,避免重复扫描
|
||||||
|
|||||||
+26
-5
@@ -11,10 +11,6 @@ if (!process.env.UV_THREADPOOL_SIZE) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PixivServer = require('./server');
|
const PixivServer = require('./server');
|
||||||
const { defaultLogger } = require('./utils/logger');
|
|
||||||
|
|
||||||
// 创建logger实例
|
|
||||||
const logger = defaultLogger.child('Start');
|
|
||||||
|
|
||||||
// 解析命令行参数
|
// 解析命令行参数
|
||||||
function parseArguments() {
|
function parseArguments() {
|
||||||
@@ -35,7 +31,12 @@ function parseArguments() {
|
|||||||
if (!isNaN(port)) {
|
if (!isNaN(port)) {
|
||||||
options.serverPort = port;
|
options.serverPort = port;
|
||||||
}
|
}
|
||||||
}
|
} else if (arg.startsWith('--log-level=')) {
|
||||||
|
const level = arg.split('=')[1].toUpperCase();
|
||||||
|
if (['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(level)) {
|
||||||
|
options.logLevel = level;
|
||||||
|
}
|
||||||
|
}
|
||||||
// 处理 --key value 格式(向后兼容)
|
// 处理 --key value 格式(向后兼容)
|
||||||
else if (arg === '--proxy-port' && i + 1 < args.length) {
|
else if (arg === '--proxy-port' && i + 1 < args.length) {
|
||||||
const port = parseInt(args[i + 1]);
|
const port = parseInt(args[i + 1]);
|
||||||
@@ -49,6 +50,12 @@ function parseArguments() {
|
|||||||
options.serverPort = port;
|
options.serverPort = port;
|
||||||
}
|
}
|
||||||
i++; // 跳过下一个参数
|
i++; // 跳过下一个参数
|
||||||
|
} else if (arg === '--log-level' && i + 1 < args.length) {
|
||||||
|
const level = args[i + 1].toUpperCase();
|
||||||
|
if (['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(level)) {
|
||||||
|
options.logLevel = level;
|
||||||
|
}
|
||||||
|
i++; // 跳过下一个参数
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +68,15 @@ const cliOptions = parseArguments();
|
|||||||
// 设置环境变量
|
// 设置环境变量
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
// 设置日志级别环境变量
|
||||||
|
if (cliOptions.logLevel) {
|
||||||
|
process.env.LOG_LEVEL = cliOptions.logLevel.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在设置环境变量后导入logger
|
||||||
|
const { defaultLogger } = require('./utils/logger');
|
||||||
|
const logger = defaultLogger.child('Start');
|
||||||
|
|
||||||
// 如果提供了代理端口,设置环境变量
|
// 如果提供了代理端口,设置环境变量
|
||||||
if (cliOptions.proxyPort) {
|
if (cliOptions.proxyPort) {
|
||||||
process.env.PROXY_PORT = cliOptions.proxyPort.toString();
|
process.env.PROXY_PORT = cliOptions.proxyPort.toString();
|
||||||
@@ -73,6 +89,11 @@ if (cliOptions.serverPort) {
|
|||||||
logger.info(`服务器端口已设置为: ${cliOptions.serverPort}`);
|
logger.info(`服务器端口已设置为: ${cliOptions.serverPort}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 输出日志级别信息
|
||||||
|
if (cliOptions.logLevel) {
|
||||||
|
logger.info(`日志级别: ${cliOptions.logLevel}`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('启动 Pixiv 后端服务器...');
|
logger.info('启动 Pixiv 后端服务器...');
|
||||||
|
|
||||||
// 创建服务器实例
|
// 创建服务器实例
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
const PixivBackend = require('./core');
|
|
||||||
const proxyConfig = require('./config');
|
|
||||||
const readline = require('readline');
|
|
||||||
const { defaultLogger } = require('./utils/logger');
|
|
||||||
|
|
||||||
// 创建logger实例
|
|
||||||
const logger = defaultLogger.child('TestLogin');
|
|
||||||
|
|
||||||
|
|
||||||
// 创建命令行交互接口
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
// 询问用户输入
|
|
||||||
function askQuestion(question) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
rl.question(question, (answer) => {
|
|
||||||
resolve(answer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试登录流程
|
|
||||||
async function testLogin() {
|
|
||||||
logger.info('=== Pixiv 登录测试脚本 ===\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 设置代理环境变量
|
|
||||||
logger.info('1. 设置代理配置...');
|
|
||||||
proxyConfig.setEnvironmentVariables();
|
|
||||||
|
|
||||||
// 2. 初始化后端
|
|
||||||
logger.info('\n2. 初始化 Pixiv 后端...');
|
|
||||||
const backend = new PixivBackend();
|
|
||||||
await backend.init();
|
|
||||||
|
|
||||||
// 3. 检查登录状态
|
|
||||||
logger.info('\n3. 检查当前登录状态...');
|
|
||||||
const loginStatus = backend.getLoginStatus();
|
|
||||||
logger.info('登录状态:', loginStatus);
|
|
||||||
|
|
||||||
if (loginStatus.isLoggedIn) {
|
|
||||||
logger.info('✅ 已登录,用户:', loginStatus.username);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 获取登录URL
|
|
||||||
logger.info('\n4. 获取登录URL...');
|
|
||||||
const loginData = backend.getLoginUrl();
|
|
||||||
logger.info('请访问以下URL进行登录:');
|
|
||||||
logger.info(loginData.login_url);
|
|
||||||
logger.info('\n登录完成后,请复制回调URL中的code参数');
|
|
||||||
|
|
||||||
// 5. 等待用户输入授权码
|
|
||||||
const code = await askQuestion('\n请输入授权码 (code参数): ');
|
|
||||||
|
|
||||||
if (!code || code.trim() === '') {
|
|
||||||
logger.info('❌ 未输入授权码,测试终止');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 处理登录回调
|
|
||||||
logger.info('\n5. 处理登录回调...');
|
|
||||||
const loginResult = await backend.handleLoginCallback(code.trim());
|
|
||||||
|
|
||||||
if (loginResult.success) {
|
|
||||||
logger.info('✅ 登录成功!');
|
|
||||||
logger.info('用户信息:', loginResult.user);
|
|
||||||
|
|
||||||
// 7. 再次检查登录状态
|
|
||||||
logger.info('\n6. 验证登录状态...');
|
|
||||||
const finalStatus = backend.getLoginStatus();
|
|
||||||
logger.info('最终登录状态:', finalStatus);
|
|
||||||
|
|
||||||
// 8. 测试获取用户信息
|
|
||||||
logger.info('\n7. 测试获取用户信息...');
|
|
||||||
const auth = backend.getAuth();
|
|
||||||
const userInfo = await auth.getUserInfo();
|
|
||||||
|
|
||||||
if (userInfo.success) {
|
|
||||||
logger.info('✅ 获取用户信息成功:', userInfo.user);
|
|
||||||
} else {
|
|
||||||
logger.info('❌ 获取用户信息失败:', userInfo.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
logger.info('❌ 登录失败:', loginResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ 测试过程中发生错误:', error.message);
|
|
||||||
logger.error('错误详情:', error);
|
|
||||||
} finally {
|
|
||||||
// 清理资源
|
|
||||||
rl.close();
|
|
||||||
logger.info('\n=== 测试完成 ===');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试重新登录功能
|
|
||||||
async function testRelogin() {
|
|
||||||
logger.info('=== 测试重新登录功能 ===\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设置代理
|
|
||||||
proxyConfig.setEnvironmentVariables();
|
|
||||||
|
|
||||||
// 初始化后端
|
|
||||||
const backend = new PixivBackend();
|
|
||||||
await backend.init();
|
|
||||||
|
|
||||||
// 检查是否有保存的登录信息
|
|
||||||
const loginStatus = backend.getLoginStatus();
|
|
||||||
|
|
||||||
if (loginStatus.isLoggedIn) {
|
|
||||||
logger.info('✅ 检测到已保存的登录信息');
|
|
||||||
logger.info('用户:', loginStatus.username);
|
|
||||||
logger.info('用户ID:', loginStatus.user_id);
|
|
||||||
} else {
|
|
||||||
logger.info('❌ 没有保存的登录信息,无法测试重新登录');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ 重新登录测试失败:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试登出功能
|
|
||||||
async function testLogout() {
|
|
||||||
logger.info('=== 测试登出功能 ===\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设置代理
|
|
||||||
proxyConfig.setEnvironmentVariables();
|
|
||||||
|
|
||||||
// 初始化后端
|
|
||||||
const backend = new PixivBackend();
|
|
||||||
await backend.init();
|
|
||||||
|
|
||||||
// 执行登出
|
|
||||||
const logoutResult = backend.logout();
|
|
||||||
|
|
||||||
if (logoutResult.success) {
|
|
||||||
logger.info('✅ 登出成功');
|
|
||||||
|
|
||||||
// 验证登出状态
|
|
||||||
const loginStatus = backend.getLoginStatus();
|
|
||||||
logger.info('登出后状态:', loginStatus);
|
|
||||||
} else {
|
|
||||||
logger.info('❌ 登出失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ 登出测试失败:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主函数
|
|
||||||
async function main() {
|
|
||||||
logger.info('请选择测试功能:');
|
|
||||||
logger.info('1. 测试完整登录流程');
|
|
||||||
logger.info('2. 测试重新登录');
|
|
||||||
logger.info('3. 测试登出');
|
|
||||||
logger.info('4. 运行所有测试');
|
|
||||||
|
|
||||||
const choice = await askQuestion('\n请输入选择 (1-4): ');
|
|
||||||
|
|
||||||
switch (choice.trim()) {
|
|
||||||
case '1':
|
|
||||||
await testLogin();
|
|
||||||
break;
|
|
||||||
case '2':
|
|
||||||
await testRelogin();
|
|
||||||
break;
|
|
||||||
case '3':
|
|
||||||
await testLogout();
|
|
||||||
break;
|
|
||||||
case '4':
|
|
||||||
logger.info('\n=== 运行所有测试 ===\n');
|
|
||||||
await testLogin();
|
|
||||||
logger.info('\n' + '='.repeat(50) + '\n');
|
|
||||||
await testRelogin();
|
|
||||||
logger.info('\n' + '='.repeat(50) + '\n');
|
|
||||||
await testLogout();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.info('❌ 无效选择');
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果直接运行此脚本
|
|
||||||
if (require.main === module) {
|
|
||||||
main().catch(logger.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
testLogin,
|
|
||||||
testRelogin,
|
|
||||||
testLogout
|
|
||||||
};
|
|
||||||
+21
-2
@@ -41,7 +41,7 @@ const ModuleColors = {
|
|||||||
'Server': '\x1b[32m', // 绿色
|
'Server': '\x1b[32m', // 绿色
|
||||||
'API': '\x1b[32m', // 绿色
|
'API': '\x1b[32m', // 绿色
|
||||||
'Start': '\x1b[34m', // 蓝色
|
'Start': '\x1b[34m', // 蓝色
|
||||||
'PixivBackend': '\x1b[35m', // 紫色
|
'PixivCore': '\x1b[35m', // 紫色
|
||||||
'PixivAuth': '\x1b[36m', // 青色
|
'PixivAuth': '\x1b[36m', // 青色
|
||||||
'TaskManager': '\x1b[33m', // 黄色
|
'TaskManager': '\x1b[33m', // 黄色
|
||||||
'ImageCache': '\x1b[92m', // 亮绿色
|
'ImageCache': '\x1b[92m', // 亮绿色
|
||||||
@@ -54,7 +54,9 @@ const ModuleColors = {
|
|||||||
'ErrorHandler': '\x1b[91m', // 亮红色
|
'ErrorHandler': '\x1b[91m', // 亮红色
|
||||||
'FileManager': '\x1b[36m', // 青色
|
'FileManager': '\x1b[36m', // 青色
|
||||||
'ProgressManager': '\x1b[35m', // 紫色
|
'ProgressManager': '\x1b[35m', // 紫色
|
||||||
|
'DownloadRegistry': '\x1b[94m', // 亮蓝色
|
||||||
'WatchlistManager': '\x1b[94m', // 亮蓝色
|
'WatchlistManager': '\x1b[94m', // 亮蓝色
|
||||||
|
'CacheConfigManager': '\x1b[94m', // 亮蓝色
|
||||||
'UpdateRoute': '\x1b[93m', // 亮黄色
|
'UpdateRoute': '\x1b[93m', // 亮黄色
|
||||||
'ArtistService': '\x1b[95m', // 亮紫色
|
'ArtistService': '\x1b[95m', // 亮紫色
|
||||||
'DownloadService': '\x1b[96m', // 亮青色
|
'DownloadService': '\x1b[96m', // 亮青色
|
||||||
@@ -152,7 +154,24 @@ class Logger {
|
|||||||
let formattedMessage = `[${timeStr}] [${levelName}] [${this.module}] ${message}`;
|
let formattedMessage = `[${timeStr}] [${levelName}] [${this.module}] ${message}`;
|
||||||
|
|
||||||
if (data !== null && data !== undefined) {
|
if (data !== null && data !== undefined) {
|
||||||
if (typeof data === 'object') {
|
if (data instanceof Error) {
|
||||||
|
// 特殊处理 Error 对象
|
||||||
|
formattedMessage += `\n Error: ${data.message}`;
|
||||||
|
if (data.stack) {
|
||||||
|
formattedMessage += `\n Stack: ${data.stack}`;
|
||||||
|
}
|
||||||
|
// 如果有其他可枚举属性,也包含进来
|
||||||
|
const errorProps = Object.getOwnPropertyNames(data).filter(prop =>
|
||||||
|
prop !== 'message' && prop !== 'stack' && prop !== 'name'
|
||||||
|
);
|
||||||
|
if (errorProps.length > 0) {
|
||||||
|
const additionalProps = {};
|
||||||
|
errorProps.forEach(prop => {
|
||||||
|
additionalProps[prop] = data[prop];
|
||||||
|
});
|
||||||
|
formattedMessage += `\n Additional: ${JSON.stringify(additionalProps, null, 2)}`;
|
||||||
|
}
|
||||||
|
} else if (typeof data === 'object') {
|
||||||
formattedMessage += ` ${JSON.stringify(data, null, 2)}`;
|
formattedMessage += ` ${JSON.stringify(data, null, 2)}`;
|
||||||
} else {
|
} else {
|
||||||
formattedMessage += ` ${data}`;
|
formattedMessage += ` ${data}`;
|
||||||
|
|||||||
+1
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pixiv-manager",
|
"name": "pixiv-manager",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"description": "Pixiv 下载浏览管理器",
|
"description": "Pixiv 下载浏览管理器",
|
||||||
"main": "backend/start.js",
|
"main": "backend/start.js",
|
||||||
"bin": "backend/start.js",
|
"bin": "backend/start.js",
|
||||||
@@ -15,9 +15,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node backend/start.js",
|
|
||||||
"dev": "node backend/start.js",
|
"dev": "node backend/start.js",
|
||||||
"test": "node backend/test-login.js",
|
|
||||||
"build": "pkg . && node scripts/add-icon.js",
|
"build": "pkg . && node scripts/add-icon.js",
|
||||||
"bp": "npm run build && node scripts/create-portable.js"
|
"bp": "npm run build && node scripts/create-portable.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ set PROXY_PORT=
|
|||||||
:: ========================================
|
:: ========================================
|
||||||
set SERVER_PORT=3000
|
set SERVER_PORT=3000
|
||||||
|
|
||||||
|
:: ========================================
|
||||||
|
:: 日志级别配置 - 可选值: ERROR, WARN, INFO, DEBUG, TRACE
|
||||||
|
:: ERROR: 只显示错误信息
|
||||||
|
:: WARN: 显示警告及以上级别信息
|
||||||
|
:: INFO: 显示一般信息及以上级别信息 (默认)
|
||||||
|
:: DEBUG: 显示调试信息及以上级别信息
|
||||||
|
:: TRACE: 显示所有级别信息 (最详细)
|
||||||
|
:: ========================================
|
||||||
|
set LOG_LEVEL=
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Pixiv Manager 启动中...
|
echo Pixiv Manager 启动中...
|
||||||
echo.
|
echo.
|
||||||
@@ -20,7 +30,8 @@ cd /d "%~dp0"
|
|||||||
|
|
||||||
echo 当前代理端口: %PROXY_PORT%
|
echo 当前代理端口: %PROXY_PORT%
|
||||||
echo 当前服务器端口: %SERVER_PORT%
|
echo 当前服务器端口: %SERVER_PORT%
|
||||||
echo 如需修改端口,请用记事本打开此文件,修改对应的端口号
|
echo 日志级别: %LOG_LEVEL%
|
||||||
|
echo 如需修改端口或日志级别,请用记事本打开此文件,修改对应的配置
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo 启动后端服务器...
|
echo 启动后端服务器...
|
||||||
@@ -29,13 +40,13 @@ echo.
|
|||||||
echo 提示: 按 Ctrl+C 停止服务器
|
echo 提示: 按 Ctrl+C 停止服务器
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: 启动服务器并传递代理端口和服务器端口
|
:: 启动服务器并传递代理端口、服务器端口和日志级别
|
||||||
if "%PROXY_PORT%"=="" (
|
if "%PROXY_PORT%"=="" (
|
||||||
node backend/start.js --server-port=%SERVER_PORT%
|
node backend/start.js --server-port=%SERVER_PORT% --log-level=%LOG_LEVEL%
|
||||||
) else (
|
) else (
|
||||||
node backend/start.js --proxy-port=%PROXY_PORT% --server-port=%SERVER_PORT%
|
node backend/start.js --proxy-port=%PROXY_PORT% --server-port=%SERVER_PORT% --log-level=%LOG_LEVEL%
|
||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo 服务器已停止
|
echo 服务器已停止
|
||||||
pause
|
pause
|
||||||
@@ -8,6 +8,7 @@ import { useUpdateStore } from '@/stores/update'
|
|||||||
import SettingsWidget from '@/components/common/SettingsWidget.vue'
|
import SettingsWidget from '@/components/common/SettingsWidget.vue'
|
||||||
import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue'
|
import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue'
|
||||||
import WatchlistWidget from '@/components/common/WatchlistWidget.vue'
|
import WatchlistWidget from '@/components/common/WatchlistWidget.vue'
|
||||||
|
import RegistryWidget from '@/components/common/RegistryWidget.vue'
|
||||||
import UpdateChecker from '@/components/common/UpdateChecker.vue'
|
import UpdateChecker from '@/components/common/UpdateChecker.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -96,6 +97,9 @@ onMounted(async () => {
|
|||||||
<!-- 设置小组件 - 只在登录时显示 -->
|
<!-- 设置小组件 - 只在登录时显示 -->
|
||||||
<SettingsWidget v-if="isLoggedIn" />
|
<SettingsWidget v-if="isLoggedIn" />
|
||||||
|
|
||||||
|
<!-- 下载注册表管理小组件 - 只在登录时显示 -->
|
||||||
|
<RegistryWidget v-if="isLoggedIn" />
|
||||||
|
|
||||||
<!-- 下载进度小组件 - 只在登录时显示,在下载管理页面隐藏 -->
|
<!-- 下载进度小组件 - 只在登录时显示,在下载管理页面隐藏 -->
|
||||||
<DownloadProgressWidget v-if="showDownloadWidget" />
|
<DownloadProgressWidget v-if="showDownloadWidget" />
|
||||||
|
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ export const actionIcons = {
|
|||||||
'empty2': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
|
'empty2': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
|
||||||
'refresh': 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z',
|
'refresh': 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z',
|
||||||
'cleanup': 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
|
'cleanup': 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
|
||||||
|
'down':"M18.414 10.656a2 2 0 0 0-2.828 0L14 12.242V5a2 2 0 0 0-4 0v7.242l-1.586-1.586a2 2 0 1 0-2.828 2.828L12 19.898l6.414-6.414a2 2 0 0 0 0-2.828"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,557 @@
|
|||||||
|
<template>
|
||||||
|
<div class="registry-widget">
|
||||||
|
<!-- 注册表管理按钮 -->
|
||||||
|
<button @click="togglePanel" class="registry-toggle" :class="{ active: isOpen }" title="下载注册表管理">
|
||||||
|
<SvgIcon name="down" class="registry-icon" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 注册表管理面板 -->
|
||||||
|
<div v-if="isOpen" class="registry-panel">
|
||||||
|
<div class="registry-header">
|
||||||
|
<h3>下载注册表管理</h3>
|
||||||
|
<button @click="togglePanel" class="close-btn" title="关闭">
|
||||||
|
<SvgIcon name="close" class="close-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="registry-content">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
<LoadingSpinner text="处理中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<div v-else-if="error" class="error">
|
||||||
|
<ErrorMessage :error="error" @dismiss="clearError" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成功提示 -->
|
||||||
|
<div v-if="successMessage" class="success-message">
|
||||||
|
<div class="success-content">
|
||||||
|
<SvgIcon name="success" class="success-icon" />
|
||||||
|
<span>{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="registry-stats">
|
||||||
|
<h4>统计信息</h4>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">作者数量:</span>
|
||||||
|
<span class="stat-value">{{ stats?.totalArtists || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">作品数量:</span>
|
||||||
|
<span class="stat-value">{{ stats?.totalArtworks || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">最后更新:</span>
|
||||||
|
<span class="stat-value">{{ formatDate(stats?.lastUpdated) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置选项 -->
|
||||||
|
<div class="registry-config">
|
||||||
|
<h4>配置选项</h4>
|
||||||
|
<div class="config-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="radio" v-model="detectionMethod" value="registry" @change="updateDetectionMethod" />
|
||||||
|
使用注册表检测
|
||||||
|
</label>
|
||||||
|
<small>优先使用JSON注册表检测作品是否已下载</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="radio" v-model="detectionMethod" value="scan" @change="updateDetectionMethod" />
|
||||||
|
使用扫盘检测
|
||||||
|
</label>
|
||||||
|
<small>直接扫描文件系统检测作品是否已下载</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="radio" v-model="detectionMethod" value="hybrid" @change="updateDetectionMethod" />
|
||||||
|
混合检测模式
|
||||||
|
</label>
|
||||||
|
<small>优先使用注册表检测,失败时回退到扫盘检测</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="registry-actions">
|
||||||
|
<h4>管理操作</h4>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button @click="refreshStats" class="btn btn-secondary" :disabled="loading">
|
||||||
|
<SvgIcon name="refresh" class="btn-icon" />
|
||||||
|
刷新统计
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="exportRegistry" class="btn btn-primary" :disabled="loading">
|
||||||
|
<SvgIcon name="download" class="btn-icon" />
|
||||||
|
导出注册表
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label class="btn btn-primary" :class="{ disabled: loading }">
|
||||||
|
<SvgIcon name="upload" class="btn-icon" />
|
||||||
|
导入注册表
|
||||||
|
<input type="file" @change="handleFileImport" accept=".json" style="display: none;" :disabled="loading" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button @click="rebuildRegistry" class="btn btn-warning" :disabled="loading">
|
||||||
|
<SvgIcon name="rebuild" class="btn-icon" />
|
||||||
|
同步文件系统
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="cleanupRegistry" class="btn btn-danger" :disabled="loading">
|
||||||
|
<SvgIcon name="trash" class="btn-icon" />
|
||||||
|
清理注册表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useRegistryStore } from '@/stores/registry';
|
||||||
|
import SvgIcon from './SvgIcon.vue';
|
||||||
|
import LoadingSpinner from './LoadingSpinner.vue';
|
||||||
|
import ErrorMessage from './ErrorMessage.vue';
|
||||||
|
|
||||||
|
const registryStore = useRegistryStore();
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const successMessage = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 从store中获取响应式数据
|
||||||
|
const { stats, loading, error, config } = storeToRefs(registryStore);
|
||||||
|
|
||||||
|
// 检测方法选择 - 不设置默认值,等待从后端配置初始化
|
||||||
|
const detectionMethod = ref<'registry' | 'scan' | 'hybrid'>();
|
||||||
|
|
||||||
|
// 切换面板显示
|
||||||
|
const togglePanel = () => {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
if (isOpen.value) {
|
||||||
|
refreshStats();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新统计信息
|
||||||
|
const refreshStats = async () => {
|
||||||
|
await registryStore.fetchStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出注册表
|
||||||
|
const exportRegistry = async () => {
|
||||||
|
const result = await registryStore.exportRegistry();
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess('注册表导出成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理文件导入
|
||||||
|
const handleFileImport = async (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const file = target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const result = await registryStore.importRegistry(file);
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess(`注册表导入成功,处理了 ${result.data?.imported || 0} 条记录`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空文件输入
|
||||||
|
target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重建注册表
|
||||||
|
const rebuildRegistry = async () => {
|
||||||
|
if (!confirm('确定要同步文件系统到注册表吗?这将扫描整个下载目录并添加新发现的作品,可能需要一些时间。')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registryStore.rebuildRegistry();
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess(`文件系统同步完成,新增 ${result.data?.addedArtworks || 0} 个作品,跳过 ${result.data?.skippedArtworks || 0} 个已存在作品`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理注册表
|
||||||
|
const cleanupRegistry = async () => {
|
||||||
|
if (!confirm('确定要清理注册表吗?这将移除不存在的文件记录。')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registryStore.cleanupRegistry();
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess(`注册表清理完成,移除了 ${result.data?.removedArtworks || 0} 条无效记录`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新检测方法配置
|
||||||
|
const updateDetectionMethod = async () => {
|
||||||
|
let useRegistryCheck = false;
|
||||||
|
let fallbackToScan = false;
|
||||||
|
|
||||||
|
switch (detectionMethod.value) {
|
||||||
|
case 'registry':
|
||||||
|
useRegistryCheck = true;
|
||||||
|
fallbackToScan = false;
|
||||||
|
break;
|
||||||
|
case 'scan':
|
||||||
|
useRegistryCheck = false;
|
||||||
|
fallbackToScan = false;
|
||||||
|
break;
|
||||||
|
case 'hybrid':
|
||||||
|
useRegistryCheck = true;
|
||||||
|
fallbackToScan = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registryStore.updateConfig({
|
||||||
|
useRegistryCheck,
|
||||||
|
fallbackToScan
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess('配置更新成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新配置(保留原方法以防其他地方调用)
|
||||||
|
const updateConfig = async () => {
|
||||||
|
const result = await registryStore.updateConfig({
|
||||||
|
useRegistryCheck: config.value.useRegistryCheck,
|
||||||
|
fallbackToScan: config.value.fallbackToScan
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showSuccess('配置更新成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除错误
|
||||||
|
const clearError = () => {
|
||||||
|
registryStore.clearError();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
const showSuccess = (message: string) => {
|
||||||
|
successMessage.value = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
successMessage.value = null;
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString?: string): string => {
|
||||||
|
if (!dateString) return '未知';
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化检测方法
|
||||||
|
const initDetectionMethod = () => {
|
||||||
|
if (config.value.useRegistryCheck && config.value.fallbackToScan) {
|
||||||
|
detectionMethod.value = 'hybrid';
|
||||||
|
} else if (config.value.useRegistryCheck && !config.value.fallbackToScan) {
|
||||||
|
detectionMethod.value = 'registry';
|
||||||
|
} else {
|
||||||
|
detectionMethod.value = 'scan';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
// 从后端获取配置并初始化检测方法
|
||||||
|
try {
|
||||||
|
await registryStore.fetchConfig();
|
||||||
|
initDetectionMethod();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置失败:', error);
|
||||||
|
// 如果获取配置失败,使用默认值
|
||||||
|
detectionMethod.value = 'hybrid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时加载统计数据
|
||||||
|
refreshStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听配置变化,自动更新检测方法
|
||||||
|
watch(config, () => {
|
||||||
|
initDetectionMethod();
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.registry-widget {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-toggle {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3b82f6;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-toggle:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-toggle.active {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-icon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4rem;
|
||||||
|
left: 0;
|
||||||
|
width: 400px;
|
||||||
|
max-height: 600px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
color: #6b7280;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .error {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #dcfce7;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-stats, .registry-config, .registry-actions {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-stats h4, .registry-config h4, .registry-actions h4 {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled, .btn.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover:not(:disabled) {
|
||||||
|
background: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.registry-panel {
|
||||||
|
width: calc(100vw - 2rem);
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -213,6 +213,40 @@ class DownloadService {
|
|||||||
eventSource.close();
|
eventSource.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取下载注册表统计信息
|
||||||
|
*/
|
||||||
|
async getRegistryStats() {
|
||||||
|
return apiService.get('/api/download/registry/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出下载注册表
|
||||||
|
*/
|
||||||
|
async exportRegistry() {
|
||||||
|
return apiService.get('/api/download/registry/export');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入下载注册表
|
||||||
|
*/
|
||||||
|
async importRegistry(registryData: any) {
|
||||||
|
return apiService.post('/api/download/registry/import', { registryData });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重建下载注册表
|
||||||
|
*/
|
||||||
|
async rebuildRegistry() {
|
||||||
|
return apiService.post('/api/download/registry/rebuild');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理下载注册表
|
||||||
|
*/
|
||||||
|
async cleanupRegistry() {
|
||||||
|
return apiService.post('/api/download/registry/cleanup');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DownloadService();
|
export default new DownloadService();
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import downloadService from '@/services/download';
|
||||||
|
import { getApiBaseUrl } from '@/services/api';
|
||||||
|
|
||||||
|
export interface RegistryStats {
|
||||||
|
totalArtists: number;
|
||||||
|
totalArtworks: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryConfig {
|
||||||
|
useRegistryCheck: boolean;
|
||||||
|
fallbackToScan: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRegistryStore = defineStore('registry', () => {
|
||||||
|
// 状态
|
||||||
|
const stats = ref<RegistryStats | null>(null);
|
||||||
|
const config = ref<RegistryConfig>({
|
||||||
|
useRegistryCheck: true,
|
||||||
|
fallbackToScan: false
|
||||||
|
});
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 获取注册表统计信息
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await downloadService.getRegistryStats();
|
||||||
|
if (response.success) {
|
||||||
|
// 映射API响应数据到组件期望的格式
|
||||||
|
stats.value = {
|
||||||
|
totalArtists: response.data.artistCount || 0,
|
||||||
|
totalArtworks: response.data.artworkCount || 0,
|
||||||
|
lastUpdated: response.data.updated_at || response.data.created_at || ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '获取统计信息失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '获取统计信息失败';
|
||||||
|
console.error('获取注册表统计信息失败:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出注册表
|
||||||
|
const exportRegistry = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await downloadService.exportRegistry();
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const blob = new Blob([JSON.stringify(response, null, 2)], {
|
||||||
|
type: 'application/json'
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `download-registry-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '导出注册表失败';
|
||||||
|
console.error('导出注册表失败:', err);
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入注册表
|
||||||
|
const importRegistry = async (file: File) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const text = await file.text();
|
||||||
|
const registryData = JSON.parse(text);
|
||||||
|
|
||||||
|
const response = await downloadService.importRegistry(registryData);
|
||||||
|
if (response.success) {
|
||||||
|
// 刷新统计信息
|
||||||
|
await fetchStats();
|
||||||
|
return { success: true, data: response.data };
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '导入注册表失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '导入注册表失败';
|
||||||
|
console.error('导入注册表失败:', err);
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重建注册表
|
||||||
|
const rebuildRegistry = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await downloadService.rebuildRegistry();
|
||||||
|
if (response.success) {
|
||||||
|
// 刷新统计信息
|
||||||
|
await fetchStats();
|
||||||
|
return { success: true, data: response.data };
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '重建注册表失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '重建注册表失败';
|
||||||
|
console.error('重建注册表失败:', err);
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理注册表
|
||||||
|
const cleanupRegistry = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await downloadService.cleanupRegistry();
|
||||||
|
if (response.success) {
|
||||||
|
// 刷新统计信息
|
||||||
|
await fetchStats();
|
||||||
|
return { success: true, data: response.data };
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '清理注册表失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '清理注册表失败';
|
||||||
|
console.error('清理注册表失败:', err);
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiBaseUrl()}/api/download/registry/config`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
config.value = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || '获取配置失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '获取配置失败';
|
||||||
|
console.error('获取配置失败:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
const updateConfig = async (newConfig: Partial<RegistryConfig>) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await fetch(`${getApiBaseUrl()}/api/download/registry/config`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newConfig),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
config.value = { ...config.value, ...result.data };
|
||||||
|
return { success: true, data: result.data };
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || '更新配置失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '更新配置失败';
|
||||||
|
console.error('更新配置失败:', err);
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除错误
|
||||||
|
const clearError = () => {
|
||||||
|
error.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
stats,
|
||||||
|
config,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
fetchStats,
|
||||||
|
exportRegistry,
|
||||||
|
importRegistry,
|
||||||
|
rebuildRegistry,
|
||||||
|
cleanupRegistry,
|
||||||
|
fetchConfig,
|
||||||
|
updateConfig,
|
||||||
|
clearError
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { getApiBaseUrl } from '@/services/api'
|
||||||
|
|
||||||
export interface RepositoryConfig {
|
export interface RepositoryConfig {
|
||||||
downloadDir: string
|
downloadDir: string
|
||||||
@@ -183,7 +184,16 @@ export const useRepositoryStore = defineStore('repository', () => {
|
|||||||
|
|
||||||
// 检查作品是否已下载
|
// 检查作品是否已下载
|
||||||
const checkArtworkDownloaded = async (artworkId: number) => {
|
const checkArtworkDownloaded = async (artworkId: number) => {
|
||||||
return await apiCall(`/check-downloaded/${artworkId}`)
|
// 使用新的下载检测API,支持注册表检测和回退机制
|
||||||
|
const response = await fetch(`${getApiBaseUrl()}/api/download/check/${artworkId}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const result = await response.json()
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'API调用失败')
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查目录是否存在
|
// 检查目录是否存在
|
||||||
@@ -223,4 +233,4 @@ export const useRepositoryStore = defineStore('repository', () => {
|
|||||||
checkDirectoryExists,
|
checkDirectoryExists,
|
||||||
migrateFromOldToNew,
|
migrateFromOldToNew,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -258,7 +258,7 @@ const handleDownload = async () => {
|
|||||||
// 清理下载状态
|
// 清理下载状态
|
||||||
downloading.value = true;
|
downloading.value = true;
|
||||||
|
|
||||||
// 如果已经下载过,则强制重新下载(跳过现有文件检查)
|
// 如果已经下载过,则强制重新下载(不跳过现有文件)
|
||||||
const skipExisting = !isDownloaded.value;
|
const skipExisting = !isDownloaded.value;
|
||||||
const response = await downloadService.downloadArtwork(artwork.value.id, {
|
const response = await downloadService.downloadArtwork(artwork.value.id, {
|
||||||
skipExisting
|
skipExisting
|
||||||
|
|||||||
Reference in New Issue
Block a user