File size: 5,759 Bytes
00be35b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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"""
    <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", "")
        # 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 = (
            "<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))

    # 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()