Spaces:
Paused
Paused
| 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; |