const express = require('express'); const router = express.Router(); const axios = require('axios'); const { authenticate } = require('../middleware/auth'); const config = require('../config'); // e621 требует описательный User-Agent с контактами const E621_USER_AGENT = 'NakamaApp/1.0 (by glpshchn00 on e621; Telegram: @glpshchn00)'; const CACHE_TTL_MS = 60 * 1000; // 1 минута const searchCache = new Map(); function getCacheKey(source, params) { return `${source}:${params.query}:${params.limit || ''}:${params.page || ''}`; } function getFromCache(key) { const entry = searchCache.get(key); if (!entry) return null; if (entry.expires < Date.now()) { searchCache.delete(key); return null; } return entry.data; } function setCache(key, data) { if (searchCache.size > 200) { // Удалить устаревшие записи, если превышен лимит for (const [cacheKey, entry] of searchCache.entries()) { if (entry.expires < Date.now()) { searchCache.delete(cacheKey); } } if (searchCache.size > 200) { const oldestKey = searchCache.keys().next().value; if (oldestKey) { searchCache.delete(oldestKey); } } } searchCache.set(key, { data, expires: Date.now() + CACHE_TTL_MS }); } // Функция для создания прокси URL function createProxyUrl(originalUrl) { if (!originalUrl) return null; // Кодируем URL в base64 const encodedUrl = Buffer.from(originalUrl).toString('base64'); return `/api/search/proxy/${encodedUrl}`; } // Эндпоинт для проксирования изображений router.get('/proxy/:encodedUrl', async (req, res) => { try { const { encodedUrl } = req.params; // Декодируем URL const originalUrl = Buffer.from(encodedUrl, 'base64').toString('utf-8'); // Проверяем, что URL от разрешенных доменов const allowedDomains = [ 'e621.net', 'static1.e621.net', 'gelbooru.com', 'img3.gelbooru.com', 'img2.gelbooru.com', 'img1.gelbooru.com', 'simg3.gelbooru.com', 'simg4.gelbooru.com' ]; const urlObj = new URL(originalUrl); if (!allowedDomains.some(domain => urlObj.hostname.includes(domain))) { return res.status(403).json({ error: 'Запрещенный домен' }); } // Запрашиваем изображение // Для e621 добавляем авторизацию const headers = { 'User-Agent': E621_USER_AGENT, 'Referer': urlObj.origin }; // Если это e621, добавляем авторизацию if (urlObj.hostname.includes('e621.net')) { const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64'); headers['Authorization'] = `Basic ${auth}`; } const response = await axios.get(originalUrl, { responseType: 'stream', headers, timeout: 30000 // 30 секунд таймаут }); // Копируем заголовки res.setHeader('Content-Type', response.headers['content-type']); res.setHeader('Cache-Control', 'public, max-age=86400'); // Кешируем на 24 часа if (response.headers['content-length']) { res.setHeader('Content-Length', response.headers['content-length']); } // Стримим изображение response.data.pipe(res); } catch (error) { console.error('Ошибка проксирования изображения:', error.message); res.status(500).json({ error: 'Ошибка загрузки изображения' }); } }); // e621 API поиск router.get('/furry', authenticate, async (req, res) => { try { const { query, limit = 320, page = 1 } = req.query; // e621 поддерживает до 320 постов на страницу if (!query) { return res.status(400).json({ error: 'Параметр query обязателен' }); } const cacheKey = getCacheKey('e621', { query: query.trim(), limit, page }); const cached = getFromCache(cacheKey); if (cached) { return res.json(cached); } // Поддержка множественных тегов через пробел // e621 API автоматически обрабатывает теги через пробел в параметре tags try { // Базовая авторизация для e621 API const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64'); const response = await axios.get('https://e621.net/posts.json', { params: { tags: query.trim(), // Множественные теги через пробел limit: Math.min(parseInt(limit) || 320, 320), // Максимум 320 page: parseInt(page) || 1 }, headers: { 'User-Agent': E621_USER_AGENT, 'Authorization': `Basic ${auth}` }, timeout: 30000, validateStatus: (status) => status < 500 // Не бросать ошибку для 429 }); // Обработка 429 (Too Many Requests) if (response.status === 429) { console.warn('⚠️ e621 rate limit (429)'); return res.json({ posts: [] }); } // Проверка на наличие данных if (!response.data || !response.data.posts || !Array.isArray(response.data.posts)) { console.warn('⚠️ e621 вернул неверный формат данных'); return res.json({ posts: [] }); } const posts = response.data.posts.map(post => ({ id: post.id, url: createProxyUrl(post.file.url), preview: createProxyUrl(post.preview.url), tags: post.tags.general, rating: post.rating, score: post.score.total, source: 'e621' })); const payload = { posts }; setCache(cacheKey, payload); return res.json(payload); } catch (error) { // Обработка 429 ошибок if (error.response && error.response.status === 429) { console.warn('⚠️ e621 rate limit (429)'); return res.json({ posts: [] }); } console.error('Ошибка e621 API:', error.message); return res.json({ posts: [] }); // Возвращаем пустой массив вместо ошибки } } catch (error) { console.error('Ошибка поиска e621:', error); return res.json({ posts: [] }); // Возвращаем пустой массив вместо ошибки } }); // Gelbooru API поиск router.get('/anime', authenticate, async (req, res) => { try { const { query, limit = 320, page = 1 } = req.query; // Gelbooru поддерживает до 320 постов на страницу if (!query) { return res.status(400).json({ error: 'Параметр query обязателен' }); } const cacheKey = getCacheKey('gelbooru', { query: query.trim(), limit, page }); const cached = getFromCache(cacheKey); if (cached) { return res.json(cached); } try { const response = await axios.get('https://gelbooru.com/index.php', { params: { page: 'dapi', s: 'post', q: 'index', json: 1, tags: query.trim(), // Множественные теги через пробел limit: Math.min(parseInt(limit) || 320, 320), // Максимум 320 pid: parseInt(page) || 1, api_key: config.gelbooruApiKey, user_id: config.gelbooruUserId }, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, timeout: 30000, validateStatus: (status) => status < 500 // Не бросать ошибку для 429 }); // Обработка 429 (Too Many Requests) if (response.status === 429) { console.warn('⚠️ Gelbooru rate limit (429)'); return res.json({ posts: [] }); } // Обработка разных форматов ответа Gelbooru let postsData = []; if (Array.isArray(response.data)) { postsData = response.data; } else if (response.data && response.data.post) { postsData = Array.isArray(response.data.post) ? response.data.post : [response.data.post]; } else if (response.data && Array.isArray(response.data)) { postsData = response.data; } const posts = postsData.map(post => ({ id: post.id, url: createProxyUrl(post.file_url), preview: createProxyUrl(post.preview_url || post.thumbnail_url || post.file_url), tags: post.tags ? (typeof post.tags === 'string' ? post.tags.split(' ') : post.tags) : [], rating: post.rating || 'unknown', score: post.score || 0, source: 'gelbooru' })); const payload = { posts }; setCache(cacheKey, payload); return res.json(payload); } catch (error) { // Обработка 429 ошибок if (error.response && error.response.status === 429) { console.warn('⚠️ Gelbooru rate limit (429)'); return res.json({ posts: [] }); } console.error('Ошибка Gelbooru API:', error.message); if (error.response) { console.error('Gelbooru ответ:', error.response.status, error.response.data); } return res.json({ posts: [] }); // Возвращаем пустой массив вместо ошибки } } catch (error) { console.error('Ошибка поиска Gelbooru:', error); return res.json({ posts: [] }); // Возвращаем пустой массив вместо ошибки } }); // Автокомплит тегов для e621 router.get('/furry/tags', authenticate, async (req, res) => { try { const { query } = req.query; if (!query || query.length < 2) { return res.json({ tags: [] }); } const cacheKey = getCacheKey('e621-tags', { query: query.trim().toLowerCase() }); const cached = getFromCache(cacheKey); if (cached) { return res.json(cached); } try { // Базовая авторизация для e621 API const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64'); const response = await axios.get('https://e621.net/tags.json', { params: { 'search[name_matches]': `${query}*`, 'search[order]': 'count', limit: 10 }, headers: { 'User-Agent': E621_USER_AGENT, 'Authorization': `Basic ${auth}` }, timeout: 10000, validateStatus: (status) => status < 500 // Не бросать ошибку для 429 }); // Обработка 429 (Too Many Requests) if (response.status === 429) { console.warn('⚠️ e621 rate limit (429)'); return res.json({ tags: [] }); } // Проверка на массив if (!response.data || !Array.isArray(response.data)) { console.warn('⚠️ e621 вернул не массив:', typeof response.data); return res.json({ tags: [] }); } const tags = response.data.map(tag => ({ name: tag.name, count: tag.post_count })); const payload = { tags }; setCache(cacheKey, payload); return res.json(payload); } catch (error) { // Обработка 429 ошибок if (error.response && error.response.status === 429) { console.warn('⚠️ e621 rate limit (429)'); return res.json({ tags: [] }); } console.error('Ошибка получения тегов e621:', error.message); return res.json({ tags: [] }); // Возвращаем пустой массив вместо ошибки } } catch (error) { console.error('Ошибка получения тегов:', error); return res.json({ tags: [] }); // Возвращаем пустой массив вместо ошибки } }); // Автокомплит тегов для Gelbooru router.get('/anime/tags', authenticate, async (req, res) => { try { const { query } = req.query; if (!query || query.length < 2) { return res.json({ tags: [] }); } try { const response = await axios.get('https://gelbooru.com/index.php', { params: { page: 'dapi', s: 'tag', q: 'index', json: 1, name_pattern: `${query}%`, orderby: 'count', limit: 10, api_key: config.gelbooruApiKey, user_id: config.gelbooruUserId }, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, timeout: 30000, validateStatus: (status) => status < 500 // Не бросать ошибку для 429 }); // Обработка 429 (Too Many Requests) if (response.status === 429) { console.warn('⚠️ Gelbooru rate limit (429)'); return res.json({ tags: [] }); } // Обработка разных форматов ответа Gelbooru let tagsData = []; if (Array.isArray(response.data)) { tagsData = response.data; } else if (response.data && response.data.tag) { tagsData = Array.isArray(response.data.tag) ? response.data.tag : [response.data.tag]; } else if (response.data && Array.isArray(response.data)) { tagsData = response.data; } // Проверка на массив перед map if (!Array.isArray(tagsData)) { console.warn('⚠️ Gelbooru вернул не массив тегов'); return res.json({ tags: [] }); } const tags = tagsData.map(tag => ({ name: tag.name || tag.tag || '', count: tag.count || tag.post_count || 0 })).filter(tag => tag.name); const payload = { tags }; setCache(cacheKey, payload); return res.json(payload); } catch (error) { // Обработка 429 ошибок if (error.response && error.response.status === 429) { console.warn('⚠️ Gelbooru rate limit (429)'); return res.json({ tags: [] }); } console.error('Ошибка получения тегов Gelbooru:', error.message); if (error.response) { console.error('Gelbooru ответ:', error.response.status, error.response.data); } // В случае ошибки возвращаем пустой массив вместо ошибки return res.json({ tags: [] }); } } catch (error) { console.error('Ошибка получения тегов Gelbooru:', error); // В случае ошибки возвращаем пустой массив вместо ошибки return res.json({ tags: [] }); } }); module.exports = router;