初始化

This commit is contained in:
2025-08-21 10:43:04 +08:00
commit 29a79b1c6b
68 changed files with 13314 additions and 0 deletions
+444
View File
@@ -0,0 +1,444 @@
const axios = require('axios');
const fs = require('fs-extra');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const ArtworkService = require('./artwork');
const ArtistService = require('./artist');
class DownloadService {
constructor(auth) {
this.auth = auth;
this.artworkService = new ArtworkService(auth);
this.artistService = new ArtistService(auth);
this.downloadPath = path.join(__dirname, '../../downloads');
this.tasks = new Map(); // 存储下载任务状态
// 确保下载目录存在
this.ensureDownloadDir();
}
/**
* 确保下载目录存在
*/
async ensureDownloadDir() {
try {
await fs.ensureDir(this.downloadPath);
console.log('下载目录已创建:', this.downloadPath);
} catch (error) {
console.error('创建下载目录失败:', error);
}
}
/**
* 下载单个作品
*/
async downloadArtwork(artworkId, options = {}) {
const taskId = uuidv4();
const { size = 'original', quality = 'high', format = 'auto' } = options;
try {
// 创建任务记录
this.tasks.set(taskId, {
id: taskId,
type: 'artwork',
artwork_id: artworkId,
status: 'downloading',
progress: 0,
total: 1,
completed: 0,
failed: 0,
files: [],
start_time: new Date(),
end_time: null
});
// 获取作品信息
const artworkResult = await this.artworkService.getArtworkDetail(artworkId);
if (!artworkResult.success) {
throw new Error(`获取作品信息失败: ${artworkResult.error}`);
}
const artwork = artworkResult.data;
const artistName = artwork.user.name.replace(/[<>:"/\\|?*]/g, '_');
const artworkTitle = artwork.title.replace(/[<>:"/\\|?*]/g, '_');
// 创建作品目录
const artworkDir = path.join(this.downloadPath, `${artistName}_${artworkId}`, artworkTitle);
await fs.ensureDir(artworkDir);
// 获取图片URL
const imagesResult = await this.artworkService.getArtworkImages(artworkId, size);
if (!imagesResult.success) {
throw new Error(`获取图片URL失败: ${imagesResult.error}`);
}
const images = imagesResult.data.images;
const task = this.tasks.get(taskId);
task.total = images.length;
// 下载所有图片
const downloadPromises = images.map(async (image, index) => {
try {
const imageUrl = image[size] || image.original;
const fileExt = this.getFileExtension(imageUrl);
const fileName = `${artworkTitle}_${artworkId}_${index + 1}${fileExt}`;
const filePath = path.join(artworkDir, fileName);
await this.downloadFile(imageUrl, filePath);
task.completed++;
task.progress = Math.round((task.completed / task.total) * 100);
task.files.push({
path: filePath,
url: imageUrl,
size: size
});
return { success: true, file: fileName };
} catch (error) {
task.failed++;
console.error(`下载图片失败 ${index + 1}:`, error.message);
return { success: false, error: error.message };
}
});
await Promise.all(downloadPromises);
// 保存作品信息
const infoPath = path.join(artworkDir, 'artwork_info.json');
await fs.writeJson(infoPath, artwork, { spaces: 2 });
// 更新任务状态
task.status = task.failed === 0 ? 'completed' : 'partial';
task.end_time = new Date();
return {
success: true,
data: {
task_id: taskId,
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
}
};
} catch (error) {
const task = this.tasks.get(taskId);
if (task) {
task.status = 'failed';
task.end_time = new Date();
}
return {
success: false,
error: error.message
};
}
}
/**
* 批量下载作品
*/
async downloadMultipleArtworks(artworkIds, options = {}) {
const taskId = uuidv4();
const { concurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options;
try {
// 创建任务记录
this.tasks.set(taskId, {
id: taskId,
type: 'batch',
artwork_ids: artworkIds,
status: 'downloading',
progress: 0,
total: artworkIds.length,
completed: 0,
failed: 0,
results: [],
start_time: new Date(),
end_time: null
});
const task = this.tasks.get(taskId);
const results = [];
// 分批下载
for (let i = 0; i < artworkIds.length; i += concurrent) {
const batch = artworkIds.slice(i, i + concurrent);
const batchPromises = batch.map(async (artworkId) => {
try {
const result = await this.downloadArtwork(artworkId, { size, quality, format });
task.completed++;
results.push({ artwork_id: artworkId, ...result });
return result;
} catch (error) {
task.failed++;
results.push({ artwork_id: artworkId, success: false, error: error.message });
return { success: false, error: error.message };
}
});
await Promise.all(batchPromises);
task.progress = Math.round((task.completed / task.total) * 100);
}
// 更新任务状态
task.status = task.failed === 0 ? 'completed' : 'partial';
task.end_time = new Date();
task.results = results;
return {
success: true,
data: {
task_id: taskId,
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.end_time = new Date();
}
return {
success: false,
error: error.message
};
}
}
/**
* 下载作者作品
*/
async downloadArtistArtworks(artistId, options = {}) {
const taskId = uuidv4();
const {
type = 'art',
filter = 'for_ios',
size = 'original',
quality = 'high',
format = 'auto',
limit = 50,
concurrent = 3
} = 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, {
id: taskId,
type: 'artist',
artist_id: artistId,
artist_name: artistName,
status: 'downloading',
progress: 0,
total: artworkIds.length,
completed: 0,
failed: 0,
results: [],
start_time: new Date(),
end_time: null
});
// 批量下载作品
const batchResult = await this.downloadMultipleArtworks(artworkIds, {
concurrent,
size,
quality,
format
});
if (batchResult.success) {
const task = this.tasks.get(taskId);
task.status = batchResult.data.failed_artworks === 0 ? 'completed' : 'partial';
task.end_time = new Date();
task.results = batchResult.data.results;
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
}
};
} else {
throw new Error(batchResult.error);
}
} catch (error) {
const task = this.tasks.get(taskId);
if (task) {
task.status = 'failed';
task.end_time = new Date();
}
return {
success: false,
error: error.message
};
}
}
/**
* 获取下载进度
*/
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;