<!doctype html>
<html lang="en">
- <head>
- <title>River</title>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="https://classless.de/classless.css">
- <link rel="stylesheet" href="./style.css">
- <script src="https://unpkg.com/htmx.org@1.9.10"></script>
- </head>
- <body>
- <nav>
- <ul>
- <li><a href="/">River View</a></li>
- <li><a href="/reader.html">Reader View</a></li>
- </ul>
- </nav>
- <header>
- <h1>River of News</h1>
- <p>
- <strong>An auto-updating list of news</strong>
- </p>
- </header>
- <main class="rss-reader row">
- <div class="sidebar col-3" hx-get="/feeds" hx-trigger="load"></div>
- <div class="feed-entries col"></div>
- </main>
- <footer>
- <hr>
- <p>
- Another project by <a href="https://xangelo">xangelo</a>
- </p>
- </footer>
- </body>
+
+<head>
+ <title>River</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="https://classless.de/classless.css">
+ <link rel="stylesheet" href="./style.css">
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
+</head>
+
+<body>
+ <nav>
+ <ul>
+ <li><a href="/">River View</a></li>
+ <li><a href="/reader.html">Reader View</a></li>
+ </ul>
+ </nav>
+ <header>
+ <h1>River of News</h1>
+ <p>
+ <strong>An auto-updating list of news</strong>
+ </p>
+ </header>
+ <main class="rss-reader row">
+ <div class="sidebar col-3" hx-get="/feeds?filter=reader" hx-trigger="load"></div>
+ <div class="feed-entries col"></div>
+ </main>
+ <footer>
+ <hr>
+ <p>
+ Another project by <a href="https://xangelo">xangelo</a>
+ </p>
+ </footer>
+</body>
<script>
const keyName = 'lastCheck';
const headerName = 'last-check';
const secret = localStorage.getItem('secret');
event.detail.headers[headerName] = val;
event.detail.headers['x-secret'] = secret;
- if(val) {
+ if (val) {
event.detail.headers['entry-count'] = document.querySelectorAll('.item').length;
}
});
document.body.addEventListener('htmx:afterRequest', evt => {
localStorage.setItem(keyName, evt.detail.xhr.getResponseHeader(headerName));
});
- </script>
-</html>
+</script>
+
+</html>
\ No newline at end of file
debug: process.env.DEBUG === 'true',
connection: {
filename: join('./data.db')
- }
+ },
+ migrations: {
+ directory: join(__dirname, '../migrations'),
+ extension: 'ts'
+ },
+ useNullAsDefault: true
});
const parser = new Parser();
const app = express();
const FEED_REFRESH_RATE = parseInt(process.env.FEED_REFRESH_RATE || '0');
function timestamp(obj: any): number {
- if(obj.getTime) {
- return Math.floor(obj.getTime()/1000)
+ if (obj.getTime) {
+ return Math.floor(obj.getTime() / 1000)
}
- else if(obj.toString() === obj) {
+ else if (obj.toString() === obj) {
return Math.floor(new Date(obj).getTime() / 1000)
}
- else if(!isNaN(obj)) {
- if(obj.toString().length === 10) {
+ else if (!isNaN(obj)) {
+ if (obj.toString().length === 10) {
return obj;
}
else {
- return Math.floor(obj/1000);
+ return Math.floor(obj / 1000);
}
}
}
function contentExtractor(feed: FeedSchema, item): string {
let str = '';
- switch(feed.title) {
+ switch (feed.title) {
case 'TechCrunch':
str = item.contentSnippet;
break;
break;
case 'The Register':
str = item.summary;
- break;
+ break;
}
str = str.replace(/<[^>]+>/g, '');
console.log(`Querying ${feedsToQuery.length} feeds`);
- for(let feed of feedsToQuery) {
+ for (let feed of feedsToQuery) {
console.log(`Querying ${feed.title}(${feed.url})`);
try {
const data = await parser.parseURL(feed.url);
const items: FeedEntrySchema<any>[] = data.items.map(item => {
const id = createHash('sha256');
- id.update(item.guid || item.id || Date.now()+Math.random());
+ id.update(item.guid || item.id || Date.now() + Math.random());
return {
id: id.digest('hex'),
title: item.title,
}
});
- for(let item of items) {
+ for (let item of items) {
// inserting one at a time so that we can ignore duplicates for now
// eventually we'll need to do some kind of merge
await db('feed_entry').insert(item).onConflict().ignore();
}
- catch(e) {
+ catch (e) {
console.log(e);
console.log('Continuing..');
}
setTimeout(queryFeeds, 1000 * 60);
}
+// Migration runner function
+async function runMigrations() {
+ try {
+ console.log('Running database migrations...');
+ const [batchNo, log] = await db.migrate.latest();
+
+ if (log.length === 0) {
+ console.log('Database is already up to date');
+ } else {
+ console.log(`Batch ${batchNo} run: ${log.length} migrations`);
+ log.forEach((migration) => console.log(`- ${migration}`));
+ }
+ } catch (error) {
+ console.error('Migration failed:', error);
+ process.exit(1);
+ }
+}
+
app.use(express.static(join(__dirname, 'public')));
app.use(express.json());
const freshReadInHours = parseInt(process.env.FRESH_READ_IN_HOURS) || 6;
let createdAt = Math.floor(timestamp(new Date()) - ((60 * 60) * freshReadInHours));
- if(!displayedItems) {
+ if (!displayedItems) {
const temp = parseInt(header);
- if(!isNaN(temp)) {
+ if (!isNaN(temp)) {
firstLoad = false;
createdAt = temp;
}
.limit(100)
.orderBy('feed_entry.pub_date', 'desc');
- if(entries[0] && entries[0].created_at) {
+ if (entries[0] && entries[0].created_at) {
res.append('last-check', entries[0].created_at.toString());
}
else {
let groupIndex = -1;
entries.forEach(entry => {
- if(entry.feed_id !== prevId) {
+ if (entry.feed_id !== prevId) {
prevId = entry.feed_id;
groupIndex++;
groups.push([]);
});
app.post('/feeds', async (req, res) => {
- if(req.body.key !== process.env.ADMIN_KEY) {
+ if (req.body.key !== process.env.ADMIN_KEY) {
res.sendStatus(400).end();
return;
}
});
app.get('/feeds', async (req, res) => {
- const feedList: FeedSchemaWithUnread[] = await db.raw(`
-select
- f.*,
- sum(fe.is_read) as read,
- count(fe.feed_id) as total
-from feeds f
-join feed_entry fe on fe.feed_id = f.id
-group by fe.feed_id
-`);
+ const filter = req.query.filter;
+ let query = `
+ select
+ f.*,
+ sum(fe.is_read) as read,
+ count(fe.feed_id) as total
+ from feeds f
+ join feed_entry fe on fe.feed_id = f.id
+ group by fe.feed_id
+ `;
+ if (filter === 'reader') {
+ query += ` having f.show_in_reader = 1`;
+ }
+ const feedList: FeedSchemaWithUnread[] = await db.raw(query);
- if(req.accepts('html')) {
+ if (req.accepts('html')) {
res.send(renderReaderAppFeedList(feedList));
return;
}
group by fe.feed_id
`, [req.params.feed_id]);
- if(req.accepts('html')) {
+ if (req.accepts('html')) {
res.send(renderReaderAppFeedEntries(page, feedData.pop(), feedEntries))
return;
}
app.post('/feed_entry/:feed_entry_id', async (req, res) => {
const authSecret = req.header('x-secret');
- if(authSecret === process.env.ADMIN_KEY) {
+ if (authSecret === process.env.ADMIN_KEY) {
const item: FeedWithEntrySchema[] = await db('feed_entry').update({
is_read: true
}).where({
});
app.delete('/feeds/:feed_id', async (req, res) => {
- if(req.query.key !== process.env.ADMIN_KEY) {
+ if (req.query.key !== process.env.ADMIN_KEY) {
res.sendStatus(400).end();
return;
}
res.json(await db.select('*').from('feeds').orderBy('created_at'));
});
-app.listen(process.env.API_PORT, () => {
- console.log(`Listening on port ${process.env.API_PORT}`);
- console.log(`Feed refresh rate: ${FEED_REFRESH_RATE}m`);
- queryFeeds();
+// Start the application
+async function startApp() {
+ // Run migrations first
+ await runMigrations();
+
+ // Then start the server
+ app.listen(process.env.API_PORT, () => {
+ console.log(`Listening on port ${process.env.API_PORT}`);
+ console.log(`Feed refresh rate: ${FEED_REFRESH_RATE}m`);
+ queryFeeds();
+ });
+}
+
+// Start the application
+startApp().catch(error => {
+ console.error('Failed to start application:', error);
+ process.exit(1);
});