Add companion app

This commit is contained in:
nmemmert
2026-04-20 14:58:57 -04:00
parent 0c328057c8
commit 74c21f956e
18 changed files with 2977 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
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})`);
});