bug修复和架构完善
This commit is contained in:
+7
-1
@@ -199,12 +199,18 @@ class PixivAuth {
|
||||
|
||||
this.accessToken = tokenData.access_token;
|
||||
this.refreshToken = tokenData.refresh_token;
|
||||
|
||||
// 如果响应中包含用户信息,则保存
|
||||
if (tokenData.user) {
|
||||
this.user = tokenData.user;
|
||||
}
|
||||
|
||||
console.log('刷新访问令牌成功');
|
||||
return {
|
||||
success: true,
|
||||
access_token: tokenData.access_token,
|
||||
refresh_token: tokenData.refresh_token
|
||||
refresh_token: tokenData.refresh_token,
|
||||
user: tokenData.user
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const Fse = require('fs-extra');
|
||||
const Path = require('path');
|
||||
const PixivAuth = require('./auth');
|
||||
const DownloadService = require('./services/download');
|
||||
|
||||
// 配置文件路径
|
||||
const CONFIG_FILE_DIR = require('appdata-path').getAppDataPath('pxder');
|
||||
@@ -24,6 +25,7 @@ class PixivBackend {
|
||||
this.config = null;
|
||||
this.auth = null;
|
||||
this.isLoggedIn = false;
|
||||
this.downloadService = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +41,10 @@ class PixivBackend {
|
||||
// 创建认证实例,传入代理配置
|
||||
this.auth = new PixivAuth(this.config.proxy);
|
||||
|
||||
// 创建下载服务实例
|
||||
this.downloadService = new DownloadService(this.auth);
|
||||
await this.downloadService.init();
|
||||
|
||||
// 检查登录状态
|
||||
if (this.config.refresh_token) {
|
||||
console.log('检测到已保存的登录信息,正在验证...');
|
||||
@@ -161,6 +167,12 @@ class PixivBackend {
|
||||
// 更新配置
|
||||
this.config.access_token = result.access_token;
|
||||
this.config.refresh_token = result.refresh_token;
|
||||
|
||||
// 如果刷新令牌响应中包含用户信息,则更新
|
||||
if (result.user) {
|
||||
this.config.user = result.user;
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
|
||||
this.isLoggedIn = true;
|
||||
@@ -253,6 +265,13 @@ class PixivBackend {
|
||||
getAuth() {
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载服务实例
|
||||
*/
|
||||
getDownloadService() {
|
||||
return this.downloadService;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PixivBackend;
|
||||
+45
-12
@@ -3,22 +3,18 @@ const router = express.Router();
|
||||
const ArtistService = require('../services/artist');
|
||||
|
||||
/**
|
||||
* 获取作者信息
|
||||
* GET /api/artist/:id
|
||||
* 获取当前用户关注的作者列表
|
||||
* GET /api/artist/following
|
||||
*/
|
||||
router.get('/:id', async (req, res) => {
|
||||
router.get('/following', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(parseInt(id))) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid artist ID'
|
||||
});
|
||||
}
|
||||
const { offset = 0, limit = 30 } = req.query;
|
||||
|
||||
const artistService = new ArtistService(req.backend.getAuth());
|
||||
const result = await artistService.getArtistInfo(parseInt(id));
|
||||
const result = await artistService.getFollowingArtists({
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
@@ -222,4 +218,41 @@ router.post('/:id/follow', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取作者信息
|
||||
* GET /api/artist/:id
|
||||
*/
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(parseInt(id))) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid artist ID'
|
||||
});
|
||||
}
|
||||
|
||||
const artistService = new ArtistService(req.backend.getAuth());
|
||||
const result = await artistService.getArtistInfo(parseInt(id));
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+151
-49
@@ -22,7 +22,7 @@ router.post('/artwork/:id', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = new DownloadService(req.backend.getAuth());
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.downloadArtwork(parseInt(id), {
|
||||
size,
|
||||
quality,
|
||||
@@ -76,7 +76,7 @@ router.post('/artworks', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = new DownloadService(req.backend.getAuth());
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.downloadMultipleArtworks(artworkIds, {
|
||||
size,
|
||||
quality,
|
||||
@@ -112,12 +112,10 @@ router.post('/artist/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
type = 'art',
|
||||
filter = 'for_ios',
|
||||
limit = 50,
|
||||
size = 'original',
|
||||
quality = 'high',
|
||||
format = 'auto',
|
||||
limit = 50,
|
||||
concurrent = 3
|
||||
format = 'auto'
|
||||
} = req.body;
|
||||
|
||||
if (!id || isNaN(parseInt(id))) {
|
||||
@@ -127,15 +125,13 @@ router.post('/artist/:id', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = new DownloadService(req.backend.getAuth());
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.downloadArtistArtworks(parseInt(id), {
|
||||
type,
|
||||
filter,
|
||||
limit: parseInt(limit),
|
||||
size,
|
||||
quality,
|
||||
format,
|
||||
limit: parseInt(limit),
|
||||
concurrent: parseInt(concurrent)
|
||||
format
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
@@ -158,34 +154,48 @@ router.post('/artist/:id', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取下载进度
|
||||
* 获取任务进度
|
||||
* GET /api/download/progress/:taskId
|
||||
*/
|
||||
router.get('/progress/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const progress = downloadService.getTaskProgress(taskId);
|
||||
|
||||
if (!progress) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Task ID is required'
|
||||
error: 'Task not found'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = new DownloadService(req.backend.getAuth());
|
||||
const result = await downloadService.getDownloadProgress(taskId);
|
||||
res.json({
|
||||
success: true,
|
||||
data: progress
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取所有任务
|
||||
* GET /api/download/tasks
|
||||
*/
|
||||
router.get('/tasks', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const tasks = downloadService.getAllTasks();
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: tasks
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
@@ -196,26 +206,19 @@ router.get('/progress/:taskId', async (req, res) => {
|
||||
|
||||
/**
|
||||
* 取消下载任务
|
||||
* DELETE /api/download/cancel/:taskId
|
||||
* POST /api/download/cancel/:taskId
|
||||
*/
|
||||
router.delete('/cancel/:taskId', async (req, res) => {
|
||||
router.post('/cancel/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Task ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = new DownloadService(req.backend.getAuth());
|
||||
const result = await downloadService.cancelDownload(taskId);
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.cancelTask(taskId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Download task cancelled successfully'
|
||||
message: 'Task cancelled successfully'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
@@ -237,21 +240,120 @@ router.delete('/cancel/:taskId', async (req, res) => {
|
||||
*/
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
offset = 0,
|
||||
limit = 20
|
||||
} = req.query;
|
||||
const { limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const downloadService = new DownloadService(req.backend.getAuth());
|
||||
const result = await downloadService.getDownloadHistory({
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(limit)
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const history = downloadService.getDownloadHistory(parseInt(limit), parseInt(offset));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: history
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取下载的文件列表
|
||||
* GET /api/download/files
|
||||
*/
|
||||
router.get('/files', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const files = await downloadService.getDownloadedFiles();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: files
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 检查作品是否已下载
|
||||
* GET /api/download/check/:artworkId
|
||||
*/
|
||||
router.get('/check/:artworkId', async (req, res) => {
|
||||
try {
|
||||
const { artworkId } = req.params;
|
||||
|
||||
if (!artworkId || isNaN(parseInt(artworkId))) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid artwork ID'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const isDownloaded = await downloadService.isArtworkDownloaded(parseInt(artworkId));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
artwork_id: parseInt(artworkId),
|
||||
is_downloaded: isDownloaded
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取已下载的作品ID列表
|
||||
* GET /api/download/downloaded-ids
|
||||
*/
|
||||
router.get('/downloaded-ids', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const downloadedIds = await downloadService.getDownloadedArtworkIds();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: downloadedIds
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除下载的文件
|
||||
* DELETE /api/download/files
|
||||
*/
|
||||
router.delete('/files', async (req, res) => {
|
||||
try {
|
||||
const { artist, artwork } = req.body;
|
||||
|
||||
if (!artist || !artwork) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Artist and artwork names are required'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.deleteDownloadedFiles(artist, artwork);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
message: 'Files deleted successfully'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
|
||||
/**
|
||||
* 图片代理
|
||||
* GET /api/proxy/image
|
||||
*/
|
||||
router.get('/image', async (req, res) => {
|
||||
try {
|
||||
const { url } = req.query;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Image URL is required'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: decodeURIComponent(url),
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'Referer': 'https://www.pixiv.net/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// 设置响应头
|
||||
res.set({
|
||||
'Content-Type': response.headers['content-type'],
|
||||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
});
|
||||
|
||||
// 流式传输图片数据
|
||||
response.data.pipe(res);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image proxy error:', error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to load image'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+5
-3
@@ -3,11 +3,12 @@ const cors = require('cors');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
|
||||
// 导入路由模块 - 临时注释掉来定位问题
|
||||
// 导入路由模块
|
||||
const authRoutes = require('./routes/auth');
|
||||
const artworkRoutes = require('./routes/artwork');
|
||||
const artistRoutes = require('./routes/artist');
|
||||
const downloadRoutes = require('./routes/download');
|
||||
const proxyRoutes = require('./routes/proxy');
|
||||
|
||||
// 导入中间件 - 临时注释掉来定位问题
|
||||
const { errorHandler } = require('./middleware/errorHandler');
|
||||
@@ -58,7 +59,7 @@ class PixivServer {
|
||||
|
||||
// CORS 中间件
|
||||
this.app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3001',
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
@@ -92,11 +93,12 @@ class PixivServer {
|
||||
});
|
||||
});
|
||||
|
||||
// API 路由 - 临时注释掉来定位问题
|
||||
// API 路由
|
||||
this.app.use('/api/auth', authRoutes);
|
||||
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/proxy', proxyRoutes); // 图片代理,不需要认证
|
||||
|
||||
// 404 处理
|
||||
this.app.use((req, res) => {
|
||||
|
||||
+119
-2
@@ -148,6 +148,110 @@ class ArtistService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户关注的作者列表
|
||||
*/
|
||||
async getFollowingArtists(options = {}) {
|
||||
try {
|
||||
const {
|
||||
offset = 0,
|
||||
limit = 30
|
||||
} = options;
|
||||
|
||||
// 检查认证状态
|
||||
if (!this.auth || !this.auth.accessToken) {
|
||||
return {
|
||||
success: false,
|
||||
error: '未登录或认证已过期'
|
||||
};
|
||||
}
|
||||
|
||||
// 尝试从认证实例获取当前用户ID
|
||||
let currentUserId = this.auth.user?.id;
|
||||
|
||||
// 如果认证实例中没有用户信息,尝试从状态中获取
|
||||
if (!currentUserId) {
|
||||
const status = this.auth.getStatus();
|
||||
currentUserId = status.user?.id;
|
||||
}
|
||||
|
||||
if (!currentUserId) {
|
||||
return {
|
||||
success: false,
|
||||
error: '无法获取当前用户信息,请重新登录'
|
||||
};
|
||||
}
|
||||
|
||||
const params = {
|
||||
user_id: currentUserId,
|
||||
restrict: 'public',
|
||||
offset,
|
||||
limit
|
||||
};
|
||||
|
||||
console.log('获取关注作者列表,参数:', params);
|
||||
|
||||
const response = await this.makeRequest(
|
||||
'GET',
|
||||
`/v1/user/following?${stringify(params)}`
|
||||
);
|
||||
|
||||
// 转换数据格式以匹配前端期望
|
||||
const artists = (response.user_previews || []).map(user => ({
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
account: user.user.account,
|
||||
profile_image_urls: user.user.profile_image_urls,
|
||||
total_illusts: 0, // 这些信息需要通过 /v1/user/detail 获取
|
||||
total_manga: 0,
|
||||
total_followers: 0,
|
||||
is_followed: user.user.is_followed || false
|
||||
}));
|
||||
|
||||
// 为前5个用户获取详细信息(避免API调用过多)
|
||||
const artistsToFetch = artists.slice(0, 5);
|
||||
const detailedArtists = await Promise.all(
|
||||
artistsToFetch.map(async (artist) => {
|
||||
try {
|
||||
const detailResponse = await this.getArtistInfo(artist.id);
|
||||
if (detailResponse.success) {
|
||||
return {
|
||||
...artist,
|
||||
total_illusts: detailResponse.data.total_illusts || 0,
|
||||
total_manga: detailResponse.data.total_manga || 0,
|
||||
total_followers: detailResponse.data.total_followers || 0
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取用户 ${artist.id} 详细信息失败:`, error.message);
|
||||
}
|
||||
return artist;
|
||||
})
|
||||
);
|
||||
|
||||
// 合并详细信息和基本信息
|
||||
const finalArtists = [
|
||||
...detailedArtists,
|
||||
...artists.slice(5) // 其余用户保持基本信息
|
||||
];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
artists: finalArtists,
|
||||
total: finalArtists.length
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取关注作者列表失败:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关注/取消关注作者
|
||||
*/
|
||||
@@ -336,8 +440,21 @@ class ArtistService {
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
try {
|
||||
console.log(`发送API请求: ${method} ${endpoint}`);
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('API请求失败:', {
|
||||
method,
|
||||
endpoint,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
message: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-32
@@ -158,15 +158,50 @@ class ArtworkService {
|
||||
limit = 30
|
||||
} = searchOptions;
|
||||
|
||||
// 验证搜索参数
|
||||
if (!keyword || keyword.trim() === '') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Search keyword is required'
|
||||
};
|
||||
}
|
||||
|
||||
// 映射搜索参数到Pixiv API格式
|
||||
const searchTargetMap = {
|
||||
'all': 'partial_match_for_tags',
|
||||
'art': 'partial_match_for_tags',
|
||||
'manga': 'partial_match_for_tags',
|
||||
'novel': 'partial_match_for_tags'
|
||||
};
|
||||
|
||||
const sortMap = {
|
||||
'date_desc': 'date_desc',
|
||||
'date_asc': 'date_asc',
|
||||
'popular_desc': 'popular_desc'
|
||||
};
|
||||
|
||||
const durationMap = {
|
||||
'all': null, // 不传递duration参数表示全部时间
|
||||
'within_last_day': 'within_last_day',
|
||||
'within_last_week': 'within_last_week',
|
||||
'within_last_month': 'within_last_month'
|
||||
};
|
||||
|
||||
const params = {
|
||||
word: keyword,
|
||||
search_target: type,
|
||||
sort: sort,
|
||||
duration: duration,
|
||||
offset,
|
||||
word: keyword.trim(),
|
||||
search_target: searchTargetMap[type] || 'partial_match_for_tags',
|
||||
sort: sortMap[sort] || 'date_desc',
|
||||
offset: parseInt(offset) || 0,
|
||||
filter: 'for_ios'
|
||||
};
|
||||
|
||||
// 只有当duration不是'all'时才添加duration参数
|
||||
if (durationMap[duration] && durationMap[duration] !== null) {
|
||||
params.duration = durationMap[duration];
|
||||
}
|
||||
|
||||
console.log('Search params:', params);
|
||||
|
||||
const response = await this.makeRequest(
|
||||
'GET',
|
||||
`/v1/search/illust?${stringify(params)}`
|
||||
@@ -175,17 +210,20 @@ class ArtworkService {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
artworks: response.illusts,
|
||||
artworks: response.illusts || [],
|
||||
next_url: response.next_url,
|
||||
search_span_limit: response.search_span_limit,
|
||||
total: response.illusts.length
|
||||
total: response.illusts ? response.illusts.length : 0
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Search error:', error.message);
|
||||
console.error('Search error details:', error.response?.data);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
error: error.message || 'Search failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -275,32 +313,50 @@ class ArtworkService {
|
||||
* 发送API请求
|
||||
*/
|
||||
async makeRequest(method, endpoint, data = null) {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.auth.accessToken}`,
|
||||
'Accept-Language': 'en-us',
|
||||
'App-OS': 'android',
|
||||
'App-OS-Version': '9.0',
|
||||
'App-Version': '5.0.234',
|
||||
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
|
||||
};
|
||||
|
||||
const config = {
|
||||
method,
|
||||
url: `${this.baseURL}${endpoint}`,
|
||||
headers,
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
config.params = data;
|
||||
} else {
|
||||
config.data = data;
|
||||
try {
|
||||
if (!this.auth || !this.auth.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.auth.accessToken}`,
|
||||
'Accept-Language': 'en-us',
|
||||
'App-OS': 'android',
|
||||
'App-OS-Version': '9.0',
|
||||
'App-Version': '5.0.234',
|
||||
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
|
||||
};
|
||||
|
||||
const config = {
|
||||
method,
|
||||
url: `${this.baseURL}${endpoint}`,
|
||||
headers,
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
config.params = data;
|
||||
} else {
|
||||
config.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Making request to: ${config.url}`);
|
||||
console.log('Request config:', { method, endpoint, data });
|
||||
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('API request failed:', {
|
||||
method,
|
||||
endpoint,
|
||||
error: error.message,
|
||||
status: error.response?.status,
|
||||
data: error.response?.data
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+576
-203
@@ -11,34 +11,336 @@ class DownloadService {
|
||||
this.artworkService = new ArtworkService(auth);
|
||||
this.artistService = new ArtistService(auth);
|
||||
this.downloadPath = path.join(__dirname, '../../downloads');
|
||||
this.tasks = new Map(); // 存储下载任务状态
|
||||
this.dataPath = path.join(__dirname, '../../data');
|
||||
this.tasksFile = path.join(this.dataPath, 'download_tasks.json');
|
||||
this.historyFile = path.join(this.dataPath, 'download_history.json');
|
||||
|
||||
// 确保下载目录存在
|
||||
this.ensureDownloadDir();
|
||||
this.tasks = new Map(); // 内存中的任务状态
|
||||
this.history = []; // 下载历史
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保下载目录存在
|
||||
* 初始化服务
|
||||
*/
|
||||
async ensureDownloadDir() {
|
||||
async init() {
|
||||
try {
|
||||
// 确保目录存在
|
||||
await fs.ensureDir(this.downloadPath);
|
||||
console.log('下载目录已创建:', this.downloadPath);
|
||||
await fs.ensureDir(this.dataPath);
|
||||
|
||||
// 加载历史记录
|
||||
await this.loadHistory();
|
||||
|
||||
// 加载任务状态
|
||||
await this.loadTasks();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('下载服务初始化完成');
|
||||
} catch (error) {
|
||||
console.error('创建下载目录失败:', error);
|
||||
console.error('下载服务初始化失败:', error);
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载下载历史
|
||||
*/
|
||||
async loadHistory() {
|
||||
try {
|
||||
if (await fs.pathExists(this.historyFile)) {
|
||||
this.history = await fs.readJson(this.historyFile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载下载历史失败:', error);
|
||||
this.history = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存下载历史
|
||||
*/
|
||||
async saveHistory() {
|
||||
try {
|
||||
await fs.writeJson(this.historyFile, this.history, { spaces: 2 });
|
||||
} catch (error) {
|
||||
console.error('保存下载历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载任务状态
|
||||
*/
|
||||
async loadTasks() {
|
||||
try {
|
||||
if (await fs.pathExists(this.tasksFile)) {
|
||||
const tasksData = await fs.readJson(this.tasksFile);
|
||||
// 只加载未完成的任务
|
||||
for (const [taskId, task] of Object.entries(tasksData)) {
|
||||
if (task.status === 'downloading' || task.status === 'pending') {
|
||||
this.tasks.set(taskId, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存任务状态
|
||||
*/
|
||||
async saveTasks() {
|
||||
try {
|
||||
const tasksData = {};
|
||||
for (const [taskId, task] of this.tasks.entries()) {
|
||||
tasksData[taskId] = task;
|
||||
}
|
||||
await fs.writeJson(this.tasksFile, tasksData, { spaces: 2 });
|
||||
} catch (error) {
|
||||
console.error('保存任务状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务进度
|
||||
*/
|
||||
getTaskProgress(taskId) {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
type: task.type,
|
||||
status: task.status,
|
||||
progress: task.progress,
|
||||
total: task.total,
|
||||
completed: task.completed,
|
||||
failed: task.failed,
|
||||
start_time: task.start_time,
|
||||
end_time: task.end_time,
|
||||
files: task.files || [],
|
||||
error: task.error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有任务
|
||||
*/
|
||||
getAllTasks() {
|
||||
const tasks = [];
|
||||
for (const [taskId, task] of this.tasks.entries()) {
|
||||
tasks.push(this.getTaskProgress(taskId));
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载历史
|
||||
*/
|
||||
getDownloadHistory(limit = 50, offset = 0) {
|
||||
return this.history
|
||||
.sort((a, b) => new Date(b.end_time) - new Date(a.end_time))
|
||||
.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载的文件列表
|
||||
*/
|
||||
async getDownloadedFiles() {
|
||||
try {
|
||||
const files = [];
|
||||
const artists = await fs.readdir(this.downloadPath);
|
||||
|
||||
for (const artist of artists) {
|
||||
const artistPath = path.join(this.downloadPath, artist);
|
||||
const artistStat = await fs.stat(artistPath);
|
||||
|
||||
if (artistStat.isDirectory()) {
|
||||
const artworks = await fs.readdir(artistPath);
|
||||
|
||||
for (const artwork of artworks) {
|
||||
const artworkPath = path.join(artistPath, artwork);
|
||||
const artworkStat = await fs.stat(artworkPath);
|
||||
|
||||
if (artworkStat.isDirectory()) {
|
||||
const artworkFiles = await fs.readdir(artworkPath);
|
||||
const imageFiles = artworkFiles.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
files.push({
|
||||
artist: artist,
|
||||
artwork: artwork,
|
||||
path: artworkPath,
|
||||
files: imageFiles,
|
||||
total_size: await this.getDirectorySize(artworkPath),
|
||||
created_at: artworkStat.birthtime
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
} catch (error) {
|
||||
console.error('获取下载文件列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查作品是否已下载
|
||||
*/
|
||||
async isArtworkDownloaded(artworkId) {
|
||||
try {
|
||||
// 从历史记录中查找
|
||||
const historyItem = this.history.find(item =>
|
||||
item.artwork_id === artworkId && item.status === 'completed'
|
||||
);
|
||||
|
||||
if (historyItem) {
|
||||
// 检查文件是否还存在
|
||||
const exists = await fs.pathExists(historyItem.download_path);
|
||||
if (exists) {
|
||||
const files = await fs.readdir(historyItem.download_path);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
return imageFiles.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('检查作品下载状态失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已下载的作品ID列表
|
||||
*/
|
||||
async getDownloadedArtworkIds() {
|
||||
try {
|
||||
const downloadedIds = new Set();
|
||||
|
||||
// 从历史记录中获取
|
||||
for (const item of this.history) {
|
||||
if (item.artwork_id && item.status === 'completed') {
|
||||
const exists = await fs.pathExists(item.download_path);
|
||||
if (exists) {
|
||||
const files = await fs.readdir(item.download_path);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/i.test(file)
|
||||
);
|
||||
if (imageFiles.length > 0) {
|
||||
downloadedIds.add(item.artwork_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(downloadedIds);
|
||||
} catch (error) {
|
||||
console.error('获取已下载作品ID列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录大小
|
||||
*/
|
||||
async getDirectorySize(dirPath) {
|
||||
try {
|
||||
const files = await fs.readdir(dirPath);
|
||||
let totalSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isFile()) {
|
||||
totalSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除下载的文件
|
||||
*/
|
||||
async deleteDownloadedFiles(artist, artwork) {
|
||||
try {
|
||||
const targetPath = path.join(this.downloadPath, artist, artwork);
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
await fs.remove(targetPath);
|
||||
|
||||
// 从历史记录中移除
|
||||
this.history = this.history.filter(item =>
|
||||
!(item.artist_name === artist && item.artwork_title === artwork)
|
||||
);
|
||||
await this.saveHistory();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '文件不存在' };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下载任务
|
||||
*/
|
||||
async cancelTask(taskId) {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
return { success: false, error: '任务不存在' };
|
||||
}
|
||||
|
||||
if (task.status === 'completed' || task.status === 'failed') {
|
||||
return { success: false, error: '任务已完成,无法取消' };
|
||||
}
|
||||
|
||||
task.status = 'cancelled';
|
||||
task.end_time = new Date();
|
||||
await this.saveTasks();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单个作品
|
||||
*/
|
||||
async downloadArtwork(artworkId, options = {}) {
|
||||
const taskId = uuidv4();
|
||||
const { size = 'original', quality = 'high', format = 'auto' } = options;
|
||||
const { size = 'original', quality = 'high', format = 'auto', skipExisting = true } = options;
|
||||
|
||||
try {
|
||||
// 检查是否已下载
|
||||
if (skipExisting && await this.isArtworkDownloaded(artworkId)) {
|
||||
console.log(`作品 ${artworkId} 已存在,跳过下载`);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task_id: taskId,
|
||||
artwork_id: artworkId,
|
||||
skipped: true,
|
||||
message: '作品已存在,跳过下载'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 创建任务记录
|
||||
this.tasks.set(taskId, {
|
||||
const task = {
|
||||
id: taskId,
|
||||
type: 'artwork',
|
||||
artwork_id: artworkId,
|
||||
@@ -49,8 +351,12 @@ class DownloadService {
|
||||
failed: 0,
|
||||
files: [],
|
||||
start_time: new Date(),
|
||||
end_time: null
|
||||
});
|
||||
end_time: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
this.tasks.set(taskId, task);
|
||||
await this.saveTasks();
|
||||
|
||||
// 获取作品信息
|
||||
const artworkResult = await this.artworkService.getArtworkDetail(artworkId);
|
||||
@@ -59,8 +365,14 @@ class DownloadService {
|
||||
}
|
||||
|
||||
const artwork = artworkResult.data;
|
||||
const artistName = artwork.user.name.replace(/[<>:"/\\|?*]/g, '_');
|
||||
const artworkTitle = artwork.title.replace(/[<>:"/\\|?*]/g, '_');
|
||||
|
||||
// 确保作品信息完整
|
||||
if (!artwork || !artwork.user || !artwork.title) {
|
||||
throw new Error('作品信息不完整');
|
||||
}
|
||||
|
||||
const artistName = (artwork.user.name || 'Unknown Artist').replace(/[<>:"/\\|?*]/g, '_');
|
||||
const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_');
|
||||
|
||||
// 创建作品目录
|
||||
const artworkDir = path.join(this.downloadPath, `${artistName}_${artworkId}`, artworkTitle);
|
||||
@@ -73,11 +385,14 @@ class DownloadService {
|
||||
}
|
||||
|
||||
const images = imagesResult.data.images;
|
||||
const task = this.tasks.get(taskId);
|
||||
task.total = images.length;
|
||||
|
||||
// 下载所有图片
|
||||
const downloadPromises = images.map(async (image, index) => {
|
||||
if (task.status === 'cancelled') {
|
||||
return { success: false, error: '任务已取消' };
|
||||
}
|
||||
|
||||
try {
|
||||
const imageUrl = image[size] || image.original;
|
||||
const fileExt = this.getFileExtension(imageUrl);
|
||||
@@ -91,9 +406,11 @@ class DownloadService {
|
||||
task.files.push({
|
||||
path: filePath,
|
||||
url: imageUrl,
|
||||
size: size
|
||||
size: size,
|
||||
filename: fileName
|
||||
});
|
||||
|
||||
await this.saveTasks();
|
||||
return { success: true, file: fileName };
|
||||
} catch (error) {
|
||||
task.failed++;
|
||||
@@ -111,6 +428,33 @@ class DownloadService {
|
||||
// 更新任务状态
|
||||
task.status = task.failed === 0 ? 'completed' : 'partial';
|
||||
task.end_time = new Date();
|
||||
await this.saveTasks();
|
||||
|
||||
// 添加到历史记录
|
||||
const historyItem = {
|
||||
id: taskId,
|
||||
type: 'artwork',
|
||||
artwork_id: artworkId,
|
||||
artist_name: artistName,
|
||||
artwork_title: artworkTitle,
|
||||
download_path: artworkDir,
|
||||
total_files: task.total,
|
||||
completed_files: task.completed,
|
||||
failed_files: task.failed,
|
||||
files: task.files,
|
||||
start_time: task.start_time,
|
||||
end_time: task.end_time,
|
||||
status: task.status
|
||||
};
|
||||
|
||||
this.history.unshift(historyItem);
|
||||
await this.saveHistory();
|
||||
|
||||
console.log('下载完成,历史记录已保存:', {
|
||||
taskId,
|
||||
historyLength: this.history.length,
|
||||
tasksCount: this.tasks.size
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -131,7 +475,9 @@ class DownloadService {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (task) {
|
||||
task.status = 'failed';
|
||||
task.error = error.message;
|
||||
task.end_time = new Date();
|
||||
await this.saveTasks();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -141,38 +487,112 @@ class DownloadService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
async downloadFile(url, filePath) {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'Referer': 'https://www.pixiv.net/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
getFileExtension(url) {
|
||||
const match = url.match(/\.([a-zA-Z0-9]+)(\?|$)/);
|
||||
return match ? `.${match[1]}` : '.jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载作品
|
||||
*/
|
||||
async downloadMultipleArtworks(artworkIds, options = {}) {
|
||||
const taskId = uuidv4();
|
||||
const { concurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options;
|
||||
const { concurrent = 3, size = 'original', quality = 'high', format = 'auto', skipExisting = true } = options;
|
||||
|
||||
try {
|
||||
// 检查重复下载
|
||||
let filteredIds = artworkIds;
|
||||
let skippedCount = 0;
|
||||
|
||||
if (skipExisting) {
|
||||
const downloadedIds = await this.getDownloadedArtworkIds();
|
||||
const downloadedSet = new Set(downloadedIds);
|
||||
|
||||
filteredIds = artworkIds.filter(id => !downloadedSet.has(id));
|
||||
skippedCount = artworkIds.length - filteredIds.length;
|
||||
|
||||
console.log(`批量下载: 总共 ${artworkIds.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${filteredIds.length} 个作品`);
|
||||
}
|
||||
|
||||
// 创建任务记录
|
||||
this.tasks.set(taskId, {
|
||||
const task = {
|
||||
id: taskId,
|
||||
type: 'batch',
|
||||
artwork_ids: artworkIds,
|
||||
filtered_ids: filteredIds,
|
||||
status: 'downloading',
|
||||
progress: 0,
|
||||
total: artworkIds.length,
|
||||
total: filteredIds.length,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
skipped: skippedCount,
|
||||
results: [],
|
||||
start_time: new Date(),
|
||||
end_time: null
|
||||
});
|
||||
end_time: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
this.tasks.set(taskId, task);
|
||||
await this.saveTasks();
|
||||
|
||||
const task = this.tasks.get(taskId);
|
||||
const results = [];
|
||||
|
||||
// 如果没有需要下载的作品,直接返回
|
||||
if (filteredIds.length === 0) {
|
||||
task.status = 'completed';
|
||||
task.end_time = new Date();
|
||||
await this.saveTasks();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task_id: taskId,
|
||||
total_artworks: artworkIds.length,
|
||||
completed_artworks: 0,
|
||||
failed_artworks: 0,
|
||||
skipped_artworks: skippedCount,
|
||||
message: '所有作品都已下载完成'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 分批下载
|
||||
for (let i = 0; i < artworkIds.length; i += concurrent) {
|
||||
const batch = artworkIds.slice(i, i + concurrent);
|
||||
for (let i = 0; i < filteredIds.length; i += concurrent) {
|
||||
if (task.status === 'cancelled') {
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = filteredIds.slice(i, i + concurrent);
|
||||
const batchPromises = batch.map(async (artworkId) => {
|
||||
try {
|
||||
const result = await this.downloadArtwork(artworkId, { size, quality, format });
|
||||
const result = await this.downloadArtwork(artworkId, { size, quality, format, skipExisting: false });
|
||||
task.completed++;
|
||||
results.push({ artwork_id: artworkId, ...result });
|
||||
return result;
|
||||
@@ -185,12 +605,19 @@ class DownloadService {
|
||||
|
||||
await Promise.all(batchPromises);
|
||||
task.progress = Math.round((task.completed / task.total) * 100);
|
||||
await this.saveTasks();
|
||||
|
||||
// 添加延迟避免请求过于频繁
|
||||
if (i + concurrent < filteredIds.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.saveTasks();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -207,7 +634,9 @@ class DownloadService {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (task) {
|
||||
task.status = 'failed';
|
||||
task.error = error.message;
|
||||
task.end_time = new Date();
|
||||
await this.saveTasks();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -222,91 +651,159 @@ class DownloadService {
|
||||
*/
|
||||
async downloadArtistArtworks(artistId, options = {}) {
|
||||
const taskId = uuidv4();
|
||||
const {
|
||||
type = 'art',
|
||||
filter = 'for_ios',
|
||||
size = 'original',
|
||||
quality = 'high',
|
||||
const {
|
||||
type = 'art',
|
||||
limit = 50,
|
||||
size = 'original',
|
||||
quality = 'high',
|
||||
format = 'auto',
|
||||
limit = 50,
|
||||
concurrent = 3
|
||||
skipExisting = true,
|
||||
maxConcurrent = 3,
|
||||
pageSize = 30
|
||||
} = options;
|
||||
|
||||
|
||||
try {
|
||||
// 获取作者信息
|
||||
const artistResult = await this.artistService.getArtistInfo(artistId);
|
||||
if (!artistResult.success) {
|
||||
throw new Error(`获取作者信息失败: ${artistResult.error}`);
|
||||
}
|
||||
|
||||
const artist = artistResult.data;
|
||||
const artistName = artist.name.replace(/[<>:"/\\|?*]/g, '_');
|
||||
|
||||
// 获取作者作品列表
|
||||
const artworksResult = await this.artistService.getArtistArtworks(artistId, {
|
||||
type,
|
||||
filter,
|
||||
limit
|
||||
});
|
||||
|
||||
if (!artworksResult.success) {
|
||||
throw new Error(`获取作者作品列表失败: ${artworksResult.error}`);
|
||||
}
|
||||
|
||||
const artworks = artworksResult.data.artworks;
|
||||
const artworkIds = artworks.map(artwork => artwork.id);
|
||||
|
||||
// 创建任务记录
|
||||
this.tasks.set(taskId, {
|
||||
const task = {
|
||||
id: taskId,
|
||||
type: 'artist',
|
||||
artist_id: artistId,
|
||||
artist_name: artistName,
|
||||
status: 'downloading',
|
||||
progress: 0,
|
||||
total: artworkIds.length,
|
||||
total: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
results: [],
|
||||
start_time: new Date(),
|
||||
end_time: null
|
||||
});
|
||||
end_time: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
this.tasks.set(taskId, task);
|
||||
await this.saveTasks();
|
||||
|
||||
// 批量下载作品
|
||||
const batchResult = await this.downloadMultipleArtworks(artworkIds, {
|
||||
concurrent,
|
||||
size,
|
||||
quality,
|
||||
format
|
||||
});
|
||||
// 获取已下载的作品ID
|
||||
const downloadedIds = skipExisting ? await this.getDownloadedArtworkIds() : [];
|
||||
const downloadedSet = new Set(downloadedIds);
|
||||
|
||||
if (batchResult.success) {
|
||||
const task = this.tasks.get(taskId);
|
||||
task.status = batchResult.data.failed_artworks === 0 ? 'completed' : 'partial';
|
||||
// 分页获取作者作品列表
|
||||
let allArtworks = [];
|
||||
let offset = 0;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore && allArtworks.length < limit) {
|
||||
const artworksResult = await this.artistService.getArtistArtworks(artistId, {
|
||||
type,
|
||||
offset: offset,
|
||||
limit: Math.min(pageSize, limit - allArtworks.length)
|
||||
});
|
||||
|
||||
if (!artworksResult.success) {
|
||||
throw new Error(`获取作者作品失败: ${artworksResult.error}`);
|
||||
}
|
||||
|
||||
const artworks = artworksResult.data.artworks;
|
||||
if (artworks.length === 0) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
allArtworks.push(...artworks);
|
||||
offset += artworks.length;
|
||||
|
||||
// 添加延迟避免请求过于频繁
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤已下载的作品
|
||||
const newArtworks = skipExisting
|
||||
? allArtworks.filter(artwork => !downloadedSet.has(artwork.id))
|
||||
: allArtworks;
|
||||
|
||||
const skippedCount = allArtworks.length - newArtworks.length;
|
||||
task.skipped = skippedCount;
|
||||
task.total = newArtworks.length;
|
||||
await this.saveTasks();
|
||||
|
||||
console.log(`作者作品下载: 总共 ${allArtworks.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${newArtworks.length} 个作品`);
|
||||
|
||||
// 如果没有需要下载的作品,直接返回
|
||||
if (newArtworks.length === 0) {
|
||||
task.status = 'completed';
|
||||
task.end_time = new Date();
|
||||
task.results = batchResult.data.results;
|
||||
|
||||
await this.saveTasks();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task_id: taskId,
|
||||
artist_id: artistId,
|
||||
artist_name: artistName,
|
||||
total_artworks: batchResult.data.total_artworks,
|
||||
completed_artworks: batchResult.data.completed_artworks,
|
||||
failed_artworks: batchResult.data.failed_artworks,
|
||||
results: batchResult.data.results
|
||||
total_artworks: allArtworks.length,
|
||||
completed_artworks: 0,
|
||||
failed_artworks: 0,
|
||||
skipped_artworks: skippedCount,
|
||||
message: '所有作品都已下载完成'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new Error(batchResult.error);
|
||||
}
|
||||
|
||||
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 {
|
||||
const result = await this.downloadArtwork(artwork.id, { size, quality, format, skipExisting: false });
|
||||
task.completed++;
|
||||
results.push({ artwork_id: artwork.id, ...result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
task.failed++;
|
||||
results.push({ artwork_id: artwork.id, success: false, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(batchPromises);
|
||||
task.progress = Math.round((task.completed / task.total) * 100);
|
||||
await this.saveTasks();
|
||||
|
||||
// 添加延迟避免请求过于频繁
|
||||
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.saveTasks();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
task_id: taskId,
|
||||
artist_id: artistId,
|
||||
total_artworks: task.total,
|
||||
completed_artworks: task.completed,
|
||||
failed_artworks: task.failed,
|
||||
results: results
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (task) {
|
||||
task.status = 'failed';
|
||||
task.error = error.message;
|
||||
task.end_time = new Date();
|
||||
await this.saveTasks();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -315,130 +812,6 @@ class DownloadService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载进度
|
||||
*/
|
||||
async getDownloadProgress(taskId) {
|
||||
const task = this.tasks.get(taskId);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Task not found'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: task.id,
|
||||
type: task.type,
|
||||
status: task.status,
|
||||
progress: task.progress,
|
||||
total: task.total,
|
||||
completed: task.completed,
|
||||
failed: task.failed,
|
||||
start_time: task.start_time,
|
||||
end_time: task.end_time,
|
||||
files: task.files || [],
|
||||
results: task.results || []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下载任务
|
||||
*/
|
||||
async cancelDownload(taskId) {
|
||||
const task = this.tasks.get(taskId);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Task not found'
|
||||
};
|
||||
}
|
||||
|
||||
if (task.status === 'completed' || task.status === 'failed') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Task already finished'
|
||||
};
|
||||
}
|
||||
|
||||
task.status = 'cancelled';
|
||||
task.end_time = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Download task cancelled successfully'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载历史
|
||||
*/
|
||||
async getDownloadHistory(options = {}) {
|
||||
const { offset = 0, limit = 20 } = options;
|
||||
|
||||
try {
|
||||
const tasks = Array.from(this.tasks.values())
|
||||
.filter(task => task.status === 'completed' || task.status === 'partial')
|
||||
.sort((a, b) => b.end_time - a.end_time)
|
||||
.slice(offset, offset + limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: tasks,
|
||||
total: this.tasks.size,
|
||||
offset,
|
||||
limit
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单个文件
|
||||
*/
|
||||
async downloadFile(url, filePath) {
|
||||
const headers = {
|
||||
'Referer': 'https://app-api.pixiv.net/',
|
||||
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
|
||||
};
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
headers,
|
||||
responseType: 'stream',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
getFileExtension(url) {
|
||||
const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
|
||||
return match ? `.${match[1]}` : '.jpg';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DownloadService;
|
||||
Reference in New Issue
Block a user