tst / server.js
tachibanaa710's picture
Update server.js
f667d48 verified
const express = require('express');
const axios = require('axios');
const cheerio = require('cheerio');
const cors = require('cors');
const app = express();
const PORT = 7860;
const HOST = '0.0.0.0';
app.use(cors());
app.use(express.json());
function extractPixivId(url) {
const patterns = [
/pixiv\.net\/(?:en\/)?artworks\/(\d+)/,
/pixiv\.net\/member_illust\.php\?.*illust_id=(\d+)/,
/pixiv\.net\/(?:en\/)?users\/\d+\/artworks\/(\d+)/
];
for (let pattern of patterns) {
const match = url.match(pattern);
if (match) return match[1];
}
return null;
}
async function getPixivMetadata(artworkId, req) {
try {
const apiUrl = `https://www.pixiv.net/ajax/illust/${artworkId}`;
const pageUrl = `https://www.pixiv.net/en/artworks/${artworkId}`;
// Set headers to mimic a browser request
const headers = {
'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',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://www.pixiv.net/'
};
try {
const response = await axios.get(apiUrl, { headers });
if (response.data && response.data.body) {
const data = response.data.body;
const baseUrl = req.protocol + '://' + req.get('host');
return {
id: data.id,
title: data.title,
description: data.description,
artist: {
id: data.userId,
name: data.userName,
account: data.userAccount
},
tags: data.tags ? data.tags.tags.map(tag => ({
tag: tag.tag,
translation: tag.translation ? tag.translation.en : null
})) : [],
images: {
thumbnail: data.urls ? data.urls.thumb : null,
small: data.urls ? data.urls.small : null,
regular: data.urls ? data.urls.regular : null,
original: data.urls ? data.urls.original : null
},
proxiedImages: {
thumbnail: data.urls?.thumb ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(data.urls.thumb)}` : null,
small: data.urls?.small ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(data.urls.small)}` : null,
regular: data.urls?.regular ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(data.urls.regular)}` : null,
original: data.urls?.original ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(data.urls.original)}` : null
},
stats: {
views: data.viewCount,
bookmarks: data.bookmarkCount,
likes: data.likeCount,
comments: data.commentCount
},
pageCount: data.pageCount,
width: data.width,
height: data.height,
createDate: data.createDate,
uploadDate: data.uploadDate,
type: data.illustType === 0 ? 'illustration' : data.illustType === 1 ? 'manga' : 'ugoira',
isR18: data.xRestrict === 1,
isAI: data.aiType === 2
};
}
} catch (apiError) {
console.log('API endpoint failed, trying page scraping...');
}
const pageResponse = await axios.get(pageUrl, { headers });
const $ = cheerio.load(pageResponse.data);
let metadata = null;
$('script').each((i, elem) => {
const text = $(elem).html();
if (text && text.includes('meta-preload-data')) {
try {
const match = text.match(/{"timestamp".*?}(?=<\/script>)/);
if (match) {
const data = JSON.parse(match[0]);
if (data.illust && data.illust[artworkId]) {
const illust = data.illust[artworkId];
const baseUrl = req.protocol + '://' + req.get('host');
metadata = {
id: illust.id,
title: illust.title,
description: illust.description,
artist: {
id: illust.userId,
name: illust.userName,
account: illust.userAccount || null
},
tags: illust.tags ? illust.tags.tags.map(tag => ({
tag: tag.tag,
translation: tag.translation ? tag.translation.en : null
})) : [],
images: {
thumbnail: illust.urls ? illust.urls.thumb : null,
small: illust.urls ? illust.urls.small : null,
regular: illust.urls ? illust.urls.regular : null,
original: illust.urls ? illust.urls.original : null
},
proxiedImages: {
thumbnail: illust.urls?.thumb ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(illust.urls.thumb)}` : null,
small: illust.urls?.small ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(illust.urls.small)}` : null,
regular: illust.urls?.regular ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(illust.urls.regular)}` : null,
original: illust.urls?.original ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(illust.urls.original)}` : null
},
stats: {
views: illust.viewCount || 0,
bookmarks: illust.bookmarkCount || 0,
likes: illust.likeCount || 0,
comments: illust.commentCount || 0
},
pageCount: illust.pageCount || 1,
width: illust.width,
height: illust.height,
createDate: illust.createDate,
uploadDate: illust.uploadDate,
type: illust.illustType === 0 ? 'illustration' : illust.illustType === 1 ? 'manga' : 'ugoira',
isR18: illust.xRestrict === 1,
isAI: illust.aiType === 2
};
}
}
} catch (e) {
}
}
});
if (metadata) {
return metadata;
}
const baseUrl = req.protocol + '://' + req.get('host');
const ogImage = $('meta[property="og:image"]').attr('content');
return {
id: artworkId,
title: $('meta[property="og:title"]').attr('content') || 'Unknown',
description: $('meta[property="og:description"]').attr('content') || '',
artist: {
name: $('meta[name="twitter:creator"]').attr('content') || 'Unknown'
},
images: {
thumbnail: ogImage || null
},
proxiedImages: {
thumbnail: ogImage ? `${baseUrl}/api/image-proxy?url=${encodeURIComponent(ogImage)}` : null
},
tags: [],
stats: {},
pageCount: 1,
type: 'illustration'
};
} catch (error) {
throw new Error(`Failed to fetch metadata: ${error.message}`);
}
}
app.get('/api/image-proxy', async (req, res) => {
try {
const { url } = req.query;
if (!url) {
return res.status(400).json({
error: 'Please provide an image URL',
example: '/api/image-proxy?url=https://i.pximg.net/...'
});
}
if (!url.includes('pximg.net')) {
return res.status(400).json({
error: 'Only Pixiv image URLs are supported'
});
}
const headers = {
'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',
'Referer': 'https://www.pixiv.net/',
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9'
};
const response = await axios.get(url, {
headers,
responseType: 'stream',
timeout: 30000
});
const contentType = response.headers['content-type'] || 'image/jpeg';
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
response.data.pipe(res);
} catch (error) {
if (error.response && error.response.status === 403) {
res.status(403).json({
error: 'Access forbidden - image may be restricted or URL invalid'
});
} else {
res.status(500).json({
error: `Failed to fetch image: ${error.message}`
});
}
}
});
app.get('/api/pixiv', async (req, res) => {
try {
const { url, id } = req.query;
if (!url && !id) {
return res.status(400).json({
error: 'Please provide either a Pixiv URL or artwork ID',
example: '/api/pixiv?url=https://www.pixiv.net/en/artworks/123456789'
});
}
let artworkId = id;
if (url) {
artworkId = extractPixivId(url);
if (!artworkId) {
return res.status(400).json({
error: 'Invalid Pixiv URL format',
supportedFormats: [
'https://www.pixiv.net/en/artworks/{id}',
'https://www.pixiv.net/artworks/{id}',
'https://www.pixiv.net/member_illust.php?illust_id={id}'
]
});
}
}
const metadata = await getPixivMetadata(artworkId, req);
res.json({
success: true,
data: metadata
});
} catch (error) {
res.status(500).json({
error: error.message,
success: false
});
}
});
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.get('/', (req, res) => {
res.json({
name: 'Pixiv Metadata API',
version: '1.0.0',
endpoints: {
'/api/pixiv': {
method: 'GET',
description: 'Get metadata for a Pixiv artwork',
parameters: {
url: 'Pixiv artwork URL (optional if id is provided)',
id: 'Pixiv artwork ID (optional if url is provided)'
},
example: '/api/pixiv?url=https://www.pixiv.net/en/artworks/123456789'
},
'/api/image-proxy': {
method: 'GET',
description: 'Proxy Pixiv images to bypass hotlink protection',
parameters: {
url: 'Pixiv image URL (pximg.net)'
},
example: '/api/image-proxy?url=https://i.pximg.net/...'
},
'/health': {
method: 'GET',
description: 'Health check endpoint'
}
}
});
});
app.listen(PORT, HOST, () => {
console.log(`Running on http://${HOST}:${PORT}`);
});
module.exports = app;