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 += ''; - 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 += ''; - 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');