初始化

This commit is contained in:
2025-08-21 10:43:04 +08:00
commit 29a79b1c6b
68 changed files with 13314 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
const axios = require('axios');
const { stringify } = require('qs');
class ArtistService {
constructor(auth) {
this.auth = auth;
this.baseURL = 'https://app-api.pixiv.net';
}
/**
* 获取作者信息
*/
async getArtistInfo(artistId) {
try {
const response = await this.makeRequest(
'GET',
'/v1/user/detail',
{ user_id: artistId }
);
return {
success: true,
data: response.user
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取作者作品列表
*/
async getArtistArtworks(artistId, options = {}) {
try {
const {
type = 'art',
filter = 'for_ios',
offset = 0,
limit = 30
} = options;
const params = {
user_id: artistId,
type,
filter,
offset
};
const response = await this.makeRequest(
'GET',
`/v1/user/illusts?${stringify(params)}`
);
return {
success: true,
data: {
artworks: response.illusts,
next_url: response.next_url,
total: response.illusts.length
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取作者关注列表
*/
async getArtistFollowing(artistId, options = {}) {
try {
const {
restrict = 'public',
offset = 0,
limit = 30
} = options;
const params = {
user_id: artistId,
restrict,
offset
};
const response = await this.makeRequest(
'GET',
`/v1/user/following?${stringify(params)}`
);
return {
success: true,
data: {
users: response.user_previews,
next_url: response.next_url,
total: response.user_previews.length
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取作者粉丝列表
*/
async getArtistFollowers(artistId, options = {}) {
try {
const {
offset = 0,
limit = 30
} = options;
const params = {
user_id: artistId,
offset
};
const response = await this.makeRequest(
'GET',
`/v1/user/follower?${stringify(params)}`
);
return {
success: true,
data: {
users: response.user_previews,
next_url: response.next_url,
total: response.user_previews.length
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 关注/取消关注作者
*/
async followArtist(artistId, action = 'follow') {
try {
const data = {
user_id: artistId,
restrict: 'public'
};
const endpoint = action === 'follow' ? '/v1/user/follow/add' : '/v1/user/follow/delete';
const response = await this.makeRequest(
'POST',
endpoint,
data
);
return {
success: true,
data: response
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 搜索作者
*/
async searchArtists(searchOptions) {
try {
const {
keyword,
sort = 'date_desc',
duration = 'all',
offset = 0,
limit = 30
} = searchOptions;
const params = {
word: keyword,
sort,
duration,
offset,
filter: 'for_ios'
};
const response = await this.makeRequest(
'GET',
`/v1/search/user?${stringify(params)}`
);
return {
success: true,
data: {
users: response.user_previews,
next_url: response.next_url,
search_span_limit: response.search_span_limit,
total: response.user_previews.length
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取推荐作者
*/
async getRecommendedArtists(options = {}) {
try {
const {
offset = 0,
limit = 30
} = options;
const params = {
offset,
filter: 'for_ios'
};
const response = await this.makeRequest(
'GET',
`/v1/user/recommended?${stringify(params)}`
);
return {
success: true,
data: {
users: response.user_previews,
next_url: response.next_url
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取作者统计信息
*/
async getArtistStats(artistId) {
try {
const response = await this.makeRequest(
'GET',
'/v1/user/detail',
{ user_id: artistId }
);
const user = response.user;
const stats = {
user_id: user.id,
total_illusts: user.total_illusts,
total_manga: user.total_manga,
total_novels: user.total_novels,
total_bookmarked_illust: user.total_bookmarked_illust,
total_following: user.total_following,
total_followers: user.total_followers,
total_illust_bookmarks_public: user.total_illust_bookmarks_public,
total_illust_series: user.total_illust_series,
total_novel_series: user.total_novel_series,
background: user.background,
twitter_account: user.twitter_account,
twitter_url: user.twitter_url,
pawoo_url: user.pawoo_url,
is_followed: user.is_followed,
is_following: user.is_following,
is_friend: user.is_friend,
is_blocking: user.is_blocking,
is_blocked: user.is_blocked,
accept_request: user.accept_request
};
return {
success: true,
data: stats
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 发送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;
}
}
const response = await axios(config);
return response.data;
}
}
module.exports = ArtistService;
+307
View File
@@ -0,0 +1,307 @@
const axios = require('axios');
const { stringify } = require('qs');
class ArtworkService {
constructor(auth) {
this.auth = auth;
this.baseURL = 'https://app-api.pixiv.net';
}
/**
* 获取作品详情
*/
async getArtworkDetail(artworkId, options = {}) {
try {
const { include_user = true, include_series = false } = options;
const params = {
include_user,
include_series
};
const response = await this.makeRequest(
'GET',
`/v1/illust/detail?${stringify(params)}`,
{ illust_id: artworkId }
);
return {
success: true,
data: response.illust
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取作品预览信息
*/
async getArtworkPreview(artworkId) {
try {
const response = await this.makeRequest(
'GET',
'/v1/illust/detail',
{ illust_id: artworkId }
);
const artwork = response.illust;
// 构建预览信息
const preview = {
id: artwork.id,
title: artwork.title,
description: artwork.caption,
user: {
id: artwork.user.id,
name: artwork.user.name,
account: artwork.user.account
},
image_urls: artwork.image_urls,
tags: artwork.tags.map(tag => tag.name),
create_date: artwork.create_date,
update_date: artwork.update_date,
type: artwork.type,
width: artwork.width,
height: artwork.height,
page_count: artwork.page_count,
is_bookmarked: artwork.is_bookmarked,
total_bookmarks: artwork.total_bookmarks,
total_view: artwork.total_view,
is_muted: artwork.is_muted,
meta_single_page: artwork.meta_single_page,
meta_pages: artwork.meta_pages
};
return {
success: true,
data: preview
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取作品图片URL
*/
async getArtworkImages(artworkId, size = 'medium') {
try {
const response = await this.makeRequest(
'GET',
'/v1/illust/detail',
{ illust_id: artworkId }
);
const artwork = response.illust;
const images = [];
if (artwork.meta_single_page && artwork.meta_single_page.original_image_url) {
// 单页作品
images.push({
page: 1,
original: artwork.meta_single_page.original_image_url,
large: artwork.meta_single_page.large_image_url,
medium: artwork.image_urls.medium,
square_medium: artwork.image_urls.square_medium
});
} else if (artwork.meta_pages && artwork.meta_pages.length > 0) {
// 多页作品
artwork.meta_pages.forEach((page, index) => {
images.push({
page: index + 1,
original: page.image_urls.original,
large: page.image_urls.large,
medium: page.image_urls.medium,
square_medium: page.image_urls.square_medium
});
});
}
return {
success: true,
data: {
artwork_id: artworkId,
total_pages: artwork.page_count,
images: images,
selected_size: size
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 搜索作品
*/
async searchArtworks(searchOptions) {
try {
const {
keyword,
type = 'all',
sort = 'date_desc',
duration = 'all',
offset = 0,
limit = 30
} = searchOptions;
const params = {
word: keyword,
search_target: type,
sort: sort,
duration: duration,
offset,
filter: 'for_ios'
};
const response = await this.makeRequest(
'GET',
`/v1/search/illust?${stringify(params)}`
);
return {
success: true,
data: {
artworks: response.illusts,
next_url: response.next_url,
search_span_limit: response.search_span_limit,
total: response.illusts.length
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取推荐作品
*/
async getRecommendedArtworks(options = {}) {
try {
const {
offset = 0,
limit = 30,
include_ranking_illusts = true,
include_privacy_policy = false
} = options;
const params = {
offset,
include_ranking_illusts,
include_privacy_policy,
filter: 'for_ios'
};
const response = await this.makeRequest(
'GET',
`/v1/illust/recommended?${stringify(params)}`
);
return {
success: true,
data: {
artworks: response.illusts,
next_url: response.next_url,
ranking_illusts: response.ranking_illusts || []
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取排行榜作品
*/
async getRankingArtworks(options = {}) {
try {
const {
mode = 'day',
filter = 'for_ios',
offset = 0
} = options;
const params = {
mode,
filter,
offset
};
const response = await this.makeRequest(
'GET',
`/v1/illust/ranking?${stringify(params)}`
);
return {
success: true,
data: {
artworks: response.illusts,
next_url: response.next_url,
mode,
date: response.date
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 发送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;
}
}
const response = await axios(config);
return response.data;
}
}
module.exports = ArtworkService;
+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;