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"""
"""
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 = "" + "\n".join(items_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()