// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Posimai Feed API - Media Management Endpoints // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Purpose: Allow users to add/remove custom RSS feeds // Location: Copy this to Synology NAS at /app/server.js (Feed API section) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const express = require('express'); const router = express.Router(); // Assume: authMiddleware, pool (PostgreSQL) are already imported // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // POST /feed/api/media/add // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * Add a new RSS feed source * * Request body: * { * feedUrl: string // RSS feed URL (required) * feedName: string // Display name (optional, auto-detected from RSS) * category: string // User-defined category (optional) * } * * Response: * { * success: boolean * mediaSource: { id, feedUrl, feedName, ... } * } */ router.post('/media/add', authMiddleware, async (req, res) => { const { feedUrl, feedName, category } = req.body || {}; // ────────────────────────────────────────────────────────────────── // 1. Validation // ────────────────────────────────────────────────────────────────── if (!feedUrl) { return res.status(400).json({ error: 'Missing required field: feedUrl' }); } // Validate URL format try { new URL(feedUrl); } catch { return res.status(400).json({ error: 'Invalid URL format' }); } try { // ────────────────────────────────────────────────────────────── // 2. Verify RSS feed (fetch and parse) // ────────────────────────────────────────────────────────────── console.log(`[Feed API] Verifying RSS feed: ${feedUrl}`); const rssData = await fetchAndParseRSS(feedUrl); if (!rssData) { return res.status(400).json({ error: 'Invalid RSS feed: Unable to parse feed' }); } const detectedName = feedName || rssData.title || new URL(feedUrl).hostname; const detectedIcon = rssData.icon || null; // ────────────────────────────────────────────────────────────── // 3. Save to database // ────────────────────────────────────────────────────────────── const result = await pool.query(` INSERT INTO media_sources ( user_id, feed_url, feed_name, feed_icon, category, is_active, last_fetched_at, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, true, NOW(), NOW(), NOW() ) ON CONFLICT (user_id, feed_url) DO UPDATE SET feed_name = EXCLUDED.feed_name, feed_icon = EXCLUDED.feed_icon, category = EXCLUDED.category, is_active = true, updated_at = NOW() RETURNING * `, [ req.userId, // $1: user_id from authMiddleware feedUrl, // $2: feed_url detectedName, // $3: feed_name detectedIcon, // $4: feed_icon category || null // $5: category ]); const mediaSource = result.rows[0]; console.log(`[Feed API] Media source added: ${detectedName} (ID=${mediaSource.id})`); // ────────────────────────────────────────────────────────────── // 4. Success response // ────────────────────────────────────────────────────────────── return res.json({ success: true, mediaSource: { id: mediaSource.id, feedUrl: mediaSource.feed_url, feedName: mediaSource.feed_name, feedIcon: mediaSource.feed_icon, category: mediaSource.category, isActive: mediaSource.is_active, createdAt: mediaSource.created_at } }); } catch (error) { console.error('[Feed API] Failed to add media source:', error); return res.status(500).json({ error: 'Failed to add RSS feed', message: error.message }); } }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // GET /feed/api/media // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * Get all media sources for authenticated user * * Response: * { * mediaSources: [ * { id, feedUrl, feedName, feedIcon, category, isActive, ... } * ] * } */ router.get('/media', authMiddleware, async (req, res) => { try { const { rows } = await pool.query(` SELECT id, feed_url, feed_name, feed_icon, category, is_active, last_fetched_at, fetch_interval_minutes, created_at, updated_at FROM media_sources WHERE user_id = $1 ORDER BY created_at DESC `, [req.userId]); return res.json({ mediaSources: rows.map(row => ({ id: row.id, feedUrl: row.feed_url, feedName: row.feed_name, feedIcon: row.feed_icon, category: row.category, isActive: row.is_active, lastFetchedAt: row.last_fetched_at, fetchIntervalMinutes: row.fetch_interval_minutes, createdAt: row.created_at, updatedAt: row.updated_at })) }); } catch (error) { console.error('[Feed API] Failed to fetch media sources:', error); return res.status(500).json({ error: 'Failed to fetch media sources', message: error.message }); } }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // DELETE /feed/api/media/:id // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * Remove a media source (soft delete - set is_active=false) * * Response: * { * success: boolean * } */ router.delete('/media/:id', authMiddleware, async (req, res) => { const { id } = req.params; try { const result = await pool.query(` UPDATE media_sources SET is_active = false, updated_at = NOW() WHERE id = $1 AND user_id = $2 RETURNING id `, [id, req.userId]); if (result.rowCount === 0) { return res.status(404).json({ error: 'Media source not found' }); } console.log(`[Feed API] Media source deleted: ID=${id}`); return res.json({ success: true }); } catch (error) { console.error('[Feed API] Failed to delete media source:', error); return res.status(500).json({ error: 'Failed to delete media source', message: error.message }); } }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Helper Functions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /** * Fetch and parse RSS feed * * @param {string} feedUrl - RSS feed URL * @returns {Promise<{title: string, icon: string}|null>} */ async function fetchAndParseRSS(feedUrl) { try { const response = await fetch(feedUrl, { headers: { 'User-Agent': 'Mozilla/5.0 Posimai Feed Bot' }, timeout: 10000 // 10 second timeout }); if (!response.ok) { console.error(`[RSS] HTTP error: ${response.status}`); return null; } const text = await response.text(); // Simple RSS/Atom parsing (you may want to use a library like 'rss-parser') // For now, we'll do basic regex matching // Extract