bug修复和架构完善
This commit is contained in:
@@ -64,3 +64,4 @@ typings/
|
||||
.next
|
||||
|
||||
old/
|
||||
downloads/
|
||||
|
||||
+7
-1
@@ -200,11 +200,17 @@ 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;
|
||||
+146
-44
@@ -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);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
data: progress
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取所有任务
|
||||
* GET /api/download/tasks
|
||||
*/
|
||||
router.get('/tasks', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const tasks = downloadService.getAllTasks();
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +313,11 @@ class ArtworkService {
|
||||
* 发送API请求
|
||||
*/
|
||||
async makeRequest(method, endpoint, data = null) {
|
||||
try {
|
||||
if (!this.auth || !this.auth.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.auth.accessToken}`,
|
||||
'Accept-Language': 'en-us',
|
||||
@@ -299,8 +342,21 @@ class ArtworkService {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+569
-196
@@ -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 {
|
||||
@@ -224,89 +653,157 @@ class DownloadService {
|
||||
const taskId = uuidv4();
|
||||
const {
|
||||
type = 'art',
|
||||
filter = 'for_ios',
|
||||
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();
|
||||
|
||||
// 获取已下载的作品ID
|
||||
const downloadedIds = skipExisting ? await this.getDownloadedArtworkIds() : [];
|
||||
const downloadedSet = new Set(downloadedIds);
|
||||
|
||||
// 分页获取作者作品列表
|
||||
let allArtworks = [];
|
||||
let offset = 0;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore && allArtworks.length < limit) {
|
||||
const artworksResult = await this.artistService.getArtistArtworks(artistId, {
|
||||
type,
|
||||
offset: offset,
|
||||
limit: Math.min(pageSize, limit - allArtworks.length)
|
||||
});
|
||||
|
||||
// 批量下载作品
|
||||
const batchResult = await this.downloadMultipleArtworks(artworkIds, {
|
||||
concurrent,
|
||||
size,
|
||||
quality,
|
||||
format
|
||||
});
|
||||
if (!artworksResult.success) {
|
||||
throw new Error(`获取作者作品失败: ${artworksResult.error}`);
|
||||
}
|
||||
|
||||
if (batchResult.success) {
|
||||
const task = this.tasks.get(taskId);
|
||||
task.status = batchResult.data.failed_artworks === 0 ? 'completed' : 'partial';
|
||||
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;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,12 @@
|
||||
<div class="artwork-card" @click="handleClick">
|
||||
<div class="artwork-image">
|
||||
<img
|
||||
:src="artwork.image_urls.medium"
|
||||
:src="getImageUrl(artwork.image_urls.medium)"
|
||||
:alt="artwork.title"
|
||||
@load="imageLoaded = true"
|
||||
@error="imageError = true"
|
||||
:class="{ loaded: imageLoaded, error: imageError }"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<div v-if="!imageLoaded && !imageError" class="image-placeholder">
|
||||
<LoadingSpinner text="加载中..." />
|
||||
@@ -24,9 +25,10 @@
|
||||
<div class="artwork-meta">
|
||||
<div class="artist-info">
|
||||
<img
|
||||
:src="artwork.user.profile_image_urls.medium"
|
||||
:src="getImageUrl(artwork.user.profile_image_urls.medium)"
|
||||
:alt="artwork.user.name"
|
||||
class="artist-avatar"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<span class="artist-name">{{ artwork.user.name }}</span>
|
||||
</div>
|
||||
@@ -85,6 +87,19 @@ const imageError = ref(false);
|
||||
const handleClick = () => {
|
||||
emit('click', props.artwork);
|
||||
};
|
||||
|
||||
// 处理图片URL,通过后端代理
|
||||
const getImageUrl = (originalUrl: string) => {
|
||||
if (!originalUrl) return '';
|
||||
|
||||
// 如果是Pixiv的图片URL,通过后端代理
|
||||
if (originalUrl.includes('i.pximg.net')) {
|
||||
const encodedUrl = encodeURIComponent(originalUrl);
|
||||
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -18,31 +18,31 @@ const router = createRouter({
|
||||
{
|
||||
path: '/search',
|
||||
name: 'search',
|
||||
component: () => import('../views/SearchView.vue'),
|
||||
component: () => import('@/views/SearchView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/artwork/:id',
|
||||
name: 'artwork',
|
||||
component: () => import('../views/ArtworkView.vue'),
|
||||
component: () => import('@/views/ArtworkView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/artist/:id',
|
||||
name: 'artist',
|
||||
component: () => import('../views/ArtistView.vue'),
|
||||
component: () => import('@/views/ArtistView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/downloads',
|
||||
name: 'downloads',
|
||||
component: () => import('../views/DownloadsView.vue'),
|
||||
component: () => import('@/views/DownloadsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/artists',
|
||||
name: 'artists',
|
||||
component: () => import('../views/ArtistsView.vue'),
|
||||
component: () => import('@/views/ArtistsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -75,6 +75,19 @@ class ArtistService {
|
||||
async followArtist(id: number, action: 'follow' | 'unfollow'): Promise<ApiResponse> {
|
||||
return apiService.post(`/api/artist/${id}/follow`, { action });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户关注的作者列表
|
||||
*/
|
||||
async getFollowingArtists(options: { offset?: number; limit?: number } = {}): Promise<ApiResponse<{ artists: Artist[]; total: number }>> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.offset !== undefined) params.append('offset', options.offset.toString());
|
||||
if (options.limit !== undefined) params.append('limit', options.limit.toString());
|
||||
|
||||
const query = params.toString();
|
||||
const url = query ? `/api/artist/following?${query}` : '/api/artist/following';
|
||||
return apiService.get<{ artists: Artist[]; total: number }>(url);
|
||||
}
|
||||
}
|
||||
|
||||
export const artistService = new ArtistService();
|
||||
|
||||
+72
-33
@@ -1,66 +1,105 @@
|
||||
import apiService from './api';
|
||||
import type { ApiResponse, DownloadTask, DownloadParams } from '@/types';
|
||||
|
||||
export interface DownloadArtworkRequest extends DownloadParams {
|
||||
size?: 'original' | 'large' | 'medium' | 'square_medium';
|
||||
quality?: 'high' | 'medium' | 'low';
|
||||
format?: 'auto' | 'jpg' | 'png';
|
||||
}
|
||||
|
||||
export interface DownloadMultipleRequest extends DownloadParams {
|
||||
artworkIds: number[];
|
||||
concurrent?: number;
|
||||
}
|
||||
|
||||
export interface DownloadArtistRequest extends DownloadParams {
|
||||
type?: 'art' | 'manga' | 'novel';
|
||||
filter?: 'for_ios' | 'for_android';
|
||||
limit?: number;
|
||||
}
|
||||
import type { DownloadTask } from '@/types';
|
||||
|
||||
class DownloadService {
|
||||
/**
|
||||
* 下载单个作品
|
||||
*/
|
||||
async downloadArtwork(id: number, params: DownloadArtworkRequest = {}): Promise<ApiResponse<any>> {
|
||||
return apiService.post(`/api/download/artwork/${id}`, params);
|
||||
async downloadArtwork(artworkId: number, options: {
|
||||
size?: string;
|
||||
quality?: string;
|
||||
format?: string;
|
||||
} = {}) {
|
||||
return apiService.post(`/api/download/artwork/${artworkId}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载作品
|
||||
*/
|
||||
async downloadMultipleArtworks(params: DownloadMultipleRequest): Promise<ApiResponse<any>> {
|
||||
return apiService.post('/api/download/artworks', params);
|
||||
async downloadMultipleArtworks(artworkIds: number[], options: {
|
||||
size?: string;
|
||||
quality?: string;
|
||||
format?: string;
|
||||
concurrent?: number;
|
||||
} = {}) {
|
||||
return apiService.post('/api/download/artworks', {
|
||||
artworkIds,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载作者作品
|
||||
*/
|
||||
async downloadArtistArtworks(id: number, params: DownloadArtistRequest = {}): Promise<ApiResponse<any>> {
|
||||
return apiService.post(`/api/download/artist/${id}`, params);
|
||||
async downloadArtistArtworks(artistId: number, options: {
|
||||
type?: string;
|
||||
limit?: number;
|
||||
size?: string;
|
||||
quality?: string;
|
||||
format?: string;
|
||||
} = {}) {
|
||||
return apiService.post(`/api/download/artist/${artistId}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载进度
|
||||
* 获取任务进度
|
||||
*/
|
||||
async getDownloadProgress(taskId: string): Promise<ApiResponse<DownloadTask>> {
|
||||
return apiService.get<DownloadTask>(`/api/download/progress/${taskId}`);
|
||||
async getTaskProgress(taskId: string) {
|
||||
return apiService.get(`/api/download/progress/${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有任务
|
||||
*/
|
||||
async getAllTasks() {
|
||||
return apiService.get('/api/download/tasks');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下载任务
|
||||
*/
|
||||
async cancelDownload(taskId: string): Promise<ApiResponse> {
|
||||
return apiService.delete(`/api/download/cancel/${taskId}`);
|
||||
async cancelTask(taskId: string) {
|
||||
return apiService.post(`/api/download/cancel/${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载历史
|
||||
*/
|
||||
async getDownloadHistory(offset: number = 0, limit: number = 20): Promise<ApiResponse<{ tasks: DownloadTask[]; total: number; offset: number; limit: number }>> {
|
||||
return apiService.get<{ tasks: DownloadTask[]; total: number; offset: number; limit: number }>(`/api/download/history?offset=${offset}&limit=${limit}`);
|
||||
async getDownloadHistory(limit: number = 50, offset: number = 0) {
|
||||
return apiService.get('/api/download/history', {
|
||||
params: { limit, offset }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载的文件列表
|
||||
*/
|
||||
async getDownloadedFiles() {
|
||||
return apiService.get('/api/download/files');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查作品是否已下载
|
||||
*/
|
||||
async checkArtworkDownloaded(artworkId: number) {
|
||||
return apiService.get(`/api/download/check/${artworkId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已下载的作品ID列表
|
||||
*/
|
||||
async getDownloadedArtworkIds() {
|
||||
return apiService.get('/api/download/downloaded-ids');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除下载的文件
|
||||
*/
|
||||
async deleteDownloadedFiles(artist: string, artwork: string) {
|
||||
return apiService.delete('/api/download/files', {
|
||||
data: { artist, artwork }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadService = new DownloadService();
|
||||
export default downloadService;
|
||||
export default new DownloadService();
|
||||
@@ -105,10 +105,14 @@ export interface DownloadTask {
|
||||
failed: number;
|
||||
start_time: string;
|
||||
end_time?: string;
|
||||
error?: string;
|
||||
artwork_id?: number;
|
||||
artist_id?: number;
|
||||
files?: Array<{
|
||||
path: string;
|
||||
url: string;
|
||||
size: string;
|
||||
filename: string;
|
||||
}>;
|
||||
results?: any[];
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
<div class="artist-header">
|
||||
<div class="artist-profile">
|
||||
<img
|
||||
:src="artist.profile_image_urls.medium"
|
||||
:src="getImageUrl(artist.profile_image_urls.medium)"
|
||||
:alt="artist.name"
|
||||
class="artist-avatar"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<div class="artist-info">
|
||||
<h1 class="artist-name">{{ artist.name }}</h1>
|
||||
@@ -64,7 +65,7 @@
|
||||
<div class="section-header">
|
||||
<h2>作品列表</h2>
|
||||
<div class="artwork-filters">
|
||||
<select v-model="artworkType" @change="fetchArtworks" class="filter-select">
|
||||
<select v-model="artworkType" @change="() => fetchArtworks()" class="filter-select">
|
||||
<option value="art">插画</option>
|
||||
<option value="manga">漫画</option>
|
||||
<option value="novel">小说</option>
|
||||
@@ -244,6 +245,19 @@ const handleDownloadAll = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图片URL,通过后端代理
|
||||
const getImageUrl = (originalUrl: string) => {
|
||||
if (!originalUrl) return '';
|
||||
|
||||
// 如果是Pixiv的图片URL,通过后端代理
|
||||
if (originalUrl.includes('i.pximg.net')) {
|
||||
const encodedUrl = encodeURIComponent(originalUrl);
|
||||
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
};
|
||||
|
||||
// 点击作品
|
||||
const handleArtworkClick = (artwork: Artwork) => {
|
||||
router.push(`/artwork/${artwork.id}`);
|
||||
|
||||
@@ -42,9 +42,10 @@
|
||||
>
|
||||
<div class="artist-header">
|
||||
<img
|
||||
:src="artist.profile_image_urls.medium"
|
||||
:src="getImageUrl(artist.profile_image_urls.medium)"
|
||||
:alt="artist.name"
|
||||
class="artist-avatar"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<div class="artist-info">
|
||||
<h3 class="artist-name">{{ artist.name }}</h3>
|
||||
@@ -105,9 +106,10 @@
|
||||
>
|
||||
<div class="artist-header">
|
||||
<img
|
||||
:src="artist.profile_image_urls.medium"
|
||||
:src="getImageUrl(artist.profile_image_urls.medium)"
|
||||
:alt="artist.name"
|
||||
class="artist-avatar"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<div class="artist-info">
|
||||
<h3 class="artist-name">{{ artist.name }}</h3>
|
||||
@@ -181,14 +183,12 @@ const fetchFollowingArtists = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
// 这里需要根据实际API调整
|
||||
// const response = await artistService.getFollowingArtists();
|
||||
// if (response.success && response.data) {
|
||||
// followingArtists.value = response.data.artists;
|
||||
// }
|
||||
|
||||
// 暂时使用模拟数据
|
||||
followingArtists.value = [];
|
||||
const response = await artistService.getFollowingArtists();
|
||||
if (response.success && response.data) {
|
||||
followingArtists.value = response.data.artists;
|
||||
} else {
|
||||
throw new Error(response.error || '获取关注列表失败');
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : '获取关注列表失败';
|
||||
console.error('获取关注列表失败:', err);
|
||||
@@ -292,6 +292,19 @@ const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
// 处理图片URL,通过后端代理
|
||||
const getImageUrl = (originalUrl: string) => {
|
||||
if (!originalUrl) return '';
|
||||
|
||||
// 如果是Pixiv的图片URL,通过后端代理
|
||||
if (originalUrl.includes('i.pximg.net')) {
|
||||
const encodedUrl = encodeURIComponent(originalUrl);
|
||||
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchFollowingArtists();
|
||||
});
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
<div class="artwork-gallery">
|
||||
<div class="main-image">
|
||||
<img
|
||||
:src="currentImageUrl"
|
||||
:src="getImageUrl(currentImageUrl)"
|
||||
:alt="artwork.title"
|
||||
@load="imageLoaded = true"
|
||||
@error="imageError = true"
|
||||
:class="{ loaded: imageLoaded, error: imageError }"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<div v-if="!imageLoaded && !imageError" class="image-placeholder">
|
||||
<LoadingSpinner text="加载中..." />
|
||||
@@ -37,7 +38,7 @@
|
||||
class="thumbnail"
|
||||
:class="{ active: currentPage === index }"
|
||||
>
|
||||
<img :src="page.image_urls.square_medium" :alt="`第 ${index + 1} 页`" />
|
||||
<img :src="getImageUrl(page.image_urls.square_medium)" :alt="`第 ${index + 1} 页`" crossorigin="anonymous" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,9 +60,10 @@
|
||||
<!-- 作者信息 -->
|
||||
<div class="artist-info">
|
||||
<img
|
||||
:src="artwork.user.profile_image_urls.medium"
|
||||
:src="getImageUrl(artwork.user.profile_image_urls.medium)"
|
||||
:alt="artwork.user.name"
|
||||
class="artist-avatar"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<div class="artist-details">
|
||||
<h3 class="artist-name">{{ artwork.user.name }}</h3>
|
||||
@@ -223,6 +225,19 @@ const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN');
|
||||
};
|
||||
|
||||
// 处理图片URL,通过后端代理
|
||||
const getImageUrl = (originalUrl: string) => {
|
||||
if (!originalUrl) return '';
|
||||
|
||||
// 如果是Pixiv的图片URL,通过后端代理
|
||||
if (originalUrl.includes('i.pximg.net')) {
|
||||
const encodedUrl = encodeURIComponent(originalUrl);
|
||||
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
};
|
||||
|
||||
// 清除错误
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
|
||||
+480
-362
File diff suppressed because it is too large
Load Diff
+126
-5
@@ -5,7 +5,33 @@
|
||||
<h1 class="page-title">搜索作品</h1>
|
||||
|
||||
<div class="search-form">
|
||||
<div class="search-input-group">
|
||||
<!-- 搜索类型选择 -->
|
||||
<div class="search-type-tabs">
|
||||
<button
|
||||
@click="searchMode = 'keyword'"
|
||||
class="tab-btn"
|
||||
:class="{ active: searchMode === 'keyword' }"
|
||||
>
|
||||
关键词搜索
|
||||
</button>
|
||||
<button
|
||||
@click="searchMode = 'artwork'"
|
||||
class="tab-btn"
|
||||
:class="{ active: searchMode === 'artwork' }"
|
||||
>
|
||||
作品ID
|
||||
</button>
|
||||
<button
|
||||
@click="searchMode = 'artist'"
|
||||
class="tab-btn"
|
||||
:class="{ active: searchMode === 'artist' }"
|
||||
>
|
||||
作者ID
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 关键词搜索 -->
|
||||
<div v-if="searchMode === 'keyword'" class="search-input-group">
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
type="text"
|
||||
@@ -20,6 +46,34 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 作品ID搜索 -->
|
||||
<div v-if="searchMode === 'artwork'" class="search-input-group">
|
||||
<input
|
||||
v-model="artworkId"
|
||||
type="text"
|
||||
placeholder="输入作品ID..."
|
||||
class="search-input"
|
||||
@keyup.enter="handleArtworkSearch"
|
||||
/>
|
||||
<button @click="handleArtworkSearch" class="search-btn" :disabled="loading">
|
||||
查看作品
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 作者ID搜索 -->
|
||||
<div v-if="searchMode === 'artist'" class="search-input-group">
|
||||
<input
|
||||
v-model="artistId"
|
||||
type="text"
|
||||
placeholder="输入作者ID..."
|
||||
class="search-input"
|
||||
@keyup.enter="handleArtistSearch"
|
||||
/>
|
||||
<button @click="handleArtistSearch" class="search-btn" :disabled="loading">
|
||||
查看作者
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="search-filters">
|
||||
<select v-model="searchType" class="filter-select">
|
||||
<option value="all">全部类型</option>
|
||||
@@ -111,6 +165,11 @@
|
||||
|
||||
// 搜索状态
|
||||
const searchKeyword = ref('');
|
||||
const searchMode = ref<'keyword' | 'artwork' | 'artist'>('keyword');
|
||||
const artworkId = ref('');
|
||||
const artistId = ref('');
|
||||
|
||||
// 关键词搜索参数
|
||||
const searchType = ref<'all' | 'art' | 'manga' | 'novel'>('all');
|
||||
const searchSort = ref<'date_desc' | 'date_asc' | 'popular_desc'>('date_desc');
|
||||
const searchDuration = ref<'all' | 'within_last_day' | 'within_last_week' | 'within_last_month'>('all');
|
||||
@@ -197,9 +256,43 @@
|
||||
router.push(`/artwork/${artwork.id}`);
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
// 作品ID搜索
|
||||
const handleArtworkSearch = () => {
|
||||
const idStr = artworkId.value?.toString().trim();
|
||||
if (!idStr) {
|
||||
error.value = '请输入作品ID';
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseInt(idStr);
|
||||
if (isNaN(id)) {
|
||||
error.value = '请输入有效的作品ID';
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/artwork/${id}`);
|
||||
};
|
||||
|
||||
// 作者ID搜索
|
||||
const handleArtistSearch = () => {
|
||||
const idStr = artistId.value?.toString().trim();
|
||||
if (!idStr) {
|
||||
error.value = '请输入作者ID';
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseInt(idStr);
|
||||
if (isNaN(id)) {
|
||||
error.value = '请输入有效的作者ID';
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/artist/${id}`);
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -233,10 +326,38 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
.search-type-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
background: white;
|
||||
color: #6b7280;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user