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-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}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Эндпоинт для проксирования изображений
|
|
|
|
|
|
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: 'Запрещенный домен' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Запрашиваем изображение
|
2025-11-21 01:28:48 +00:00
|
|
|
|
// Для 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}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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: [] });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Проверка на наличие данных
|
|
|
|
|
|
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'
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
// Проверка на массив
|
|
|
|
|
|
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
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
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-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;
|
|
|
|
|
|
|