#!/usr/bin/env python3 """ Enhanced hourly news briefing with long summaries, yourls links, and continuous marquee scroll. Fetches from FreshRSS, summarizes with Ollama, generates short URLs, rescrolls continuously. """ import os import sys import json import time import requests import re import schedule import textwrap from datetime import datetime # Debug logging DEBUG_LOG = "/tmp/briefing_debug.log" def debug_log(msg): with open(DEBUG_LOG, "a") as f: f.write("[{}] {}\n".format(datetime.now().strftime('%H:%M:%S'), msg)) f.flush() # Load config from local copy CONFIG_PATH = "/home/pi/piNail2/briefing_config.json" with open(CONFIG_PATH, "r") as f: config = json.load(f) FRESHRSS_URL = config["freshrss_url"] FRESHRSS_USER = config["freshrss_user"] FRESHRSS_TOKEN = config["freshrss_api_key"] OLLAMA_URL = config["ollama_url"] OLLAMA_MODEL = config.get("ollama_model", "mistral") YOURLS_URL = config.get("yourls_url", "http://192.168.0.152:8080/yourls-api.php") YOURLS_USER = config.get("yourls_user", "admin") YOURLS_PASS = config.get("yourls_pass", "REDACTED_PASSWORD") def fetch_news(count=12): """Fetch top articles from FreshRSS.""" login_url = "{}/api/greader.php/accounts/ClientLogin".format(FRESHRSS_URL) login_data = {"Email": FRESHRSS_USER, "Passwd": FRESHRSS_TOKEN} try: login_resp = requests.post(login_url, data=login_data, timeout=15) if login_resp.status_code != 200: return [] auth_token = "" for line in login_resp.text.splitlines(): if line.startswith("Auth="): auth_token = line.split("=", 1)[1] break if not auth_token: return [] except Exception as e: print("[!] Login exception: {}".format(e), file=sys.stderr) return [] api_url = "{}/api/greader.php/reader/api/0/stream/contents/user/-/state/com.google/reading-list".format(FRESHRSS_URL) params = {"n": count + 5, "xt": "user/-/state/com.google/read", "output": "json"} headers = {"Authorization": "GoogleLogin auth={}".format(auth_token)} try: response = requests.get(api_url, params=params, headers=headers, timeout=15) if response.status_code != 200: return [] data = response.json() items = [] for item in data.get("items", [])[:count]: title = item.get("title", "") summary = item.get("summary", {}).get("content", "") or item.get("content", {}).get("content", "") source = item.get("origin", {}).get("title", "Unknown Source") link = item.get("alternate", [{}])[0].get("href", "") if item.get("alternate") else "" summary_clean = re.sub('<[^<]+?>', '', summary).strip() items.append({ "title": title, "summary": summary_clean, "source": source, "link": link }) return items except Exception as e: print("[!] Exception fetching news: {}".format(e), file=sys.stderr) return [] def summarize_article(text): """Summarize article with Ollama (longer, 4-6 sentences).""" if not text: return "" payload = { "model": OLLAMA_MODEL, "prompt": "Provide a detailed summary in 4-6 sentences: {}".format(text[:1000]), "stream": False, "options": {"num_ctx": 2048, "temperature": 0.3} } try: response = requests.post("{}/api/generate".format(OLLAMA_URL), json=payload, timeout=60) return response.json().get("response", "").strip() except: return text[:400] + "..." def create_yourls_link(original_url, index): """Generate short URL using yourls with pi____ format.""" if not original_url: debug_log("No URL provided") return "" try: # Create sequential alias: pi0, pi1, pi2, etc. alias = "pi{}".format(index) debug_log("Creating yourls link with alias: {}".format(alias)) payload = { "action": "shorturl", "url": original_url, "keyword": alias, "username": YOURLS_USER, "password": YOURLS_PASS, "format": "json" } response = requests.post(YOURLS_URL, data=payload, timeout=10) data = response.json() # Check if we got a shorturl (success or URL already exists) if data.get("shorturl"): short_link = data.get("shorturl", "") debug_log("Created link {}: {}".format(alias, short_link)) return short_link # Try random alias if keyword alias is taken if "already exists" in data.get("message", "").lower() or "keyword already exists" in data.get("message", "").lower(): debug_log("Alias {} taken, trying random".format(alias)) payload_random = { "action": "shorturl", "url": original_url, "username": YOURLS_USER, "password": YOURLS_PASS, "format": "json" } response = requests.post(YOURLS_URL, data=payload_random, timeout=10) data = response.json() if data.get("shorturl"): short_link = data.get("shorturl", "") debug_log("Created fallback link: {}".format(short_link)) return short_link debug_log("yourls failed: {}".format(data.get("message", "unknown error"))) return "" except Exception as e: debug_log("yourls exception: {}".format(e)) return "" def render_marquee(articles, tty): """Render articles as continuous vertical marquee.""" # Build full content once full_content = "" for i, article in enumerate(articles, 1): full_content += "\n{}\n".format("="*70) full_content += "[{}/{}] {}\n".format(i, len(articles), article['source']) full_content += "{}\n\n".format("─" * 70) full_content += " {}\n".format(article['title']) full_content += " {}\n\n".format("─" * 70) summary = article.get('ai_summary', article['summary'][:300]) wrapped = textwrap.fill(summary, width=66, initial_indent=" ", subsequent_indent=" ") full_content += "{}\n".format(wrapped) short_link = article.get('short_link', article.get('link', '')) if short_link: full_content += "\n Link: {}\n".format(short_link) full_content += "\n" full_content += "\n{}\n".format("="*70) full_content += " --- END OF BRIEFING, RESCROLLING IN 30 SECONDS ---\n" full_content += "{}\n".format("="*70) return full_content def scroll_content(tty, content): """Scroll content at reading speed, continuous loop without waiting.""" char_delay = 0.025 # 25ms per char = ~40 chars/sec while True: for char in content: tty.write(char) tty.flush() if char == '\n': time.sleep(0.1) else: time.sleep(char_delay) # Loop back immediately without pause def hourly_briefing(): """Fetch, summarize, and display briefing. Continuously scrolls until next hourly refresh.""" try: tty = open('/dev/tty1', 'w') except Exception: tty = sys.stdout print("[*] Fetching briefing at {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')), file=sys.stderr) articles = fetch_news(count=12) if not articles: tty.write(" [!] No articles fetched\n") tty.flush() print("[!] No articles fetched", file=sys.stderr) if tty is not sys.stdout: tty.close() return # Summarize and generate yourls links debug_log("Summarizing {} articles...".format(len(articles))) for i, article in enumerate(articles): debug_log("Article {}/{}: summarizing...".format(i+1, len(articles))) article['ai_summary'] = summarize_article(article['summary']) debug_log("Article {}/{}: creating link...".format(i+1, len(articles))) article['short_link'] = create_yourls_link(article['link'], i) debug_log("Article {}/{}: done. Link={}".format(i+1, len(articles), article.get('short_link', ''))) # Render and display header timestamp = datetime.now().strftime("%A, %B %d, %Y | %I:%M %p") tty.write("\033[2J\033[H\033[?25l") # Clear and hide cursor tty.write("\n{}\n".format("="*70)) tty.write(" PINAIL NEWS BRIEFING - {}\n".format(timestamp)) tty.write(" {} stories\n".format(len(articles))) tty.write("{}\n\n".format("="*70)) tty.flush() time.sleep(1) # Build full marquee content full_content = render_marquee(articles, tty) # Continuously scroll content until next scheduled run print("[*] Starting continuous scroll", file=sys.stderr) try: scroll_content(tty, full_content) except KeyboardInterrupt: pass finally: tty.write('\033[?25h') # Restore cursor tty.flush() if tty is not sys.stdout: tty.close() def main(): """Schedule hourly briefing.""" schedule.every().hour.at(":00").do(hourly_briefing) print("[*] piNail News Briefing scheduled for every hour", file=sys.stderr) next_run = datetime.now().replace(minute=0, second=0, microsecond=0) print("[*] Next run at {}".format(next_run.strftime('%H:%M')), file=sys.stderr) # Run first briefing immediately hourly_briefing() # Keep scheduler alive (will interrupt scroll_content when next hour arrives) while True: schedule.run_pending() time.sleep(1) if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n[*] Briefing scheduler stopped", file=sys.stderr) sys.exit(0) except Exception as e: print("[!] Fatal error: {}".format(e), file=sys.stderr) raise