Refactor favorites management to use Redis

- Removed local favorites.json file and related file handling in the code.
- Implemented Redis caching for managing favorite articles, including methods to set, get, and check favorites.
- Updated TelegramManager and server API to interact with Redis for favorite operations.
- Added search functionality for articles in Redis, enhancing user experience.
- Adjusted frontend components to support searching and displaying articles from Redis.
This commit is contained in:
Omar Sánchez Pizarro
2026-01-19 20:42:11 +01:00
parent 9939c4d9ed
commit a316844576
7 changed files with 524 additions and 169 deletions

View File

@@ -24,7 +24,6 @@ app.use(express.json());
// Configuración
const CONFIG_PATH = join(PROJECT_ROOT, 'config.yaml');
const WORKERS_PATH = join(PROJECT_ROOT, 'workers.json');
const FAVORITES_PATH = join(PROJECT_ROOT, 'favorites.json');
// Función para obtener la ruta del log (en Docker puede estar en /data/logs)
function getLogPath() {
@@ -160,11 +159,42 @@ async function getNotifiedArticles() {
// API Routes
// Obtener favoritos desde Redis
async function getFavorites() {
if (!redisClient) {
return [];
}
try {
const keys = await redisClient.keys('notified:*');
const favorites = [];
for (const key of keys) {
const value = await redisClient.get(key);
if (value) {
try {
const articleData = JSON.parse(value);
if (articleData.is_favorite === true) {
favorites.push(articleData);
}
} catch (e) {
// Si no es JSON válido, ignorar
}
}
}
return favorites;
} catch (error) {
console.error('Error obteniendo favoritos de Redis:', error.message);
return [];
}
}
// Obtener estadísticas
app.get('/api/stats', async (req, res) => {
try {
const workers = readJSON(WORKERS_PATH, { items: [] });
const favorites = readJSON(FAVORITES_PATH, []);
const favorites = await getFavorites();
const notifiedArticles = await getNotifiedArticles();
const stats = {
@@ -210,9 +240,9 @@ app.put('/api/workers', (req, res) => {
});
// Obtener favoritos
app.get('/api/favorites', (req, res) => {
app.get('/api/favorites', async (req, res) => {
try {
const favorites = readJSON(FAVORITES_PATH, []);
const favorites = await getFavorites();
res.json(favorites);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -220,22 +250,40 @@ app.get('/api/favorites', (req, res) => {
});
// Añadir favorito
app.post('/api/favorites', (req, res) => {
app.post('/api/favorites', async (req, res) => {
try {
const favorite = req.body;
const favorites = readJSON(FAVORITES_PATH, []);
if (!redisClient) {
return res.status(500).json({ error: 'Redis no está disponible' });
}
// Evitar duplicados
if (!favorites.find(f => f.id === favorite.id && f.platform === favorite.platform)) {
favorites.push({
...favorite,
addedAt: new Date().toISOString(),
});
writeJSON(FAVORITES_PATH, favorites);
const { platform, id } = req.body;
if (!platform || !id) {
return res.status(400).json({ error: 'platform e id son requeridos' });
}
const key = `notified:${platform}:${id}`;
const value = await redisClient.get(key);
if (!value) {
return res.status(404).json({ error: 'Artículo no encontrado' });
}
try {
const articleData = JSON.parse(value);
articleData.is_favorite = true;
// Mantener el TTL existente
const ttl = await redisClient.ttl(key);
if (ttl > 0) {
await redisClient.setex(key, ttl, JSON.stringify(articleData));
} else {
await redisClient.set(key, JSON.stringify(articleData));
}
const favorites = await getFavorites();
broadcast({ type: 'favorites_updated', data: favorites });
res.json({ success: true, favorites });
} else {
res.json({ success: false, message: 'Ya existe en favoritos' });
} catch (e) {
res.status(500).json({ error: 'Error procesando artículo' });
}
} catch (error) {
res.status(500).json({ error: error.message });
@@ -243,17 +291,37 @@ app.post('/api/favorites', (req, res) => {
});
// Eliminar favorito
app.delete('/api/favorites/:platform/:id', (req, res) => {
app.delete('/api/favorites/:platform/:id', async (req, res) => {
try {
const { platform, id } = req.params;
const favorites = readJSON(FAVORITES_PATH, []);
const filtered = favorites.filter(
f => !(f.platform === platform && f.id === id)
);
if (!redisClient) {
return res.status(500).json({ error: 'Redis no está disponible' });
}
writeJSON(FAVORITES_PATH, filtered);
broadcast({ type: 'favorites_updated', data: filtered });
res.json({ success: true, favorites: filtered });
const { platform, id } = req.params;
const key = `notified:${platform}:${id}`;
const value = await redisClient.get(key);
if (!value) {
return res.status(404).json({ error: 'Artículo no encontrado' });
}
try {
const articleData = JSON.parse(value);
articleData.is_favorite = false;
// Mantener el TTL existente
const ttl = await redisClient.ttl(key);
if (ttl > 0) {
await redisClient.setex(key, ttl, JSON.stringify(articleData));
} else {
await redisClient.set(key, JSON.stringify(articleData));
}
const favorites = await getFavorites();
broadcast({ type: 'favorites_updated', data: favorites });
res.json({ success: true, favorites });
} catch (e) {
res.status(500).json({ error: 'Error procesando artículo' });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -280,6 +348,59 @@ app.get('/api/articles', async (req, res) => {
}
});
// Buscar artículos en Redis
app.get('/api/articles/search', async (req, res) => {
try {
const query = req.query.q || '';
if (!query.trim()) {
return res.json({ articles: [], total: 0 });
}
const searchTerm = query.toLowerCase().trim();
const allArticles = await getNotifiedArticles();
// Filtrar artículos que coincidan con la búsqueda
const filtered = allArticles.filter(article => {
// Buscar en título
const title = (article.title || '').toLowerCase();
if (title.includes(searchTerm)) return true;
// Buscar en descripción
const description = (article.description || '').toLowerCase();
if (description.includes(searchTerm)) return true;
// Buscar en localidad
const location = (article.location || '').toLowerCase();
if (location.includes(searchTerm)) return true;
// Buscar en precio (como número o texto)
const price = String(article.price || '').toLowerCase();
if (price.includes(searchTerm)) return true;
// Buscar en plataforma
const platform = (article.platform || '').toLowerCase();
if (platform.includes(searchTerm)) return true;
// Buscar en ID
const id = String(article.id || '').toLowerCase();
if (id.includes(searchTerm)) return true;
return false;
});
// Ordenar por fecha de notificación (más recientes primero)
const sorted = filtered.sort((a, b) => b.notifiedAt - a.notifiedAt);
res.json({
articles: sorted,
total: sorted.length,
query: query,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Obtener logs (últimas líneas)
app.get('/api/logs', (req, res) => {
try {
@@ -355,20 +476,17 @@ if (!existsSync(watchLogPath)) {
}
}
// Watch files for changes
const watcher = watch([WORKERS_PATH, FAVORITES_PATH, watchLogPath].filter(p => existsSync(p)), {
// Watch files for changes (ya no vigilamos FAVORITES_PATH porque usa Redis)
const watcher = watch([WORKERS_PATH, watchLogPath].filter(p => existsSync(p)), {
persistent: true,
ignoreInitial: true,
});
watcher.on('change', (path) => {
watcher.on('change', async (path) => {
console.log(`Archivo cambiado: ${path}`);
if (path === WORKERS_PATH) {
const workers = readJSON(WORKERS_PATH);
broadcast({ type: 'workers_updated', data: workers });
} else if (path === FAVORITES_PATH) {
const favorites = readJSON(FAVORITES_PATH);
broadcast({ type: 'favorites_updated', data: favorites });
} else if (path === LOG_PATH) {
broadcast({ type: 'logs_updated' });
}