const express = require('express'); const router = express.Router(); const db = require('../db'); const { fetchFeedMeta, fetchAndStoreFeed } = require('../feedFetcher'); router.get('/', (req, res) => { const feeds = db.prepare(` SELECT f.*, (SELECT COUNT(*) FROM articles a WHERE a.feed_id = f.id AND a.is_read = 0) AS unread_count, (SELECT COUNT(*) FROM articles a WHERE a.feed_id = f.id) AS total_count FROM feeds f ORDER BY f.sort_order, f.title COLLATE NOCASE `).all(); res.json(feeds); }); router.post('/', async (req, res) => { const { url, folder_id } = req.body; if (!url || !url.trim()) return res.status(400).json({ error: 'Feed URL is required' }); const cleanUrl = url.trim(); const existing = db.prepare('SELECT * FROM feeds WHERE url = ?').get(cleanUrl); if (existing) return res.status(409).json({ error: 'This feed is already in your list', feed: existing }); try { const meta = await fetchFeedMeta(cleanUrl); const info = db.prepare(` INSERT INTO feeds (url, title, site_url, description, folder_id) VALUES (?, ?, ?, ?, ?) `).run(cleanUrl, meta.title, meta.siteUrl, meta.description, folder_id || null); const feed = db.prepare('SELECT * FROM feeds WHERE id = ?').get(info.lastInsertRowid); fetchAndStoreFeed(feed).catch(() => {}); res.status(201).json(feed); } catch (err) { res.status(422).json({ error: `Couldn't read that feed. Check the URL and try again. (${err.message})` }); } }); router.put('/:id', (req, res) => { const { title, folder_id, fetch_interval_minutes } = req.body; const feed = db.prepare('SELECT * FROM feeds WHERE id = ?').get(req.params.id); if (!feed) return res.status(404).json({ error: 'Feed not found' }); db.prepare(`UPDATE feeds SET title = COALESCE(?, title), folder_id = ?, fetch_interval_minutes = COALESCE(?, fetch_interval_minutes) WHERE id = ?`).run( title, folder_id === undefined ? feed.folder_id : folder_id, fetch_interval_minutes, req.params.id ); res.json(db.prepare('SELECT * FROM feeds WHERE id = ?').get(req.params.id)); }); router.delete('/:id', (req, res) => { db.prepare('DELETE FROM feeds WHERE id = ?').run(req.params.id); res.status(204).end(); }); router.post('/:id/refresh', async (req, res) => { const feed = db.prepare('SELECT * FROM feeds WHERE id = ?').get(req.params.id); if (!feed) return res.status(404).json({ error: 'Feed not found' }); const result = await fetchAndStoreFeed(feed); res.json(result); }); router.post('/refresh-all', async (req, res) => { const feeds = db.prepare('SELECT * FROM feeds').all(); const results = []; for (const feed of feeds) { results.push({ feedId: feed.id, ...(await fetchAndStoreFeed(feed)) }); } res.json(results); }); // OPML export router.get('/export/opml', (req, res) => { const feeds = db.prepare('SELECT f.*, fo.name AS folder_name FROM feeds f LEFT JOIN folders fo ON f.folder_id = fo.id').all(); const byFolder = {}; const noFolder = []; for (const f of feeds) { if (f.folder_name) { byFolder[f.folder_name] = byFolder[f.folder_name] || []; byFolder[f.folder_name].push(f); } else { noFolder.push(f); } } const esc = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/\n`; for (const f of list) { body += ` \n`; } body += `\n`; } for (const f of noFolder) { body += `\n`; } const opml = `\n\nDispatch Feed Export\n\n${body}\n`; res.set('Content-Type', 'text/x-opml'); res.set('Content-Disposition', 'attachment; filename="dispatch-feeds.opml"'); res.send(opml); }); // OPML import router.post('/import/opml', express.text({ type: '*/*', limit: '5mb' }), async (req, res) => { const xml = req.body; const urlMatches = [...xml.matchAll(/xmlUrl="([^"]+)"/g)].map(m => m[1].replace(/&/g, '&')); const titleMatches = [...xml.matchAll(/xmlUrl="[^"]+"\s+(?:htmlUrl="[^"]*"\s*)?/g)]; const results = []; for (const url of urlMatches) { const existing = db.prepare('SELECT id FROM feeds WHERE url = ?').get(url); if (existing) { results.push({ url, status: 'skipped' }); continue; } try { const meta = await fetchFeedMeta(url); const info = db.prepare('INSERT INTO feeds (url, title, site_url, description) VALUES (?, ?, ?, ?)') .run(url, meta.title, meta.siteUrl, meta.description); const feed = db.prepare('SELECT * FROM feeds WHERE id = ?').get(info.lastInsertRowid); fetchAndStoreFeed(feed).catch(() => {}); results.push({ url, status: 'added' }); } catch (err) { results.push({ url, status: 'failed', error: err.message }); } } res.json({ imported: results.length, results }); }); module.exports = router;