Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<feed_url>" --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:
Expand Down
17 changes: 16 additions & 1 deletion api/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,6 +180,12 @@ <h1>YouTube RSS Scanner</h1>
copyText(decodeURIComponent(encodedText));
}

function openInReader(encodedText) {
const feedUrl = decodeURIComponent(encodedText);
const feedlySubscribeUrl = 'https://feedly.com/i/subscription/feed/' + encodeURIComponent(feedUrl);
window.open(feedlySubscribeUrl, '_blank', 'noopener,noreferrer');
}

async function getFeed() {
const url = document.getElementById('channelUrl').value.trim();
const resultDiv = document.getElementById('result');
Expand Down Expand Up @@ -220,7 +234,8 @@ <h1>YouTube RSS Scanner</h1>

html += '<div class="feed-row"><div class="feed-label">Selected RSS Feed:</div>';
html += '<div class="feed-link">' + (data.selected_feed || data.youtube_rss) + '</div>';
html += '<button class="copy-btn" onclick="copyEncodedText(\'' + encodeURIComponent((data.selected_feed || data.youtube_rss)) + '\')">Copy Selected RSS</button></div>';
html += '<button class="copy-btn" onclick="copyEncodedText(\'' + encodeURIComponent((data.selected_feed || data.youtube_rss)) + '\')">Copy Selected RSS</button>';
html += '<button class="open-reader-btn" onclick="openInReader(\'' + encodeURIComponent((data.selected_feed || data.youtube_rss)) + '\')">Open in Reader</button></div>';

if (data.official_feeds) {
html += '<div class="atom-output"><div class="feed-label">Official YouTube Feeds:</div>';
Expand Down
20 changes: 20 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -167,6 +175,12 @@ <h1>YouTube RSS Scanner</h1>
}
}


function openInReader(feedUrl) {
const feedlySubscribeUrl = 'https://feedly.com/i/subscription/feed/' + encodeURIComponent(feedUrl);
window.open(feedlySubscribeUrl, '_blank', 'noopener,noreferrer');
}

async function getFeed() {
const url = document.getElementById('channelUrl').value.trim();
const resultDiv = document.getElementById('result');
Expand Down Expand Up @@ -259,6 +273,12 @@ <h1>YouTube RSS Scanner</h1>
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');
Expand Down
9 changes: 9 additions & 0 deletions scripts/run_terminal_reader.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -lt 1 ]]; then
echo "Usage: $0 <feed_url> [--interval SECONDS] [--limit N]"
exit 1
fi

python scripts/terminal_reader.py "$@"
87 changes: 87 additions & 0 deletions scripts/terminal_reader.py
Original file line number Diff line number Diff line change
@@ -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())
30 changes: 29 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -167,6 +175,19 @@ <h1>YouTube RSS Scanner</h1>
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
: feedUrl.startsWith('http://')
? 'feed:' + feedUrl
: feedUrl;
Comment thread
DisabledAbel marked this conversation as resolved.

window.open(feedProtocolUrl, '_blank', 'noopener,noreferrer');
}

async function getFeed() {
const url = document.getElementById('channelUrl').value.trim();
const resultDiv = document.getElementById('result');
Expand Down Expand Up @@ -214,7 +235,8 @@ <h1>YouTube RSS Scanner</h1>

html += '<div class="feed-row"><div class="feed-label">Selected RSS Feed:</div>';
html += '<div class="feed-link">' + (data.selected_feed || data.youtube_rss) + '</div>';
html += '<button class="copy-btn" data-encoded="' + encodeURIComponent((data.selected_feed || data.youtube_rss)) + '">Copy Selected RSS</button></div>';
html += '<button class="copy-btn" data-encoded="' + encodeURIComponent((data.selected_feed || data.youtube_rss)) + '">Copy Selected RSS</button>';
html += '<button class="open-reader-btn" data-encoded="' + encodeURIComponent((data.selected_feed || data.youtube_rss)) + '">Open in Reader</button></div>';

if (data.official_feeds) {
html += '<div class="atom-output"><div class="feed-label">Official YouTube Feeds:</div>';
Expand Down Expand Up @@ -245,6 +267,12 @@ <h1>YouTube RSS Scanner</h1>
copyEncodedText(this.dataset.encoded);
});
});

resultDiv.querySelectorAll('.open-reader-btn').forEach(button => {
button.addEventListener('click', function() {
openInReader(this.dataset.encoded);
});
});
}
} catch (e) {
resultDiv.innerHTML = '<div class="error">Error: ' + e.message + '</div>';
Expand Down