103 lines
2.5 KiB
JavaScript
103 lines
2.5 KiB
JavaScript
import express from 'express';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { XMLParser } from 'fast-xml-parser';
|
|
|
|
const RSS_URL = 'https://anchor.fm/s/11068d290/podcast/rss';
|
|
const PORT = Number(process.env.PORT || 5181);
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const parser = new XMLParser({
|
|
ignoreAttributes: false,
|
|
trimValues: true
|
|
});
|
|
|
|
function toArray(value) {
|
|
if (!value) return [];
|
|
return Array.isArray(value) ? value : [value];
|
|
}
|
|
|
|
function cleanText(input) {
|
|
if (!input) return '';
|
|
return String(input)
|
|
.replace(/<[^>]*>/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function normalizeItem(item, index) {
|
|
const title = cleanText(item.title) || `Episode ${index + 1}`;
|
|
const summary = cleanText(item['content:encoded'] || item.description) || 'New episode now available.';
|
|
const link = item.link || item.guid || RSS_URL;
|
|
|
|
const date = item.pubDate ? new Date(item.pubDate) : null;
|
|
const publishedAt = date && !Number.isNaN(date.getTime()) ? date.toISOString() : null;
|
|
|
|
return {
|
|
id: cleanText(item.guid) || link,
|
|
title,
|
|
summary,
|
|
scripture: '',
|
|
reflectionPrompt: '',
|
|
prayerFocus: '',
|
|
challenge: '',
|
|
link,
|
|
publishedAt
|
|
};
|
|
}
|
|
|
|
async function readFeedEpisodes() {
|
|
const response = await fetch(RSS_URL, {
|
|
headers: {
|
|
'User-Agent': 'Siteforge-Companion/1.0'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`RSS request failed with ${response.status}`);
|
|
}
|
|
|
|
const xml = await response.text();
|
|
const parsed = parser.parse(xml);
|
|
const items = toArray(parsed?.rss?.channel?.item);
|
|
return items.map(normalizeItem).slice(0, 24);
|
|
}
|
|
|
|
const app = express();
|
|
|
|
app.get('/api/episodes', async (_req, res) => {
|
|
try {
|
|
const episodes = await readFeedEpisodes();
|
|
res.json({
|
|
source: 'rss',
|
|
rssUrl: RSS_URL,
|
|
updatedAt: new Date().toISOString(),
|
|
episodes
|
|
});
|
|
} catch (error) {
|
|
res.status(502).json({
|
|
source: 'rss',
|
|
rssUrl: RSS_URL,
|
|
error: error instanceof Error ? error.message : 'Unable to load RSS feed',
|
|
episodes: []
|
|
});
|
|
}
|
|
});
|
|
|
|
if (isProd) {
|
|
const distDir = path.resolve(__dirname, 'dist');
|
|
app.use(express.static(distDir));
|
|
|
|
app.get('*', (_req, res) => {
|
|
res.sendFile(path.join(distDir, 'index.html'));
|
|
});
|
|
}
|
|
|
|
app.listen(PORT, () => {
|
|
const mode = isProd ? 'production' : 'development';
|
|
console.log(`Companion API listening on http://localhost:${PORT} (${mode})`);
|
|
});
|