From fbcfd666d8490e663a1b2a4e7a6793db461e5368 Mon Sep 17 00:00:00 2001
From: Abel <196466003+DisabledAbel@users.noreply.github.com>
Date: Thu, 14 May 2026 11:29:59 -0700
Subject: [PATCH 1/4] Add Open in Reader RSS action button
---
api/index.html | 22 +++++++++++++++++++++-
index.html | 25 +++++++++++++++++++++++++
templates/index.html | 30 +++++++++++++++++++++++++++++-
3 files changed, 75 insertions(+), 2 deletions(-)
diff --git a/api/index.html b/api/index.html
index 2d53dc1..0daa883 100644
--- a/api/index.html
+++ b/api/index.html
@@ -67,6 +67,14 @@
font-size: 12px;
background: #444;
}
+ .open-reader-btn {
+ margin-top: 6px;
+ margin-left: 8px;
+ width: auto;
+ padding: 7px 10px;
+ font-size: 12px;
+ background: #0066cc;
+ }
.feed-label { color: #aaa; font-size: 12px; margin-bottom: 4px; }
.atom-output {
margin-top: 15px;
@@ -172,6 +180,17 @@
YouTube RSS Scanner
copyText(decodeURIComponent(encodedText));
}
+ function openInReader(encodedText) {
+ const feedUrl = decodeURIComponent(encodedText);
+ const feedProtocolUrl = feedUrl.startsWith('https://')
+ ? 'feed://' + feedUrl.slice('https://'.length)
+ : feedUrl.startsWith('http://')
+ ? 'feed://' + feedUrl.slice('http://'.length)
+ : feedUrl;
+
+ window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');
+ }
+
async function getFeed() {
const url = document.getElementById('channelUrl').value.trim();
const resultDiv = document.getElementById('result');
@@ -220,7 +239,8 @@ YouTube RSS Scanner
html += 'Selected RSS Feed:
';
html += '
' + (data.selected_feed || data.youtube_rss) + '
';
- html += '
';
+ html += '';
+ html += '';
if (data.official_feeds) {
html += 'Official YouTube Feeds:
';
diff --git a/index.html b/index.html
index a873bad..83c6060 100644
--- a/index.html
+++ b/index.html
@@ -67,6 +67,14 @@
font-size: 12px;
background: #444;
}
+ .open-reader-btn {
+ margin-top: 6px;
+ margin-left: 8px;
+ width: auto;
+ padding: 7px 10px;
+ font-size: 12px;
+ background: #0066cc;
+ }
.feed-label { color: #aaa; font-size: 12px; margin-bottom: 4px; }
.atom-output {
margin-top: 15px;
@@ -167,6 +175,17 @@
YouTube RSS Scanner
}
}
+
+ function openInReader(feedUrl) {
+ const feedProtocolUrl = feedUrl.startsWith('https://')
+ ? 'feed://' + feedUrl.slice('https://'.length)
+ : feedUrl.startsWith('http://')
+ ? 'feed://' + feedUrl.slice('http://'.length)
+ : feedUrl;
+
+ window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');
+ }
+
async function getFeed() {
const url = document.getElementById('channelUrl').value.trim();
const resultDiv = document.getElementById('result');
@@ -259,6 +278,12 @@
YouTube RSS Scanner
copyBtn.textContent = 'Copy Selected RSS';
copyBtn.addEventListener('click', () => copyText((data.selected_feed || data.youtube_rss)));
feedRow.appendChild(copyBtn);
+
+ const openReaderBtn = document.createElement('button');
+ openReaderBtn.className = 'open-reader-btn';
+ openReaderBtn.textContent = 'Open in Reader';
+ openReaderBtn.addEventListener('click', () => openInReader((data.selected_feed || data.youtube_rss)));
+ feedRow.appendChild(openReaderBtn);
resultDiv.appendChild(feedRow);
if (data.official_feeds) {
const officialBox = document.createElement('div');
diff --git a/templates/index.html b/templates/index.html
index 18c7b15..b6c190a 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -67,6 +67,14 @@
font-size: 12px;
background: #444;
}
+ .open-reader-btn {
+ margin-top: 6px;
+ margin-left: 8px;
+ width: auto;
+ padding: 7px 10px;
+ font-size: 12px;
+ background: #0066cc;
+ }
.feed-label { color: #aaa; font-size: 12px; margin-bottom: 4px; }
.atom-output {
margin-top: 15px;
@@ -167,6 +175,19 @@
YouTube RSS Scanner
copyText(decodeURIComponent(encodedText));
}
+ function openInReader(encodedText) {
+ const feedUrl = decodeURIComponent(encodedText);
+ // Many desktop/mobile feed apps register the feed:// protocol.
+ // Convert https:// URLs to feed:// where possible to trigger RSS readers.
+ const feedProtocolUrl = feedUrl.startsWith('https://')
+ ? 'feed://' + feedUrl.slice('https://'.length)
+ : feedUrl.startsWith('http://')
+ ? 'feed://' + feedUrl.slice('http://'.length)
+ : feedUrl;
+
+ window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');
+ }
+
async function getFeed() {
const url = document.getElementById('channelUrl').value.trim();
const resultDiv = document.getElementById('result');
@@ -214,7 +235,8 @@
YouTube RSS Scanner
html += '
Selected RSS Feed:
';
html += '
' + (data.selected_feed || data.youtube_rss) + '
';
- html += '
';
+ html += '
';
+ html += '
';
if (data.official_feeds) {
html += 'Official YouTube Feeds:
';
@@ -245,6 +267,12 @@
YouTube RSS Scanner
copyEncodedText(this.dataset.encoded);
});
});
+
+ resultDiv.querySelectorAll('.open-reader-btn').forEach(button => {
+ button.addEventListener('click', function() {
+ openInReader(this.dataset.encoded);
+ });
+ });
}
} catch (e) {
resultDiv.innerHTML = '
Error: ' + e.message + '
';
From 09a344320b932ca3808b7883f814e1c021e75d6b Mon Sep 17 00:00:00 2001
From: Abel <196466003+DisabledAbel@users.noreply.github.com>
Date: Thu, 14 May 2026 11:34:35 -0700
Subject: [PATCH 2/4] Fix RSS reader button to use Feedly subscribe URL
---
api/index.html | 9 ++-------
index.html | 9 ++-------
2 files changed, 4 insertions(+), 14 deletions(-)
diff --git a/api/index.html b/api/index.html
index 0daa883..9ac4108 100644
--- a/api/index.html
+++ b/api/index.html
@@ -182,13 +182,8 @@
YouTube RSS Scanner
function openInReader(encodedText) {
const feedUrl = decodeURIComponent(encodedText);
- const feedProtocolUrl = feedUrl.startsWith('https://')
- ? 'feed://' + feedUrl.slice('https://'.length)
- : feedUrl.startsWith('http://')
- ? 'feed://' + feedUrl.slice('http://'.length)
- : feedUrl;
-
- window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');
+ const feedlySubscribeUrl = 'https://feedly.com/i/subscription/feed/' + encodeURIComponent(feedUrl);
+ window.open(feedlySubscribeUrl, '_blank', 'noopener,noreferrer');
}
async function getFeed() {
diff --git a/index.html b/index.html
index 83c6060..8d2768f 100644
--- a/index.html
+++ b/index.html
@@ -177,13 +177,8 @@
YouTube RSS Scanner
function openInReader(feedUrl) {
- const feedProtocolUrl = feedUrl.startsWith('https://')
- ? 'feed://' + feedUrl.slice('https://'.length)
- : feedUrl.startsWith('http://')
- ? 'feed://' + feedUrl.slice('http://'.length)
- : feedUrl;
-
- window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');
+ const feedlySubscribeUrl = 'https://feedly.com/i/subscription/feed/' + encodeURIComponent(feedUrl);
+ window.open(feedlySubscribeUrl, '_blank', 'noopener,noreferrer');
}
async function getFeed() {
From 369adbce4a92f3f6611ed3547090ed55d85b0758 Mon Sep 17 00:00:00 2001
From: Abel <196466003+DisabledAbel@users.noreply.github.com>
Date: Thu, 14 May 2026 11:39:16 -0700
Subject: [PATCH 3/4] Add self-hosted terminal RSS reader scripts
---
README.md | 18 +++++++
scripts/run_terminal_reader.sh | 9 ++++
scripts/terminal_reader.py | 87 ++++++++++++++++++++++++++++++++++
3 files changed, 114 insertions(+)
create mode 100755 scripts/run_terminal_reader.sh
create mode 100755 scripts/terminal_reader.py
diff --git a/README.md b/README.md
index cd3ee2f..fc94477 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,24 @@ Pass `discord_webhook_url` to `/api/feed` and the app will send a message to Dis
---
+
+## Self-Hosted Terminal Reader
+
+If you want to host your own simple feed reader directly in your terminal, use:
+
+```bash
+./scripts/run_terminal_reader.sh "https://www.youtube.com/feeds/videos.xml?channel_id=UCXuqSBlHAE6Xw-yeJA0Tunw"
+```
+
+Optional flags:
+
+```bash
+./scripts/run_terminal_reader.sh "
" --interval 60 --limit 15
+```
+
+- `--interval`: refresh frequency in seconds.
+- `--limit`: max entries shown per refresh.
+
## CLI RSS Reader (Python)
If you are running locally and want to read recent items directly in the terminal:
diff --git a/scripts/run_terminal_reader.sh b/scripts/run_terminal_reader.sh
new file mode 100755
index 0000000..8f289b6
--- /dev/null
+++ b/scripts/run_terminal_reader.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ $# -lt 1 ]]; then
+ echo "Usage: $0 [--interval SECONDS] [--limit N]"
+ exit 1
+fi
+
+python scripts/terminal_reader.py "$@"
diff --git a/scripts/terminal_reader.py b/scripts/terminal_reader.py
new file mode 100755
index 0000000..cecf32a
--- /dev/null
+++ b/scripts/terminal_reader.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""Simple terminal RSS reader host.
+
+Fetches an RSS/Atom feed on an interval and prints new entries to the terminal.
+"""
+
+from __future__ import annotations
+
+import argparse
+import time
+import urllib.request
+import xml.etree.ElementTree as ET
+from datetime import datetime
+
+
+def fetch_feed(feed_url: str, timeout: int = 20) -> bytes:
+ req = urllib.request.Request(feed_url, headers={"User-Agent": "TerminalRSSReader/1.0"})
+ with urllib.request.urlopen(req, timeout=timeout) as response:
+ return response.read()
+
+
+def parse_entries(xml_bytes: bytes, limit: int) -> list[tuple[str, str, str]]:
+ root = ET.fromstring(xml_bytes)
+
+ entries: list[tuple[str, str, str]] = []
+
+ # RSS
+ for item in root.findall('.//item'):
+ title = (item.findtext('title') or 'Untitled').strip()
+ link = (item.findtext('link') or '').strip()
+ published = (item.findtext('pubDate') or 'Unknown date').strip()
+ entries.append((title, link, published))
+
+ # Atom fallback
+ if not entries:
+ ns = {'atom': 'http://www.w3.org/2005/Atom'}
+ for entry in root.findall('.//atom:entry', ns):
+ title = (entry.findtext('atom:title', default='Untitled', namespaces=ns) or 'Untitled').strip()
+ link_el = entry.find('atom:link', ns)
+ link = (link_el.attrib.get('href', '') if link_el is not None else '').strip()
+ published = (entry.findtext('atom:published', default='Unknown date', namespaces=ns) or 'Unknown date').strip()
+ entries.append((title, link, published))
+
+ return entries[:limit]
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='Host a simple RSS reader in your terminal.')
+ parser.add_argument('feed_url', help='RSS/Atom feed URL to watch')
+ parser.add_argument('--interval', type=int, default=120, help='Seconds between refreshes (default: 120)')
+ parser.add_argument('--limit', type=int, default=10, help='Maximum entries to show per refresh (default: 10)')
+ args = parser.parse_args()
+
+ seen_links: set[str] = set()
+
+ print(f"\n๐ก Terminal Reader started for: {args.feed_url}")
+ print(f"โฑ Refresh interval: {args.interval}s | ๐งพ Entry limit: {args.limit}")
+ print("Press Ctrl+C to stop.\n")
+
+ try:
+ while True:
+ timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
+ try:
+ xml_bytes = fetch_feed(args.feed_url)
+ entries = parse_entries(xml_bytes, args.limit)
+ print(f"\n=== Refresh @ {timestamp} ===")
+ if not entries:
+ print('No entries found.')
+ for idx, (title, link, published) in enumerate(entries, start=1):
+ marker = '๐' if link and link not in seen_links else ' '
+ print(f"{idx:02d}. {marker} {title}")
+ print(f" Date: {published}")
+ if link:
+ print(f" Link: {link}")
+ if link:
+ seen_links.add(link)
+ except Exception as exc:
+ print(f"Error fetching/parsing feed: {exc}")
+
+ time.sleep(args.interval)
+ except KeyboardInterrupt:
+ print("\nStopped terminal reader.")
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
From d1e9d9198615ce51cc13905a402771d71b4db2fa Mon Sep 17 00:00:00 2001
From: Abel <196466003+DisabledAbel@users.noreply.github.com>
Date: Thu, 14 May 2026 11:41:20 -0700
Subject: [PATCH 4/4] Update templates/index.html
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
templates/index.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/templates/index.html b/templates/index.html
index b6c190a..ad230ea 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -180,9 +180,9 @@ YouTube RSS Scanner
// Many desktop/mobile feed apps register the feed:// protocol.
// Convert https:// URLs to feed:// where possible to trigger RSS readers.
const feedProtocolUrl = feedUrl.startsWith('https://')
- ? 'feed://' + feedUrl.slice('https://'.length)
+ ? 'feed:' + feedUrl
: feedUrl.startsWith('http://')
- ? 'feed://' + feedUrl.slice('http://'.length)
+ ? 'feed:' + feedUrl
: feedUrl;
window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');