92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
|
|
import { NextRequest, NextResponse } from 'next/server';
|
||
|
|
import { getDb } from '@/lib/db';
|
||
|
|
import Parser from 'rss-parser';
|
||
|
|
|
||
|
|
export const dynamic = 'force-dynamic';
|
||
|
|
|
||
|
|
export async function GET() {
|
||
|
|
const db = await getDb();
|
||
|
|
const feeds = await db.all(`
|
||
|
|
SELECT f.*, (SELECT COUNT(*) FROM rss_items WHERE feed_id = f.id) as item_count
|
||
|
|
FROM rss_feeds f ORDER BY f.created_at ASC
|
||
|
|
`);
|
||
|
|
return NextResponse.json(feeds);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function POST(req: NextRequest) {
|
||
|
|
const { url, name } = await req.json();
|
||
|
|
|
||
|
|
if (!url || typeof url !== 'string') {
|
||
|
|
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate URL format
|
||
|
|
try {
|
||
|
|
new URL(url);
|
||
|
|
} catch {
|
||
|
|
return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate it's actually an RSS feed
|
||
|
|
const parser = new Parser({ timeout: 5000 });
|
||
|
|
let parsed;
|
||
|
|
try {
|
||
|
|
parsed = await parser.parseURL(url);
|
||
|
|
} catch {
|
||
|
|
return NextResponse.json({ error: 'Could not parse RSS feed at this URL' }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const feedName = name?.trim() || parsed.title || new URL(url).hostname;
|
||
|
|
|
||
|
|
const db = await getDb();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await db.run(
|
||
|
|
'INSERT INTO rss_feeds (name, url) VALUES (?, ?)',
|
||
|
|
feedName, url.trim()
|
||
|
|
);
|
||
|
|
const feedId = result.lastID;
|
||
|
|
|
||
|
|
// Immediately fetch and cache items
|
||
|
|
for (const item of parsed.items || []) {
|
||
|
|
if (!item.title || !item.link) continue;
|
||
|
|
await db.run(
|
||
|
|
`INSERT OR IGNORE INTO rss_items (feed_id, title, link, pub_date, creator, snippet)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||
|
|
feedId,
|
||
|
|
item.title,
|
||
|
|
item.link,
|
||
|
|
item.pubDate || item.isoDate || null,
|
||
|
|
item.creator || item.author || null,
|
||
|
|
(item.contentSnippet || item.content || '').substring(0, 500) || null
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
await db.run(
|
||
|
|
'UPDATE rss_feeds SET last_fetched = ? WHERE id = ?',
|
||
|
|
new Date().toISOString(), feedId
|
||
|
|
);
|
||
|
|
|
||
|
|
const feed = await db.get('SELECT * FROM rss_feeds WHERE id = ?', feedId);
|
||
|
|
return NextResponse.json(feed, { status: 201 });
|
||
|
|
} catch (err: unknown) {
|
||
|
|
if (err instanceof Error && err.message.includes('UNIQUE')) {
|
||
|
|
return NextResponse.json({ error: 'Feed URL already exists' }, { status: 409 });
|
||
|
|
}
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function DELETE(req: NextRequest) {
|
||
|
|
const { id } = await req.json();
|
||
|
|
if (!id) {
|
||
|
|
return NextResponse.json({ error: 'Feed id is required' }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const db = await getDb();
|
||
|
|
await db.run('DELETE FROM rss_items WHERE feed_id = ?', id);
|
||
|
|
await db.run('DELETE FROM rss_feeds WHERE id = ?', id);
|
||
|
|
|
||
|
|
return NextResponse.json({ ok: true });
|
||
|
|
}
|