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 для прокси
|
|
|
|
|
|
router.get('/proxy/:encodedUrl', proxyLimiter, 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;
|
|
|
|
|
|
|