import datetime as dt import html import textwrap from typing import List, Tuple import feedparser import gradio as gr import requests DEFAULT_URL = "https://sachet.ndma.gov.in/cap_public_website/rss/rss_india.xml" UA = "MinimalRSS/1.0 (+https://huggingface.co/spaces)" def _fetch(url: str, timeout: int = 12) -> bytes: resp = requests.get(url, headers={"User-Agent": UA}, timeout=timeout) resp.raise_for_status() return resp.content def _truncate(text: str, n: int = 220) -> str: text = " ".join(text.split()) return text if len(text) <= n else text[: n - 1].rstrip() + "…" def _format_time(struct_time) -> str: if not struct_time: return "" # Display in IST by default (Asia/Kolkata = UTC+5:30). We’ll label UTC to avoid timezone assumptions. # Feed times are usually UTC; to stay unambiguous, show ISO 8601/Z. try: return dt.datetime(*struct_time[:6], tzinfo=dt.timezone.utc).isoformat().replace("+00:00", "Z") except Exception: return "" def render_feed(url: str, max_items: int, show_summaries: bool) -> Tuple[str, str]: try: raw = _fetch(url.strip() or DEFAULT_URL) parsed = feedparser.parse(raw) except Exception as e: return "", f"⚠️ Could not load the feed. {type(e).__name__}: {e}" title = parsed.feed.get("title", "Feed") subtitle = parsed.feed.get("subtitle", "") updated = parsed.feed.get("updated_parsed") header_html = f"""
{html.escape(title)}
{"
"+html.escape(subtitle)+"
" if subtitle else ""}
Updated: {html.escape(_format_time(updated) or "—")}
""" items_html: List[str] = [] for entry in parsed.entries[:max_items]: etitle = html.escape(entry.get("title", "Untitled")) link = entry.get("link", "#") published = _format_time(entry.get("published_parsed")) summary = entry.get("summary", "") or entry.get("description", "") # Remove very long XML artifacts summary = html.escape(_truncate(summary, 500)) caps = [] for key in ("category", "tags"): if key in entry and entry[key]: if key == "category": caps.append(str(entry["category"])) else: for t in entry["tags"]: lab = t.get("term") or t.get("label") if lab: caps.append(str(lab)) caps = [c for c in [c.strip() for c in caps] if c] cap_html = ( "
" + " ".join(f"{html.escape(c)}" for c in caps) + "
" if caps else "" ) item = f"""
  • {etitle}
    {("Published: " + published) if published else ""}
    {cap_html} {f"
    {summary}
    " if show_summaries and summary else ""}
  • """ items_html.append(item) if not items_html: items_html.append("
  • No items found.
  • ") body_html = "" full_html = f"""
    {header_html} {body_html}
    """ return full_html, "" MINIMAL_CSS = """ :root { --fg:#111; --muted:#666; --bg:#fff; --card:#fafafa; --link:#0b57d0; } @media (prefers-color-scheme: dark) { :root { --fg:#eee; --muted:#aaa; --bg:#0b0b0b; --card:#141414; --link:#7fb0ff; } } *{box-sizing:border-box} body{{background:var(--bg)}} .wrap{max-width:920px;margin:24px auto;padding:0 16px;color:var(--fg);font:16px/1.55 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial} .header{margin:12px 0 8px 0} .feed-title{font-weight:700;font-size:20px} .feed-sub{color:var(--muted);margin-top:2px} .feed-meta{color:var(--muted);font-size:13px;margin-top:6px} .list{list-style:none;padding:0;margin:16px 0} .item{background:var(--card);border:1px solid rgba(127,127,127,.2);border-radius:12px;padding:14px 16px;margin:10px 0} .item .title{font-weight:600;text-decoration:none;color:var(--link)} .item .title:hover{text-decoration:underline} .item .meta{color:var(--muted);font-size:13px;margin-top:6px} .caps{margin-top:8px;display:flex;gap:6px;flex-wrap:wrap} .cap{border:1px solid rgba(127,127,127,.25);padding:2px 8px;border-radius:999px;font-size:12px;color:var(--muted)} .summary{margin-top:10px;white-space:pre-wrap} .empty{color:var(--muted);text-align:center} .footer{max-width:920px;margin:8px auto 24px auto;padding:0 16px;color:var(--muted);font:12px/1.4 system-ui} """ with gr.Blocks(css=MINIMAL_CSS, fill_height=True, theme=gr.themes.Soft()) as demo: gr.Markdown("### NDMA Sachet — Minimal RSS Viewer") with gr.Row(): url_in = gr.Textbox( label="RSS URL", value=DEFAULT_URL, placeholder="Paste an RSS/Atom URL…", max_lines=1 ) max_items = gr.Slider(5, 50, value=20, step=1, label="Items") show_summaries = gr.Checkbox(value=True, label="Show summaries") refresh = gr.Button("Refresh", variant="primary") out_html = gr.HTML() out_err = gr.Markdown(elem_classes=["footer"]) def _go(u, m, s): return render_feed(u, int(m), bool(s)) # Initial load demo.load(_go, [url_in, max_items, show_summaries], [out_html, out_err]) # Manual refresh refresh.click(_go, [url_in, max_items, show_summaries], [out_html, out_err]) if __name__ == "__main__": demo.launch()