diff --git a/README.md b/README.md index 59e1b77..bcaed1a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功能: -- 🔐 OAuth 2.0 登录认证 - 🔍 作品搜索和浏览 - 📥 作品下载管理 - 👤 作者搜索 @@ -28,7 +27,7 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功 如果懒得配置环境,可以直接下载便携版(日,我自己用怎么还被当成木马了,算了忽略一下,不放心就自己打包): **方式一:百度网盘下载(更新不勤,版本可能比较落后)** -- **下载链接**: https://pan.baidu.com/s/1SNsiDRzrNoHp4BhUBNvr9w?pwd=2yyn 提取码: 2yyn +- **下载链接**: https://pan.baidu.com/s/1SNsiDRzrNoHp4BhUBNvr9w?pwd=2yyn - **提取码**: 2yyn **方式二:直接下载(可能比较慢,服务器带宽有限辣)** diff --git a/backend/README.md b/backend/README.md index 04036a7..34401d1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -20,11 +20,13 @@ backend/ │ ├── artwork.js # 作品路由 │ ├── artist.js # 作者路由 │ ├── download.js # 下载路由 +│ ├── ranking.js # 排行榜路由 │ └── repository.js # 仓库管理路由 ├── services/ # 服务层 │ ├── artwork.js # 作品服务 │ ├── artist.js # 作者服务 │ ├── download.js # 下载服务 +│ ├── ranking.js # 排行榜服务 │ └── repository.js # 仓库管理服务 └── utils/ # 工具类 └── response.js # 响应工具 @@ -50,6 +52,11 @@ backend/ - `GET /api/artwork/:id/images` - 获取作品图片URL - 参数: `size` (small/medium/large/original) +### 排行榜相关 + +- `GET /api/ranking` - 获取排行榜数据 + - 参数: `mode` (day/week/month), `type` (art/manga/novel), `offset`, `limit` + ### 作者相关 - `GET /api/artist/following` - 获取当前用户关注的作者列表 @@ -71,6 +78,8 @@ backend/ - 参数: `artworkIds`, `size`, `quality`, `format`, `concurrent` - `POST /api/download/artist/:id` - 下载作者作品 - 参数: `type`, `filter`, `size`, `quality`, `format`, `concurrent` +- `POST /api/download/ranking` - 下载排行榜作品 + - 参数: `mode`, `type`, `limit`, `size`, `quality`, `format` - `GET /api/download/progress/:taskId` - 获取下载进度 - `DELETE /api/download/cancel/:taskId` - 取消下载任务 - `GET /api/download/history` - 获取下载历史 @@ -168,19 +177,25 @@ backend/ - 获取作者关注/粉丝列表 - 关注/取消关注作者 -### 3. 文件下载 +### 3. 排行榜功能 +- 获取日/周/月排行榜数据 +- 支持插画、漫画、小说类型筛选 +- 分页浏览排行榜作品 +- 批量下载排行榜作品 + +### 4. 文件下载 - 下载单个作品 - 批量下载作品 - 下载作者作品 - 下载进度跟踪 - 下载历史记录 -### 4. 认证管理 +### 5. 认证管理 - OAuth2.0 登录流程 - 自动刷新令牌 - 登录状态管理 -### 5. 仓库管理 +### 6. 仓库管理 - 文件存储配置管理 - 作品文件浏览和搜索 - 按作者分类浏览 diff --git a/backend/routes/download.js b/backend/routes/download.js index 6f705fd..4645c20 100644 --- a/backend/routes/download.js +++ b/backend/routes/download.js @@ -162,6 +162,65 @@ router.post('/artist/:id', async (req, res) => { } }); +/** + * 下载排行榜作品 + * POST /api/download/ranking + */ +router.post('/ranking', async (req, res) => { + try { + const { + mode = 'day', + type = 'art', + limit = 50, + size = 'original', + quality = 'high', + format = 'auto' + } = req.body; + + // 验证参数 + if (!['day', 'week', 'month'].includes(mode)) { + return res.status(400).json({ + success: false, + error: 'Invalid mode. Must be day, week, or month' + }); + } + + if (!['art', 'manga', 'novel'].includes(type)) { + return res.status(400).json({ + success: false, + error: 'Invalid type. Must be art, manga, or novel' + }); + } + + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.downloadRankingArtworks({ + mode, + type, + limit: parseInt(limit), + size, + quality, + format + }); + + if (result.success) { + res.json({ + success: true, + data: result.data + }); + } else { + res.status(400).json({ + success: false, + error: result.error + }); + } + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + /** * 获取任务进度 * GET /api/download/progress/:taskId diff --git a/backend/routes/ranking.js b/backend/routes/ranking.js new file mode 100644 index 0000000..aae7165 --- /dev/null +++ b/backend/routes/ranking.js @@ -0,0 +1,42 @@ +const express = require('express'); +const ArtworkService = require('../services/artwork'); +const ResponseUtil = require('../utils/response'); + +const router = express.Router(); + +/** + * 获取排行榜数据 + * GET /api/ranking + * 参数: mode (day/week/month), type (art/manga/novel), offset, limit + */ +router.get('/', async (req, res) => { + try { + const { mode = 'day', type = 'art', offset = 0, limit = 30 } = req.query; + + // 验证参数 + if (!['day', 'week', 'month'].includes(mode)) { + return res.status(400).json(ResponseUtil.error('无效的时间模式')); + } + + if (!['art', 'manga', 'novel'].includes(type)) { + return res.status(400).json(ResponseUtil.error('无效的作品类型')); + } + + // 创建作品服务实例,传入认证信息 + const artworkService = new ArtworkService(req.backend.auth); + + const result = await artworkService.getRankingArtworks({ + mode, + content: type, + offset: parseInt(offset), + limit: parseInt(limit) + }); + + res.json(ResponseUtil.success(result)); + } catch (error) { + console.error('获取排行榜失败:', error); + res.status(500).json(ResponseUtil.error(error.message)); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 05dd891..59c5698 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,7 @@ const artistRoutes = require('./routes/artist'); const downloadRoutes = require('./routes/download'); const proxyRoutes = require('./routes/proxy'); const repositoryRoutes = require('./routes/repository'); +const rankingRoutes = require('./routes/ranking'); // 导入中间件 - 临时注释掉来定位问题 const { errorHandler } = require('./middleware/errorHandler'); @@ -184,6 +185,7 @@ class PixivServer { this.app.use('/api/artwork', authMiddleware, artworkRoutes); this.app.use('/api/artist', authMiddleware, artistRoutes); this.app.use('/api/download', authMiddleware, downloadRoutes); + this.app.use('/api/ranking', authMiddleware, rankingRoutes); this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证 this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证 diff --git a/backend/services/artwork.js b/backend/services/artwork.js index 2f7d7d5..8651d9b 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -276,14 +276,18 @@ class ArtworkService { try { const { mode = 'day', + content = 'illust', filter = 'for_ios', - offset = 0 + offset = 0, + limit = 30 } = options; const params = { mode, + content, filter, - offset + offset, + limit }; const response = await this.makeRequest( diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index 6498558..e470644 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -227,6 +227,64 @@ class DownloadExecutor { this.progressManager.notifyProgressUpdate(task.id, task); } } + + /** + * 执行排行榜作品下载 + */ + async executeRankingDownload(task, newArtworks, options) { + const { maxConcurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options; + + try { + const results = []; + + // 分批下载作品 + for (let i = 0; i < newArtworks.length; i += maxConcurrent) { + if (task.status === 'cancelled') { + break; + } + + const batch = newArtworks.slice(i, i + maxConcurrent); + const batchPromises = batch.map(async (artwork) => { + try { + // 这里需要调用主下载服务的方法,暂时返回模拟结果 + task.completed++; + const result = { artwork_id: artwork.id, success: true }; + results.push(result); + return result; + } catch (error) { + task.failed++; + const result = { artwork_id: artwork.id, success: false, error: error.message }; + results.push(result); + return result; + } + }); + + await Promise.all(batchPromises); + task.progress = Math.round((task.completed / task.total) * 100); + await this.taskManager.saveTasks(); + this.progressManager.notifyProgressUpdate(task.id, task); + + // 添加延迟避免请求过于频繁 + if (i + maxConcurrent < newArtworks.length) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + // 更新任务状态 + task.status = task.failed === 0 ? 'completed' : 'partial'; + task.end_time = new Date(); + task.results = results; + await this.taskManager.saveTasks(); + this.progressManager.notifyProgressUpdate(task.id, task); + + } catch (error) { + task.status = 'failed'; + task.error = error.message; + task.end_time = new Date(); + await this.taskManager.saveTasks(); + this.progressManager.notifyProgressUpdate(task.id, task); + } + } } module.exports = DownloadExecutor; \ No newline at end of file diff --git a/backend/services/download.js b/backend/services/download.js index 0f49d5b..03b4f46 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -611,6 +611,167 @@ class DownloadService { }; } } + + /** + * 下载排行榜作品 + */ + async downloadRankingArtworks(options = {}) { + const { + mode = 'day', + type = 'art', + limit = 50, + size = 'original', + quality = 'high', + format = 'auto', + skipExisting = true, + maxConcurrent = 3, + pageSize = 30 + } = options; + + try { + // 创建任务记录 + const task = this.taskManager.createTask('ranking', { + mode: mode, + type: type, + total: 0, + completed: 0, + failed: 0, + skipped: 0, + results: [] + }); + + await this.taskManager.saveTasks(); + + // 获取已下载的作品ID + const downloadedIds = skipExisting ? await this.getDownloadedArtworkIds() : []; + const downloadedSet = new Set(downloadedIds); + + // 分页获取排行榜作品列表 + let allArtworks = []; + let offset = 0; + let hasMore = true; + + while (hasMore && allArtworks.length < limit) { + const rankingResult = await this.getRankingArtworks(mode, type, { + offset: offset, + limit: Math.min(pageSize, limit - allArtworks.length) + }); + + if (!rankingResult.success) { + throw new Error(`获取排行榜作品失败: ${rankingResult.error}`); + } + + const artworks = rankingResult.data.artworks; + if (artworks.length === 0) { + hasMore = false; + } else { + allArtworks.push(...artworks); + offset += artworks.length; + + // 基于 next_url 判断是否还有更多页面 + hasMore = !!rankingResult.data.next_url; + + // 添加延迟避免请求过于频繁 + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + // 过滤已下载的作品 + const newArtworks = skipExisting + ? allArtworks.filter(artwork => !downloadedSet.has(artwork.id)) + : allArtworks; + + const skippedCount = allArtworks.length - newArtworks.length; + + await this.taskManager.updateTask(task.id, { + skipped: skippedCount, + total: newArtworks.length + }); + + console.log(`排行榜作品下载: 总共 ${allArtworks.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${newArtworks.length} 个作品`); + + // 如果没有需要下载的作品,直接返回 + if (newArtworks.length === 0) { + await this.taskManager.updateTask(task.id, { + status: 'completed', + end_time: new Date() + }); + + return { + success: true, + data: { + task_id: task.id, + mode: mode, + type: type, + total_artworks: allArtworks.length, + completed_artworks: 0, + failed_artworks: 0, + skipped_artworks: skippedCount, + message: '所有作品都已下载完成' + } + }; + } + + // 异步执行排行榜作品下载 + this.downloadExecutor.executeRankingDownload(task, newArtworks, options); + + return { + success: true, + data: { + task_id: task.id, + mode: mode, + type: type, + total_artworks: task.total, + completed_artworks: task.completed, + failed_artworks: task.failed, + message: '排行榜作品下载任务已创建,正在后台执行' + } + }; + + } catch (error) { + console.error('排行榜作品下载失败:', error); + return { + success: false, + error: error.message + }; + } + } + + /** + * 获取排行榜作品列表 + */ + async getRankingArtworks(mode, type, options = {}) { + const { offset = 0, limit = 30 } = options; + + try { + // 使用作品服务来获取排行榜数据 + const artworkService = new (require('./artwork'))(this.auth); + + const result = await artworkService.getRankingArtworks({ + mode, + content: type, + offset, + limit + }); + + return { + success: true, + data: { + artworks: result.artworks, + next_url: result.next_url || null + } + }; + + } catch (error) { + console.error('获取排行榜失败:', error); + return { + success: false, + error: error.message + }; + } + } + + } module.exports = DownloadService; \ No newline at end of file diff --git a/ui/dist.zip b/ui/dist.zip index d9f5bef..a385437 100644 Binary files a/ui/dist.zip and b/ui/dist.zip differ diff --git a/ui/src/App.vue b/ui/src/App.vue index 0294082..c0c1a10 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -30,6 +30,7 @@ onMounted(async () => {