#!/usr/bin/env python3 """One-shot setup for the digest's World + Local headlines sections. Creates the two miniflux categories (idempotent — no-op if present) and subscribes a curated default feed list into each. Skips any feed that's already subscribed in miniflux, so reruns are safe. Run from inside the news-digest-worker container so the MINIFLUX_* env vars are already set: docker exec news-digest-worker python3 /app/seed-headlines.py After this completes, the next digest run picks up the categories automatically — no app restart needed. """ from __future__ import annotations import os import sys import requests MINIFLUX_URL = os.environ.get("MINIFLUX_URL", "http://miniflux:8080").rstrip("/") MINIFLUX_USER = os.environ.get("MINIFLUX_USER", "lkraven") MINIFLUX_PASS = os.environ.get("MINIFLUX_PASSWORD", "") WORLD_CATEGORY = os.environ.get("DIGEST_MINIFLUX_WORLD_CATEGORY", "World") LOCAL_CATEGORY = os.environ.get("DIGEST_MINIFLUX_LOCAL_CATEGORY", "Local") # Default feed lists. Picked for: well-maintained RSS, low duplication # across the set, mix of styles (wire-service vs editorial). Edit here # and rerun the script to add/remove — already-subscribed feeds are # skipped, so adding a new one only adds. # (feed_url, display_title) — display_title overrides whatever miniflux # pulls from the feed's element. Keeps the dense headlines rail # tidy ("BBC" beats "BBC News"; "Al Jazeera" beats the 60-char default). WORLD_FEEDS = [ ("http://feeds.bbci.co.uk/news/world/rss.xml", "BBC"), ("https://feeds.npr.org/1001/rss.xml", "NPR"), ("https://www.aljazeera.com/xml/rss/all.xml", "Al Jazeera"), ] LOCAL_FEEDS = [ ("https://www.latimes.com/local/rss2.0.xml", "LA Times"), ("https://www.latimes.com/california/rss2.0.xml", "LA Times CA"), ("https://voiceofoc.org/feed/", "Voice of OC"), # OC Register blocks miniflux's fetcher (403). KTLA / Daily Pilot # are options if more OC-specific coverage is needed later. ] S = requests.Session() S.auth = (MINIFLUX_USER, MINIFLUX_PASS) def get(path: str) -> object: r = S.get(f"{MINIFLUX_URL}{path}", timeout=20) r.raise_for_status() return r.json() def post(path: str, body: dict) -> requests.Response: return S.post(f"{MINIFLUX_URL}{path}", json=body, timeout=30) def ensure_category(title: str) -> int: """Return the ID of the category named `title`, creating it if missing.""" cats = get("/v1/categories") for c in cats: if c["title"].lower() == title.lower(): print(f" category {title!r} already exists (id={c['id']})") return c["id"] r = post("/v1/categories", {"title": title}) r.raise_for_status() cat_id = r.json()["id"] print(f" created category {title!r} (id={cat_id})") return cat_id def find_feed_id(feed_url: str) -> int | None: """Return miniflux feed ID for `feed_url` if subscribed; else None. Compares with both the original URL and miniflux's canonical rewrite (some feeds get http→https'd or trailing-slashed at fetch time).""" feeds = get("/v1/feeds") target = feed_url.rstrip("/") for f in feeds: url = (f.get("feed_url") or "").rstrip("/") if url == target or url == target.replace("http://", "https://", 1): return f["id"] return None def set_feed_title(feed_id: int, title: str) -> None: r = S.put(f"{MINIFLUX_URL}/v1/feeds/{feed_id}", json={"title": title}, timeout=20) if not r.ok: print(f" ! rename failed ({r.status_code}): {r.text[:120]}") def subscribe(feed_url: str, display_title: str, category_id: int) -> None: existing = find_feed_id(feed_url) if existing is not None: print(f" skip (already subscribed): {feed_url}") set_feed_title(existing, display_title) return r = post("/v1/feeds", {"feed_url": feed_url, "category_id": category_id}) if r.status_code in (200, 201): new_id = r.json().get("feed_id") print(f" + subscribed: {feed_url}") if new_id: set_feed_title(new_id, display_title) else: # Most common failure mode is a feed-discovery hiccup at # miniflux's end — log loudly and continue so one bad URL # doesn't block the rest. print(f" ! FAILED ({r.status_code}): {feed_url} {r.text[:200]}") def main() -> int: if not MINIFLUX_PASS: print("MINIFLUX_PASSWORD not set — bailing", file=sys.stderr) return 2 print(f"miniflux: {MINIFLUX_URL} user={MINIFLUX_USER}") print(f"\n→ {WORLD_CATEGORY}") world_id = ensure_category(WORLD_CATEGORY) for url, title in WORLD_FEEDS: subscribe(url, title, world_id) print(f"\n→ {LOCAL_CATEGORY}") local_id = ensure_category(LOCAL_CATEGORY) for url, title in LOCAL_FEEDS: subscribe(url, title, local_id) print("\ndone — next digest run will pick up the new sections.") return 0 if __name__ == "__main__": sys.exit(main())