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;