| 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 "" |
| |
| |
| 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""" |
| <div class="header"> |
| <div class="feed-title">{html.escape(title)}</div> |
| {"<div class='feed-sub'>"+html.escape(subtitle)+"</div>" if subtitle else ""} |
| <div class="feed-meta">Updated: {html.escape(_format_time(updated) or "—")}</div> |
| </div> |
| """ |
|
|
| 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", "") |
| |
| 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 = ( |
| "<div class='caps'>" + " ".join(f"<span class='cap'>{html.escape(c)}</span>" for c in caps) + "</div>" |
| if caps |
| else "" |
| ) |
|
|
| item = f""" |
| <li class="item"> |
| <a class="title" href="{html.escape(link)}" target="_blank" rel="noopener noreferrer">{etitle}</a> |
| <div class="meta">{("Published: " + published) if published else ""}</div> |
| {cap_html} |
| {f"<div class='summary'>{summary}</div>" if show_summaries and summary else ""} |
| </li> |
| """ |
| items_html.append(item) |
|
|
| if not items_html: |
| items_html.append("<li class='item empty'>No items found.</li>") |
|
|
| body_html = "<ul class='list'>" + "\n".join(items_html) + "</ul>" |
|
|
| full_html = f""" |
| <div class="wrap"> |
| {header_html} |
| {body_html} |
| </div> |
| """ |
|
|
| 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)) |
|
|
| |
| demo.load(_go, [url_in, max_items, show_summaries], [out_html, out_err]) |
| |
| refresh.click(_go, [url_in, max_items, show_summaries], [out_html, out_err]) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|