nakama/backend/routes/search.js

537 lines
20 KiB
JavaScript
Raw Normal View History

2025-11-03 20:35:01 +00:00
const express = require('express');
const router = express.Router();
const axios = require('axios');
const { authenticate } = require('../middleware/auth');
2025-12-01 01:11:27 +00:00
const { proxyLimiter } = require('../middleware/rateLimiter');
2025-11-03 22:51:17 +00:00
const config = require('../config');
2025-11-03 20:35:01 +00:00
2025-11-21 01:28:48 +00:00
// e621 требует описательный User-Agent с контактами
const E621_USER_AGENT = 'NakamaApp/1.0 (by glpshchn00 on e621; Telegram: @glpshchn00)';
2025-11-10 20:13:22 +00:00
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
});
}
2025-11-03 20:35:01 +00:00
// Функция для создания прокси URL
function createProxyUrl(originalUrl) {
if (!originalUrl) return null;
// Кодируем URL в base64
const encodedUrl = Buffer.from(originalUrl).toString('base64');
return `/api/search/proxy/${encodedUrl}`;
}
// Эндпоинт для проксирования изображений
2025-12-01 01:11:27 +00:00
// Используем более мягкий rate limiter для прокси
2025-12-04 20:47:07 +00:00
router.get('/proxy/:encodedUrl', async (req, res) => {
2025-11-03 20:35:01 +00:00
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: 'Запрещенный домен' });
}
// Запрашиваем изображение
2025-11-21 01:28:48 +00:00
// Для e621 добавляем авторизацию
const headers = {
'User-Agent': E621_USER_AGENT,
'Referer': urlObj.origin
};
2025-12-01 01:18:46 +00:00
// Если это e621, добавляем авторизацию (если есть учетные данные)
if (urlObj.hostname.includes('e621.net') && config.e621Username && config.e621ApiKey) {
try {
2025-12-01 14:26:18 +00:00
const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
2025-12-01 01:18:46 +00:00
} catch (error) {
console.warn('⚠️ Ошибка создания Basic auth для e621:', error.message);
// Продолжаем без авторизации
}
2025-11-21 01:28:48 +00:00
}
2025-11-03 20:35:01 +00:00
const response = await axios.get(originalUrl, {
responseType: 'stream',
2025-11-21 01:28:48 +00:00
headers,
2025-11-03 20:35:01 +00:00
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 {
2025-11-04 21:51:05 +00:00
const { query, limit = 320, page = 1 } = req.query; // e621 поддерживает до 320 постов на страницу
2025-11-03 20:35:01 +00:00
if (!query) {
return res.status(400).json({ error: 'Параметр query обязателен' });
}
2025-11-10 20:13:22 +00:00
const cacheKey = getCacheKey('e621', { query: query.trim(), limit, page });
const cached = getFromCache(cacheKey);
if (cached) {
return res.json(cached);
}
2025-11-04 21:51:05 +00:00
// Поддержка множественных тегов через пробел
// e621 API автоматически обрабатывает теги через пробел в параметре tags
2025-11-03 20:35:01 +00:00
2025-11-04 21:51:05 +00:00
try {
2025-11-21 01:28:48 +00:00
// Базовая авторизация для e621 API
const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64');
2025-11-04 21:51:05 +00:00
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: {
2025-11-21 01:28:48 +00:00
'User-Agent': E621_USER_AGENT,
'Authorization': `Basic ${auth}`
2025-11-04 21:51:05 +00:00
},
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: [] });
}
2025-12-01 01:18:46 +00:00
// Обработка ошибок аутентификации
if (response.data && response.data.success === false) {
if (response.data.message && response.data.message.includes('Authentication')) {
console.warn('⚠️ e621 ошибка аутентификации, пробуем без авторизации:', response.data.message);
// Пробуем запрос без авторизации (для публичных данных это должно работать)
try {
const publicResponse = await axios.get('https://e621.net/posts.json', {
params: {
tags: query.trim(),
limit: Math.min(parseInt(limit) || 320, 320),
page: parseInt(page) || 1
},
headers: {
'User-Agent': E621_USER_AGENT
},
timeout: 30000,
validateStatus: (status) => status < 500
});
if (publicResponse.status === 429) {
return res.json({ posts: [] });
}
// Используем данные из публичного запроса
response.data = publicResponse.data;
} catch (publicError) {
console.error('⚠️ e621 публичный запрос тоже не удался:', publicError.message);
return res.json({ posts: [] });
}
} else {
console.warn('⚠️ e621 вернул ошибку:', response.data.message);
return res.json({ posts: [] });
}
}
2025-12-01 01:11:27 +00:00
// Проверка на наличие данных (e621 может возвращать массив напрямую или объект с posts)
let postsData = null;
if (Array.isArray(response.data)) {
postsData = response.data;
} else if (response.data && Array.isArray(response.data.posts)) {
postsData = response.data.posts;
} else if (response.data && Array.isArray(response.data.data)) {
postsData = response.data.data;
} else {
console.warn('⚠️ e621 вернул неверный формат данных для постов:', {
type: typeof response.data,
keys: response.data ? Object.keys(response.data) : null,
hasPosts: !!(response.data && response.data.posts),
isArray: Array.isArray(response.data)
});
2025-11-04 21:51:05 +00:00
return res.json({ posts: [] });
}
2025-12-01 01:11:27 +00:00
const posts = postsData
.filter(post => post && post.file && post.file.url) // Фильтруем посты без URL
.map(post => ({
2025-12-01 14:26:18 +00:00
id: post.id,
url: createProxyUrl(post.file.url),
2025-12-01 01:11:27 +00:00
preview: post.preview && post.preview.url ? createProxyUrl(post.preview.url) : null,
tags: post.tags && post.tags.general ? post.tags.general : [],
rating: post.rating || 'q',
score: post.score && post.score.total ? post.score.total : 0,
2025-12-01 14:26:18 +00:00
source: 'e621'
}));
2025-11-04 21:51:05 +00:00
2025-11-10 20:13:22 +00:00
const payload = { posts };
setCache(cacheKey, payload);
return res.json(payload);
2025-11-04 21:51:05 +00:00
} 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: [] }); // Возвращаем пустой массив вместо ошибки
}
2025-11-03 20:35:01 +00:00
} catch (error) {
2025-11-04 21:51:05 +00:00
console.error('Ошибка поиска e621:', error);
return res.json({ posts: [] }); // Возвращаем пустой массив вместо ошибки
2025-11-03 20:35:01 +00:00
}
});
// Gelbooru API поиск
router.get('/anime', authenticate, async (req, res) => {
try {
2025-11-04 21:51:05 +00:00
const { query, limit = 320, page = 1 } = req.query; // Gelbooru поддерживает до 320 постов на страницу
2025-11-03 20:35:01 +00:00
if (!query) {
return res.status(400).json({ error: 'Параметр query обязателен' });
}
2025-11-10 20:13:22 +00:00
const cacheKey = getCacheKey('gelbooru', { query: query.trim(), limit, page });
const cached = getFromCache(cacheKey);
if (cached) {
return res.json(cached);
}
2025-11-04 21:51:05 +00:00
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'
}));
2025-11-10 20:13:22 +00:00
const payload = { posts };
setCache(cacheKey, payload);
return res.json(payload);
2025-11-04 21:51:05 +00:00
} 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: [] }); // Возвращаем пустой массив вместо ошибки
2025-11-03 22:41:34 +00:00
}
2025-11-03 20:35:01 +00:00
} catch (error) {
2025-11-04 21:51:05 +00:00
console.error('Ошибка поиска Gelbooru:', error);
return res.json({ posts: [] }); // Возвращаем пустой массив вместо ошибки
2025-11-03 20:35:01 +00:00
}
});
// Автокомплит тегов для e621
router.get('/furry/tags', authenticate, async (req, res) => {
try {
const { query } = req.query;
if (!query || query.length < 2) {
return res.json({ tags: [] });
}
2025-11-10 20:13:22 +00:00
const cacheKey = getCacheKey('e621-tags', { query: query.trim().toLowerCase() });
const cached = getFromCache(cacheKey);
if (cached) {
return res.json(cached);
}
2025-11-03 20:35:01 +00:00
2025-11-04 21:51:05 +00:00
try {
2025-11-21 01:28:48 +00:00
// Базовая авторизация для e621 API
const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64');
2025-11-04 21:51:05 +00:00
const response = await axios.get('https://e621.net/tags.json', {
params: {
'search[name_matches]': `${query}*`,
'search[order]': 'count',
limit: 10
},
headers: {
2025-11-21 01:28:48 +00:00
'User-Agent': E621_USER_AGENT,
'Authorization': `Basic ${auth}`
2025-11-04 21:51:05 +00:00
},
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: [] });
2025-11-03 20:35:01 +00:00
}
2025-11-04 21:51:05 +00:00
2025-12-01 01:18:46 +00:00
// Обработка ошибок аутентификации
if (response.data && response.data.success === false) {
if (response.data.message && response.data.message.includes('Authentication')) {
console.warn('⚠️ e621 ошибка аутентификации, пробуем без авторизации:', response.data.message);
// Пробуем запрос без авторизации (для публичных данных это должно работать)
try {
const publicResponse = await axios.get('https://e621.net/tags.json', {
params: {
'search[name_matches]': `${query}*`,
'search[order]': 'count',
limit: 10
},
headers: {
'User-Agent': E621_USER_AGENT
},
timeout: 10000,
validateStatus: (status) => status < 500
});
if (publicResponse.status === 429) {
return res.json({ tags: [] });
}
// Используем данные из публичного запроса
response.data = publicResponse.data;
} catch (publicError) {
console.error('⚠️ e621 публичный запрос тоже не удался:', publicError.message);
return res.json({ tags: [] });
}
} else {
console.warn('⚠️ e621 вернул ошибку:', response.data.message);
return res.json({ tags: [] });
}
}
2025-12-01 01:11:27 +00:00
// Проверка на массив (e621 может возвращать массив напрямую или объект с массивом)
let tagsData = null;
if (Array.isArray(response.data)) {
tagsData = response.data;
} else if (response.data && Array.isArray(response.data.tags)) {
tagsData = response.data.tags;
} else if (response.data && Array.isArray(response.data.data)) {
tagsData = response.data.data;
} else {
console.warn('⚠️ e621 вернул неверный формат данных для тегов:', {
type: typeof response.data,
keys: response.data ? Object.keys(response.data) : null,
data: response.data
});
2025-11-04 21:51:05 +00:00
return res.json({ tags: [] });
}
2025-12-01 01:11:27 +00:00
const tags = tagsData.map(tag => ({
2025-11-04 21:51:05 +00:00
name: tag.name,
2025-12-01 01:11:27 +00:00
count: tag.post_count || tag.count || 0
2025-11-04 21:51:05 +00:00
}));
2025-11-10 20:13:22 +00:00
const payload = { tags };
setCache(cacheKey, payload);
return res.json(payload);
2025-11-04 21:51:05 +00:00
} 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: [] }); // Возвращаем пустой массив вместо ошибки
}
2025-11-03 20:35:01 +00:00
} catch (error) {
console.error('Ошибка получения тегов:', error);
2025-11-04 21:51:05 +00:00
return res.json({ tags: [] }); // Возвращаем пустой массив вместо ошибки
2025-11-03 20:35:01 +00:00
}
});
// Автокомплит тегов для Gelbooru
router.get('/anime/tags', authenticate, async (req, res) => {
try {
const { query } = req.query;
if (!query || query.length < 2) {
return res.json({ tags: [] });
}
2025-12-01 01:11:27 +00:00
const cacheKey = getCacheKey('gelbooru-tags', { query: query.trim().toLowerCase() });
const cached = getFromCache(cacheKey);
if (cached) {
return res.json(cached);
}
2025-11-03 20:35:01 +00:00
2025-11-04 21:51:05 +00:00
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);
2025-11-10 20:13:22 +00:00
const payload = { tags };
setCache(cacheKey, payload);
return res.json(payload);
2025-11-04 21:51:05 +00:00
} 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: [] });
2025-11-03 22:41:34 +00:00
}
2025-11-03 20:35:01 +00:00
} catch (error) {
2025-11-04 21:51:05 +00:00
console.error('Ошибка получения тегов Gelbooru:', error);
2025-11-03 22:41:34 +00:00
// В случае ошибки возвращаем пустой массив вместо ошибки
2025-11-04 21:51:05 +00:00
return res.json({ tags: [] });
2025-11-03 20:35:01 +00:00
}
});
module.exports = router;