初始化

This commit is contained in:
2025-08-21 10:43:04 +08:00
commit 29a79b1c6b
68 changed files with 13314 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
# Pixiv 后端服务
这是一个优雅的 Pixiv 后端服务架构,提供作品信息获取、作者信息查询、文件下载等功能。
## 🏗️ 项目架构
```
backend/
├── server.js # 主服务器文件
├── core.js # 核心后端逻辑
├── auth.js # 认证模块
├── config.js # 代理配置
├── start.js # 启动脚本
├── test-login.js # 登录测试脚本
├── middleware/ # 中间件
│ ├── auth.js # 认证中间件
│ └── errorHandler.js # 错误处理中间件
├── routes/ # 路由模块
│ ├── auth.js # 认证路由
│ ├── artwork.js # 作品路由
│ ├── artist.js # 作者路由
│ └── download.js # 下载路由
├── services/ # 服务层
│ ├── artwork.js # 作品服务
│ ├── artist.js # 作者服务
│ └── download.js # 下载服务
└── utils/ # 工具类
└── response.js # 响应工具
```
## 🚀 快速开始
### 1. 安装依赖
```bash
npm install
```
### 2. 启动服务器
```bash
# 开发模式
npm run dev
# 生产模式
npm start
# 或直接运行
node backend/start.js
```
### 3. 测试登录
```bash
node backend/test-login.js
```
## 📡 API 接口
### 认证相关
- `GET /api/auth/status` - 获取登录状态
- `GET /api/auth/login-url` - 获取登录URL
- `POST /api/auth/callback` - 处理登录回调
- `POST /api/auth/relogin` - 重新登录
- `POST /api/auth/logout` - 登出
### 作品相关
- `GET /api/artwork/:id` - 获取作品详情
- `GET /api/artwork/:id/preview` - 获取作品预览
- `GET /api/artwork/:id/images` - 获取作品图片URL
- `GET /api/artwork/search` - 搜索作品
### 作者相关
- `GET /api/artist/:id` - 获取作者信息
- `GET /api/artist/:id/artworks` - 获取作者作品列表
- `GET /api/artist/:id/following` - 获取作者关注列表
- `GET /api/artist/:id/followers` - 获取作者粉丝列表
- `POST /api/artist/:id/follow` - 关注/取消关注作者
### 下载相关
- `POST /api/download/artwork/:id` - 下载单个作品
- `POST /api/download/artworks` - 批量下载作品
- `POST /api/download/artist/:id` - 下载作者作品
- `GET /api/download/progress/:taskId` - 获取下载进度
- `DELETE /api/download/cancel/:taskId` - 取消下载任务
- `GET /api/download/history` - 获取下载历史
## 🔧 配置说明
### 代理配置
`config.js` 中配置代理设置:
```javascript
const proxyConfig = {
system: {
host: '127.0.0.1',
port: 7897,
protocol: 'http'
}
};
```
### 环境变量
- `PORT` - 服务器端口 (默认: 3000)
- `NODE_ENV` - 运行环境 (development/production)
- `FRONTEND_URL` - 前端URL (用于CORS)
## 📁 文件结构说明
### 核心模块
- **server.js**: 主服务器类,负责初始化、配置和启动服务器
- **core.js**: 核心后端逻辑,管理认证状态和配置
- **auth.js**: 认证模块,处理OAuth2.0登录流程
### 中间件
- **auth.js**: 认证中间件,验证用户登录状态
- **errorHandler.js**: 全局错误处理中间件
### 路由模块
- **auth.js**: 认证相关路由
- **artwork.js**: 作品相关路由
- **artist.js**: 作者相关路由
- **download.js**: 下载相关路由
### 服务层
- **artwork.js**: 作品服务,处理作品API调用
- **artist.js**: 作者服务,处理作者API调用
- **download.js**: 下载服务,处理文件下载
### 工具类
- **response.js**: 统一API响应格式工具
## 🎯 主要功能
### 1. 作品信息获取
- 获取作品详细信息
- 获取作品预览信息
- 获取作品图片URL
- 搜索作品
### 2. 作者信息查询
- 获取作者基本信息
- 获取作者作品列表
- 获取作者关注/粉丝列表
- 关注/取消关注作者
### 3. 文件下载
- 下载单个作品
- 批量下载作品
- 下载作者作品
- 下载进度跟踪
- 下载历史记录
### 4. 认证管理
- OAuth2.0 登录流程
- 自动刷新令牌
- 登录状态管理
## 🔒 安全特性
- 统一的错误处理
- 请求参数验证
- 认证中间件保护
- CORS 配置
- 代理支持
## 📊 监控和日志
- 请求日志记录
- 错误日志记录
- 健康检查接口
- 下载进度跟踪
## 🛠️ 开发指南
### 添加新路由
1.`routes/` 目录下创建新的路由文件
2.`server.js` 中注册路由
3. 添加相应的中间件保护
### 添加新服务
1.`services/` 目录下创建新的服务类
2. 实现相应的业务逻辑
3. 在路由中调用服务
### 添加新中间件
1.`middleware/` 目录下创建新的中间件文件
2.`server.js` 中注册中间件
## 📝 注意事项
1. 确保代理配置正确
2. 首次使用需要登录获取访问令牌
3. 下载功能需要足够的磁盘空间
4. 建议在生产环境中使用PM2等进程管理器
## 🤝 贡献
欢迎提交 Issue 和 Pull Request
## 许可证
MIT License
+274
View File
@@ -0,0 +1,274 @@
const axios = require('axios');
const Crypto = require('crypto');
const { Base64 } = require('js-base64');
const { stringify } = require('qs');
const moment = require('moment');
const { ProxyAgent } = require('proxy-agent');
// OAuth 2.0 配置
const CLIENT_ID = 'MOBrBDS8blbauoSck0ZfDbtuzpyT';
const CLIENT_SECRET = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj';
const REDIRECT_URI = 'https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback';
const LOGIN_URL = 'https://app-api.pixiv.net/web/v1/login';
const HASH_SECRET = '28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c';
class PixivAuth {
constructor(proxy = null) {
this.accessToken = null;
this.refreshToken = null;
this.user = null;
this.proxy = proxy;
// 创建 axios 实例,支持代理
this.axiosInstance = this.createAxiosInstance();
}
/**
* 创建支持代理的 axios 实例
*/
createAxiosInstance() {
const config = {
timeout: 30000, // 30秒超时
headers: this.getDefaultHeaders()
};
// 如果设置了代理,添加代理配置
if (this.proxy) {
console.log('使用代理:', this.proxy);
config.httpsAgent = new ProxyAgent(this.proxy);
} else {
// 尝试使用系统代理
const systemProxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy;
if (systemProxy) {
console.log('使用系统代理:', systemProxy);
config.httpsAgent = new ProxyAgent(systemProxy);
}
}
return axios.create(config);
}
/**
* 设置代理
*/
setProxy(proxy) {
this.proxy = proxy;
this.axiosInstance = this.createAxiosInstance();
}
/**
* 获取默认头部信息
*/
getDefaultHeaders() {
const datetime = moment().format();
return {
'App-OS': 'android',
'Accept-Language': 'en-us',
'App-OS-Version': '9.0',
'App-Version': '5.0.234',
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)',
'X-Client-Time': datetime,
'X-Client-Hash': Crypto.createHash('md5').update(`${datetime}${HASH_SECRET}`).digest('hex')
};
}
/**
* 生成 PKCE 参数
*/
generatePKCE() {
const codeVerifier = Base64.fromUint8Array(Crypto.randomBytes(32), true);
const codeChallenge = Base64.encodeURI(Crypto.createHash('sha256').update(codeVerifier).digest());
return {
code_verifier: codeVerifier,
code_challenge: codeChallenge
};
}
/**
* 获取登录URL
*/
getLoginUrl() {
const pkce = this.generatePKCE();
const params = {
code_challenge: pkce.code_challenge,
code_challenge_method: 'S256',
client: 'pixiv-android'
};
const loginUrl = `${LOGIN_URL}?${stringify(params)}`;
return {
login_url: loginUrl,
code_verifier: pkce.code_verifier
};
}
/**
* 使用授权码获取访问令牌
*/
async getAccessToken(code, codeVerifier) {
try {
console.log('正在获取访问令牌...');
console.log('Code:', code);
console.log('Code Verifier:', codeVerifier);
const data = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: code,
code_verifier: codeVerifier,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
include_policy: true
};
console.log('请求数据:', data);
const headers = {
...this.getDefaultHeaders(),
'Content-Type': 'application/x-www-form-urlencoded'
};
console.log('请求头部:', headers);
const response = await this.axiosInstance.post('https://oauth.secure.pixiv.net/auth/token',
stringify(data),
{ headers }
);
console.log('响应状态:', response.status);
console.log('响应数据:', JSON.stringify(response.data, null, 2));
const tokenData = response.data.response;
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
this.user = tokenData.user;
console.log('获取访问令牌成功');
return {
success: true,
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
user: tokenData.user
};
} catch (error) {
console.error('获取访问令牌失败:');
console.error('错误对象:', error);
console.error('响应状态:', error.response?.status);
console.error('响应数据:', error.response?.data);
console.error('错误消息:', error.message);
return {
success: false,
error: error.response?.data || error.message
};
}
}
/**
* 使用刷新令牌更新访问令牌
*/
async refreshAccessToken(refreshToken) {
try {
console.log('正在刷新访问令牌...');
const data = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
get_secure_url: true,
include_policy: true,
grant_type: 'refresh_token',
refresh_token: refreshToken
};
const headers = {
...this.getDefaultHeaders(),
'Content-Type': 'application/x-www-form-urlencoded'
};
const response = await this.axiosInstance.post('https://oauth.secure.pixiv.net/auth/token',
stringify(data),
{ headers }
);
const tokenData = response.data.response;
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
console.log('刷新访问令牌成功');
return {
success: true,
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token
};
} catch (error) {
console.error('刷新访问令牌失败:', error.response?.data || error.message);
return {
success: false,
error: error.response?.data || error.message
};
}
}
/**
* 获取用户信息
*/
async getUserInfo() {
if (!this.accessToken) {
return { success: false, error: '未登录' };
}
try {
const headers = {
...this.getDefaultHeaders(),
'Authorization': `Bearer ${this.accessToken}`
};
const response = await this.axiosInstance.get('https://app-api.pixiv.net/v1/user/me', {
headers
});
return {
success: true,
user: response.data.user
};
} catch (error) {
console.error('获取用户信息失败:', error.response?.data || error.message);
return {
success: false,
error: error.response?.data || error.message
};
}
}
/**
* 登出
*/
logout() {
this.accessToken = null;
this.refreshToken = null;
this.user = null;
console.log('已登出');
return { success: true };
}
/**
* 获取当前状态
*/
getStatus() {
return {
isLoggedIn: !!this.accessToken,
user: this.user,
hasRefreshToken: !!this.refreshToken
};
}
}
module.exports = PixivAuth;
+36
View File
@@ -0,0 +1,36 @@
// 代理配置
const proxyConfig = {
// 系统代理配置
system: {
host: '127.0.0.1',
port: 7897,
protocol: 'http'
},
// 代理URL格式
get proxyUrl() {
return `${this.system.protocol}://${this.system.host}:${this.system.port}`;
},
// 环境变量设置
setEnvironmentVariables() {
process.env.HTTP_PROXY = this.proxyUrl;
process.env.HTTPS_PROXY = this.proxyUrl;
process.env.http_proxy = this.proxyUrl;
process.env.https_proxy = this.proxyUrl;
console.log('代理环境变量已设置:', this.proxyUrl);
},
// 清除环境变量
clearEnvironmentVariables() {
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.http_proxy;
delete process.env.https_proxy;
console.log('代理环境变量已清除');
}
};
module.exports = proxyConfig;
+258
View File
@@ -0,0 +1,258 @@
const Fse = require('fs-extra');
const Path = require('path');
const PixivAuth = require('./auth');
// 配置文件路径
const CONFIG_FILE_DIR = require('appdata-path').getAppDataPath('pxder');
const CONFIG_FILE = Path.resolve(CONFIG_FILE_DIR, 'config.json');
// 默认配置
const defaultConfig = {
download: {
thread: 5,
timeout: 30,
path: null
},
refresh_token: null,
access_token: null,
user: null,
proxy: null
};
class PixivBackend {
constructor() {
this.config = null;
this.auth = null;
this.isLoggedIn = false;
}
/**
* 初始化后端
*/
async init() {
console.log('正在初始化 Pixiv 后端...');
// 初始化配置
this.initConfig();
this.config = this.readConfig();
// 创建认证实例,传入代理配置
this.auth = new PixivAuth(this.config.proxy);
// 检查登录状态
if (this.config.refresh_token) {
console.log('检测到已保存的登录信息,正在验证...');
await this.relogin();
} else {
console.log('未检测到登录信息,需要先登录');
}
return this;
}
/**
* 初始化配置文件
*/
initConfig() {
Fse.ensureDirSync(CONFIG_FILE_DIR);
if (!Fse.existsSync(CONFIG_FILE)) {
Fse.writeJSONSync(CONFIG_FILE, defaultConfig);
}
}
/**
* 读取配置
*/
readConfig() {
try {
const config = Fse.readJsonSync(CONFIG_FILE);
// 合并默认配置
return { ...defaultConfig, ...config };
} catch (error) {
console.error('读取配置文件失败:', error.message);
return { ...defaultConfig };
}
}
/**
* 保存配置
*/
saveConfig() {
try {
Fse.writeJsonSync(CONFIG_FILE, this.config);
console.log('配置已保存');
} catch (error) {
console.error('保存配置失败:', error.message);
}
}
/**
* 获取登录URL
*/
getLoginUrl() {
const loginData = this.auth.getLoginUrl();
this.config.code_verifier = loginData.code_verifier;
this.saveConfig();
return {
login_url: loginData.login_url,
code_verifier: loginData.code_verifier
};
}
/**
* 处理登录回调
*/
async handleLoginCallback(code) {
try {
console.log('正在处理登录回调...');
if (!this.config.code_verifier) {
throw new Error('缺少 code_verifier,请重新获取登录URL');
}
// 使用新的认证模块进行登录
const result = await this.auth.getAccessToken(code, this.config.code_verifier);
if (result.success) {
// 保存登录信息
this.config.refresh_token = result.refresh_token;
this.config.access_token = result.access_token;
this.config.user = result.user;
// 清理临时数据
delete this.config.code_verifier;
this.saveConfig();
this.isLoggedIn = true;
console.log(`登录成功!用户: ${result.user.account}`);
return {
success: true,
user: result.user
};
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('登录失败:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* 重新登录(使用保存的 refresh_token
*/
async relogin() {
try {
if (!this.config.refresh_token) {
throw new Error('没有保存的登录信息');
}
console.log('正在使用保存的登录信息重新登录...');
const result = await this.auth.refreshAccessToken(this.config.refresh_token);
if (result.success) {
// 更新配置
this.config.access_token = result.access_token;
this.config.refresh_token = result.refresh_token;
this.saveConfig();
this.isLoggedIn = true;
console.log('重新登录成功!');
return { success: true };
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('重新登录失败:', error.message);
// 清除无效的登录信息
this.config.refresh_token = null;
this.config.access_token = null;
this.config.user = null;
this.saveConfig();
this.isLoggedIn = false;
return {
success: false,
error: error.message
};
}
}
/**
* 登出
*/
logout() {
this.auth.logout();
this.config.refresh_token = null;
this.config.access_token = null;
this.config.user = null;
this.isLoggedIn = false;
this.saveConfig();
console.log('已登出');
return { success: true };
}
/**
* 获取登录状态
*/
getLoginStatus() {
const status = this.auth.getStatus();
return {
isLoggedIn: status.isLoggedIn,
username: this.config.user?.account,
user_id: this.config.user?.id
};
}
/**
* 设置下载路径
*/
setDownloadPath(path) {
this.config.download.path = path;
this.saveConfig();
console.log(`下载路径已设置为: ${path}`);
return { success: true };
}
/**
* 获取配置信息
*/
getConfig() {
return {
download: this.config.download,
proxy: this.config.proxy,
isLoggedIn: this.isLoggedIn
};
}
/**
* 设置代理
*/
setProxy(proxy) {
this.config.proxy = proxy;
this.auth.setProxy(proxy);
this.saveConfig();
console.log(`代理已设置为: ${proxy}`);
return { success: true };
}
/**
* 获取认证实例(用于后续API调用)
*/
getAuth() {
return this.auth;
}
}
module.exports = PixivBackend;
+50
View File
@@ -0,0 +1,50 @@
/**
* 认证中间件
*/
const authMiddleware = (req, res, next) => {
try {
// 检查后端是否已登录
if (!req.backend || !req.backend.isLoggedIn) {
return res.status(401).json({
error: true,
message: 'Authentication required',
code: 'AUTH_REQUIRED'
});
}
// 检查访问令牌是否有效
const auth = req.backend.getAuth();
if (!auth || !auth.accessToken) {
return res.status(401).json({
error: true,
message: 'Invalid access token',
code: 'INVALID_TOKEN'
});
}
next();
} catch (error) {
next(error);
}
};
/**
* 可选的认证中间件(不强制要求登录)
*/
const optionalAuthMiddleware = (req, res, next) => {
try {
// 如果后端已登录,将用户信息添加到请求对象
if (req.backend && req.backend.isLoggedIn) {
req.user = req.backend.config.user;
}
next();
} catch (error) {
next(error);
}
};
module.exports = {
authMiddleware,
optionalAuthMiddleware
};
+62
View File
@@ -0,0 +1,62 @@
/**
* 全局错误处理中间件
*/
const errorHandler = (err, req, res, next) => {
console.error('错误详情:', err);
// 默认错误信息
let statusCode = 500;
let message = 'Internal Server Error';
let details = null;
// 根据错误类型设置状态码和消息
if (err.name === 'ValidationError') {
statusCode = 400;
message = 'Validation Error';
details = err.message;
} else if (err.name === 'UnauthorizedError') {
statusCode = 401;
message = 'Unauthorized';
} else if (err.name === 'ForbiddenError') {
statusCode = 403;
message = 'Forbidden';
} else if (err.name === 'NotFoundError') {
statusCode = 404;
message = 'Not Found';
} else if (err.code === 'ENOTFOUND') {
statusCode = 503;
message = 'Service Unavailable';
details = 'Network connection failed';
} else if (err.code === 'ECONNREFUSED') {
statusCode = 503;
message = 'Service Unavailable';
details = 'Connection refused';
} else if (err.response) {
// Axios 错误
statusCode = err.response.status || 500;
message = err.response.statusText || 'Request Failed';
details = err.response.data;
} else if (err.message) {
message = err.message;
}
// 构建错误响应
const errorResponse = {
error: true,
message,
statusCode,
timestamp: new Date().toISOString(),
path: req.originalUrl,
method: req.method
};
// 在开发环境下添加详细信息
if (process.env.NODE_ENV === 'development') {
errorResponse.details = details;
errorResponse.stack = err.stack;
}
res.status(statusCode).json(errorResponse);
};
module.exports = { errorHandler };
+225
View File
@@ -0,0 +1,225 @@
const express = require('express');
const router = express.Router();
const ArtistService = require('../services/artist');
/**
* 获取作者信息
* 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
});
}
});
/**
* 获取作者作品列表
* GET /api/artist/:id/artworks
*/
router.get('/:id/artworks', async (req, res) => {
try {
const { id } = req.params;
const {
type = 'art',
filter = 'for_ios',
offset = 0,
limit = 30
} = req.query;
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.getArtistArtworks(parseInt(id), {
type,
filter,
offset: parseInt(offset),
limit: parseInt(limit)
});
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
});
}
});
/**
* 获取作者关注列表
* GET /api/artist/:id/following
*/
router.get('/:id/following', async (req, res) => {
try {
const { id } = req.params;
const {
restrict = 'public',
offset = 0,
limit = 30
} = req.query;
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.getArtistFollowing(parseInt(id), {
restrict,
offset: parseInt(offset),
limit: parseInt(limit)
});
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
});
}
});
/**
* 获取作者粉丝列表
* GET /api/artist/:id/followers
*/
router.get('/:id/followers', async (req, res) => {
try {
const { id } = req.params;
const {
offset = 0,
limit = 30
} = req.query;
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.getArtistFollowers(parseInt(id), {
offset: parseInt(offset),
limit: parseInt(limit)
});
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
});
}
});
/**
* 关注/取消关注作者
* POST /api/artist/:id/follow
*/
router.post('/:id/follow', async (req, res) => {
try {
const { id } = req.params;
const { action = 'follow' } = req.body;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
success: false,
error: 'Invalid artist ID'
});
}
if (!['follow', 'unfollow'].includes(action)) {
return res.status(400).json({
success: false,
error: 'Invalid action. Must be "follow" or "unfollow"'
});
}
const artistService = new ArtistService(req.backend.getAuth());
const result = await artistService.followArtist(parseInt(id), action);
if (result.success) {
res.json({
success: true,
message: `Artist ${action === 'follow' ? 'followed' : 'unfollowed'} successfully`
});
} else {
res.status(400).json({
success: false,
error: result.error
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;
+172
View File
@@ -0,0 +1,172 @@
const express = require('express');
const router = express.Router();
const ArtworkService = require('../services/artwork');
/**
* 搜索作品
* GET /api/artwork/search
*/
router.get('/search', async (req, res) => {
try {
const {
keyword,
type = 'all',
sort = 'date_desc',
duration = 'all',
offset = 0,
limit = 30
} = req.query;
if (!keyword) {
return res.status(400).json({
success: false,
error: 'Search keyword is required'
});
}
const artworkService = new ArtworkService(req.backend.getAuth());
const result = await artworkService.searchArtworks({
keyword,
type,
sort,
duration,
offset: parseInt(offset),
limit: parseInt(limit)
});
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/artwork/:id
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const { include_user = 'true', include_series = 'false' } = req.query;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
success: false,
error: 'Invalid artwork ID'
});
}
const artworkService = new ArtworkService(req.backend.getAuth());
const result = await artworkService.getArtworkDetail(parseInt(id), {
include_user: include_user === 'true',
include_series: include_series === 'true'
});
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
});
}
});
/**
* 获取作品预览信息
* GET /api/artwork/:id/preview
*/
router.get('/:id/preview', async (req, res) => {
try {
const { id } = req.params;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
success: false,
error: 'Invalid artwork ID'
});
}
const artworkService = new ArtworkService(req.backend.getAuth());
const result = await artworkService.getArtworkPreview(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
});
}
});
/**
* 获取作品图片URL
* GET /api/artwork/:id/images
*/
router.get('/:id/images', async (req, res) => {
try {
const { id } = req.params;
const { size = 'medium' } = req.query;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
success: false,
error: 'Invalid artwork ID'
});
}
const artworkService = new ArtworkService(req.backend.getAuth());
const result = await artworkService.getArtworkImages(parseInt(id), size);
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;
+124
View File
@@ -0,0 +1,124 @@
const express = require('express');
const router = express.Router();
/**
* 获取登录状态
* GET /api/auth/status
*/
router.get('/status', (req, res) => {
try {
const status = req.backend.getLoginStatus();
res.json({
success: true,
data: status
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 获取登录URL
* GET /api/auth/login-url
*/
router.get('/login-url', (req, res) => {
try {
const loginData = req.backend.getLoginUrl();
res.json({
success: true,
data: loginData
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 处理登录回调
* POST /api/auth/callback
*/
router.post('/callback', async (req, res) => {
try {
const { code } = req.body;
if (!code) {
return res.status(400).json({
success: false,
error: 'Authorization code is required'
});
}
const result = await req.backend.handleLoginCallback(code);
if (result.success) {
res.json({
success: true,
data: result
});
} else {
res.status(400).json({
success: false,
error: result.error
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 重新登录
* POST /api/auth/relogin
*/
router.post('/relogin', async (req, res) => {
try {
const result = await req.backend.relogin();
if (result.success) {
res.json({
success: true,
message: 'Relogin successful'
});
} else {
res.status(400).json({
success: false,
error: result.error
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 登出
* POST /api/auth/logout
*/
router.post('/logout', (req, res) => {
try {
const result = req.backend.logout();
res.json({
success: true,
message: 'Logout successful'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;
+270
View File
@@ -0,0 +1,270 @@
const express = require('express');
const router = express.Router();
const DownloadService = require('../services/download');
/**
* 下载单个作品
* POST /api/download/artwork/:id
*/
router.post('/artwork/:id', async (req, res) => {
try {
const { id } = req.params;
const {
size = 'original',
quality = 'high',
format = 'auto'
} = req.body;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
success: false,
error: 'Invalid artwork ID'
});
}
const downloadService = new DownloadService(req.backend.getAuth());
const result = await downloadService.downloadArtwork(parseInt(id), {
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
});
}
});
/**
* 批量下载作品
* POST /api/download/artworks
*/
router.post('/artworks', async (req, res) => {
try {
const {
artworkIds,
size = 'original',
quality = 'high',
format = 'auto',
concurrent = 3
} = req.body;
if (!artworkIds || !Array.isArray(artworkIds) || artworkIds.length === 0) {
return res.status(400).json({
success: false,
error: 'Artwork IDs array is required'
});
}
if (artworkIds.length > 50) {
return res.status(400).json({
success: false,
error: 'Maximum 50 artworks can be downloaded at once'
});
}
const downloadService = new DownloadService(req.backend.getAuth());
const result = await downloadService.downloadMultipleArtworks(artworkIds, {
size,
quality,
format,
concurrent: parseInt(concurrent)
});
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
});
}
});
/**
* 下载作者作品
* POST /api/download/artist/:id
*/
router.post('/artist/:id', async (req, res) => {
try {
const { id } = req.params;
const {
type = 'art',
filter = 'for_ios',
size = 'original',
quality = 'high',
format = 'auto',
limit = 50,
concurrent = 3
} = req.body;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
success: false,
error: 'Invalid artist ID'
});
}
const downloadService = new DownloadService(req.backend.getAuth());
const result = await downloadService.downloadArtistArtworks(parseInt(id), {
type,
filter,
size,
quality,
format,
limit: parseInt(limit),
concurrent: parseInt(concurrent)
});
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
*/
router.get('/progress/: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.getDownloadProgress(taskId);
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
});
}
});
/**
* 取消下载任务
* DELETE /api/download/cancel/:taskId
*/
router.delete('/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);
if (result.success) {
res.json({
success: true,
message: 'Download task cancelled successfully'
});
} else {
res.status(400).json({
success: false,
error: result.error
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* 获取下载历史
* GET /api/download/history
*/
router.get('/history', async (req, res) => {
try {
const {
offset = 0,
limit = 20
} = req.query;
const downloadService = new DownloadService(req.backend.getAuth());
const result = await downloadService.getDownloadHistory({
offset: parseInt(offset),
limit: parseInt(limit)
});
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
});
}
});
module.exports = router;
+155
View File
@@ -0,0 +1,155 @@
const express = require('express');
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 { errorHandler } = require('./middleware/errorHandler');
const { authMiddleware } = require('./middleware/auth');
// 导入核心模块
const PixivBackend = require('./core');
const proxyConfig = require('./config');
class PixivServer {
constructor() {
this.app = express();
this.backend = null;
this.port = process.env.PORT || 3000;
}
/**
* 初始化服务器
*/
async init() {
console.log('正在初始化 Pixiv 后端服务器...');
// 设置代理
proxyConfig.setEnvironmentVariables();
// 初始化 Pixiv 后端
this.backend = new PixivBackend();
await this.backend.init();
// 配置中间件
this.setupMiddleware();
// 配置路由
this.setupRoutes();
// 配置错误处理 - 临时注释掉
this.setupErrorHandling();
console.log('服务器初始化完成');
}
/**
* 配置中间件
*/
setupMiddleware() {
// 日志中间件
this.app.use(morgan('combined'));
// CORS 中间件
this.app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3001',
credentials: true
}));
// JSON 解析中间件
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 静态文件服务 - 临时注释掉
this.app.use('/downloads', express.static(path.join(__dirname, '../downloads')));
// 将后端实例注入到请求对象中
this.app.use((req, res, next) => {
req.backend = this.backend;
next();
});
}
/**
* 配置路由
*/
setupRoutes() {
// 健康检查
this.app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
backend: {
isLoggedIn: req.backend.isLoggedIn,
user: req.backend.config.user?.account
}
});
});
// 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);
// 404 处理
this.app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.originalUrl} not found`
});
});
}
/**
* 配置错误处理
*/
setupErrorHandling() {
this.app.use(errorHandler);
}
/**
* 启动服务器
*/
start() {
this.app.listen(this.port, () => {
console.log(`🚀 Pixiv 后端服务器已启动`);
console.log(`📍 地址: http://localhost:${this.port}`);
console.log(`🔗 健康检查: http://localhost:${this.port}/health`);
console.log(`📊 登录状态: ${this.backend.isLoggedIn ? '已登录' : '未登录'}`);
if (this.backend.isLoggedIn) {
console.log(`👤 用户: ${this.backend.config.user?.account}`);
}
});
}
/**
* 优雅关闭
*/
async shutdown() {
console.log('正在关闭服务器...');
// 清理代理环境变量
proxyConfig.clearEnvironmentVariables();
process.exit(0);
}
}
// 如果直接运行此文件
if (require.main === module) {
const server = new PixivServer();
// 处理进程信号
process.on('SIGINT', () => server.shutdown());
process.on('SIGTERM', () => server.shutdown());
// 启动服务器
server.init().then(() => server.start()).catch(console.error);
}
module.exports = PixivServer;
+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;
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env node
/**
* Pixiv 后端服务器启动脚本
*/
const PixivServer = require('./server');
// 设置环境变量
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
console.log('🚀 启动 Pixiv 后端服务器...');
console.log(`📊 环境: ${process.env.NODE_ENV}`);
console.log(`🌐 端口: ${process.env.PORT || 3000}`);
// 创建服务器实例
const server = new PixivServer();
// 处理进程信号
process.on('SIGINT', async () => {
console.log('\n🛑 收到 SIGINT 信号,正在关闭服务器...');
await server.shutdown();
});
process.on('SIGTERM', async () => {
console.log('\n🛑 收到 SIGTERM 信号,正在关闭服务器...');
await server.shutdown();
});
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
console.error('❌ 未捕获的异常:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('❌ 未处理的 Promise 拒绝:', reason);
process.exit(1);
});
// 启动服务器
server.init()
.then(() => server.start())
.catch((error) => {
console.error('❌ 服务器启动失败:', error);
process.exit(1);
});
+198
View File
@@ -0,0 +1,198 @@
const PixivBackend = require('./core');
const proxyConfig = require('./config');
const readline = require('readline');
// 创建命令行交互接口
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() {
console.log('=== Pixiv 登录测试脚本 ===\n');
try {
// 1. 设置代理环境变量
console.log('1. 设置代理配置...');
proxyConfig.setEnvironmentVariables();
// 2. 初始化后端
console.log('\n2. 初始化 Pixiv 后端...');
const backend = new PixivBackend();
await backend.init();
// 3. 检查登录状态
console.log('\n3. 检查当前登录状态...');
const loginStatus = backend.getLoginStatus();
console.log('登录状态:', loginStatus);
if (loginStatus.isLoggedIn) {
console.log('✅ 已登录,用户:', loginStatus.username);
return;
}
// 4. 获取登录URL
console.log('\n4. 获取登录URL...');
const loginData = backend.getLoginUrl();
console.log('请访问以下URL进行登录:');
console.log(loginData.login_url);
console.log('\n登录完成后,请复制回调URL中的code参数');
// 5. 等待用户输入授权码
const code = await askQuestion('\n请输入授权码 (code参数): ');
if (!code || code.trim() === '') {
console.log('❌ 未输入授权码,测试终止');
return;
}
// 6. 处理登录回调
console.log('\n5. 处理登录回调...');
const loginResult = await backend.handleLoginCallback(code.trim());
if (loginResult.success) {
console.log('✅ 登录成功!');
console.log('用户信息:', loginResult.user);
// 7. 再次检查登录状态
console.log('\n6. 验证登录状态...');
const finalStatus = backend.getLoginStatus();
console.log('最终登录状态:', finalStatus);
// 8. 测试获取用户信息
console.log('\n7. 测试获取用户信息...');
const auth = backend.getAuth();
const userInfo = await auth.getUserInfo();
if (userInfo.success) {
console.log('✅ 获取用户信息成功:', userInfo.user);
} else {
console.log('❌ 获取用户信息失败:', userInfo.error);
}
} else {
console.log('❌ 登录失败:', loginResult.error);
}
} catch (error) {
console.error('❌ 测试过程中发生错误:', error.message);
console.error('错误详情:', error);
} finally {
// 清理资源
rl.close();
console.log('\n=== 测试完成 ===');
}
}
// 测试重新登录功能
async function testRelogin() {
console.log('=== 测试重新登录功能 ===\n');
try {
// 设置代理
proxyConfig.setEnvironmentVariables();
// 初始化后端
const backend = new PixivBackend();
await backend.init();
// 检查是否有保存的登录信息
const loginStatus = backend.getLoginStatus();
if (loginStatus.isLoggedIn) {
console.log('✅ 检测到已保存的登录信息');
console.log('用户:', loginStatus.username);
console.log('用户ID:', loginStatus.user_id);
} else {
console.log('❌ 没有保存的登录信息,无法测试重新登录');
}
} catch (error) {
console.error('❌ 重新登录测试失败:', error.message);
}
}
// 测试登出功能
async function testLogout() {
console.log('=== 测试登出功能 ===\n');
try {
// 设置代理
proxyConfig.setEnvironmentVariables();
// 初始化后端
const backend = new PixivBackend();
await backend.init();
// 执行登出
const logoutResult = backend.logout();
if (logoutResult.success) {
console.log('✅ 登出成功');
// 验证登出状态
const loginStatus = backend.getLoginStatus();
console.log('登出后状态:', loginStatus);
} else {
console.log('❌ 登出失败');
}
} catch (error) {
console.error('❌ 登出测试失败:', error.message);
}
}
// 主函数
async function main() {
console.log('请选择测试功能:');
console.log('1. 测试完整登录流程');
console.log('2. 测试重新登录');
console.log('3. 测试登出');
console.log('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':
console.log('\n=== 运行所有测试 ===\n');
await testLogin();
console.log('\n' + '='.repeat(50) + '\n');
await testRelogin();
console.log('\n' + '='.repeat(50) + '\n');
await testLogout();
break;
default:
console.log('❌ 无效选择');
rl.close();
}
}
// 如果直接运行此脚本
if (require.main === module) {
main().catch(console.error);
}
module.exports = {
testLogin,
testRelogin,
testLogout
};
+60
View File
@@ -0,0 +1,60 @@
/**
* 统一API响应格式工具类
*/
class ResponseUtil {
/**
* 成功响应
*/
static success(data = null, message = 'Success') {
return {
success: true,
message,
data,
timestamp: new Date().toISOString()
};
}
/**
* 错误响应
*/
static error(message = 'Error', code = null, details = null) {
return {
success: false,
message,
code,
details,
timestamp: new Date().toISOString()
};
}
/**
* 分页响应
*/
static paginated(data, page, limit, total) {
return {
success: true,
data,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
},
timestamp: new Date().toISOString()
};
}
/**
* 列表响应
*/
static list(data, total = null) {
return {
success: true,
data,
total: total || (Array.isArray(data) ? data.length : 0),
timestamp: new Date().toISOString()
};
}
}
module.exports = ResponseUtil;