dindizz commited on
Commit
00be35b
·
verified ·
1 Parent(s): 3e68a4b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +152 -0
app.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime as dt
2
+ import html
3
+ import textwrap
4
+ from typing import List, Tuple
5
+
6
+ import feedparser
7
+ import gradio as gr
8
+ import requests
9
+
10
+ DEFAULT_URL = "https://sachet.ndma.gov.in/cap_public_website/rss/rss_india.xml"
11
+ UA = "MinimalRSS/1.0 (+https://huggingface.co/spaces)"
12
+
13
+ def _fetch(url: str, timeout: int = 12) -> bytes:
14
+ resp = requests.get(url, headers={"User-Agent": UA}, timeout=timeout)
15
+ resp.raise_for_status()
16
+ return resp.content
17
+
18
+ def _truncate(text: str, n: int = 220) -> str:
19
+ text = " ".join(text.split())
20
+ return text if len(text) <= n else text[: n - 1].rstrip() + "…"
21
+
22
+ def _format_time(struct_time) -> str:
23
+ if not struct_time:
24
+ return ""
25
+ # Display in IST by default (Asia/Kolkata = UTC+5:30). We’ll label UTC to avoid timezone assumptions.
26
+ # Feed times are usually UTC; to stay unambiguous, show ISO 8601/Z.
27
+ try:
28
+ return dt.datetime(*struct_time[:6], tzinfo=dt.timezone.utc).isoformat().replace("+00:00", "Z")
29
+ except Exception:
30
+ return ""
31
+
32
+ def render_feed(url: str, max_items: int, show_summaries: bool) -> Tuple[str, str]:
33
+ try:
34
+ raw = _fetch(url.strip() or DEFAULT_URL)
35
+ parsed = feedparser.parse(raw)
36
+ except Exception as e:
37
+ return "", f"⚠️ Could not load the feed. {type(e).__name__}: {e}"
38
+
39
+ title = parsed.feed.get("title", "Feed")
40
+ subtitle = parsed.feed.get("subtitle", "")
41
+ updated = parsed.feed.get("updated_parsed")
42
+
43
+ header_html = f"""
44
+ <div class="header">
45
+ <div class="feed-title">{html.escape(title)}</div>
46
+ {"<div class='feed-sub'>"+html.escape(subtitle)+"</div>" if subtitle else ""}
47
+ <div class="feed-meta">Updated: {html.escape(_format_time(updated) or "—")}</div>
48
+ </div>
49
+ """
50
+
51
+ items_html: List[str] = []
52
+ for entry in parsed.entries[:max_items]:
53
+ etitle = html.escape(entry.get("title", "Untitled"))
54
+ link = entry.get("link", "#")
55
+ published = _format_time(entry.get("published_parsed"))
56
+ summary = entry.get("summary", "") or entry.get("description", "")
57
+ # Remove very long XML artifacts
58
+ summary = html.escape(_truncate(summary, 500))
59
+
60
+ caps = []
61
+ for key in ("category", "tags"):
62
+ if key in entry and entry[key]:
63
+ if key == "category":
64
+ caps.append(str(entry["category"]))
65
+ else:
66
+ for t in entry["tags"]:
67
+ lab = t.get("term") or t.get("label")
68
+ if lab:
69
+ caps.append(str(lab))
70
+ caps = [c for c in [c.strip() for c in caps] if c]
71
+
72
+ cap_html = (
73
+ "<div class='caps'>" + " ".join(f"<span class='cap'>{html.escape(c)}</span>" for c in caps) + "</div>"
74
+ if caps
75
+ else ""
76
+ )
77
+
78
+ item = f"""
79
+ <li class="item">
80
+ <a class="title" href="{html.escape(link)}" target="_blank" rel="noopener noreferrer">{etitle}</a>
81
+ <div class="meta">{("Published: " + published) if published else ""}</div>
82
+ {cap_html}
83
+ {f"<div class='summary'>{summary}</div>" if show_summaries and summary else ""}
84
+ </li>
85
+ """
86
+ items_html.append(item)
87
+
88
+ if not items_html:
89
+ items_html.append("<li class='item empty'>No items found.</li>")
90
+
91
+ body_html = "<ul class='list'>" + "\n".join(items_html) + "</ul>"
92
+
93
+ full_html = f"""
94
+ <div class="wrap">
95
+ {header_html}
96
+ {body_html}
97
+ </div>
98
+ """
99
+
100
+ return full_html, ""
101
+
102
+ MINIMAL_CSS = """
103
+ :root { --fg:#111; --muted:#666; --bg:#fff; --card:#fafafa; --link:#0b57d0; }
104
+ @media (prefers-color-scheme: dark) {
105
+ :root { --fg:#eee; --muted:#aaa; --bg:#0b0b0b; --card:#141414; --link:#7fb0ff; }
106
+ }
107
+ *{box-sizing:border-box}
108
+ body{{background:var(--bg)}}
109
+ .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}
110
+ .header{margin:12px 0 8px 0}
111
+ .feed-title{font-weight:700;font-size:20px}
112
+ .feed-sub{color:var(--muted);margin-top:2px}
113
+ .feed-meta{color:var(--muted);font-size:13px;margin-top:6px}
114
+ .list{list-style:none;padding:0;margin:16px 0}
115
+ .item{background:var(--card);border:1px solid rgba(127,127,127,.2);border-radius:12px;padding:14px 16px;margin:10px 0}
116
+ .item .title{font-weight:600;text-decoration:none;color:var(--link)}
117
+ .item .title:hover{text-decoration:underline}
118
+ .item .meta{color:var(--muted);font-size:13px;margin-top:6px}
119
+ .caps{margin-top:8px;display:flex;gap:6px;flex-wrap:wrap}
120
+ .cap{border:1px solid rgba(127,127,127,.25);padding:2px 8px;border-radius:999px;font-size:12px;color:var(--muted)}
121
+ .summary{margin-top:10px;white-space:pre-wrap}
122
+ .empty{color:var(--muted);text-align:center}
123
+ .footer{max-width:920px;margin:8px auto 24px auto;padding:0 16px;color:var(--muted);font:12px/1.4 system-ui}
124
+ """
125
+
126
+ with gr.Blocks(css=MINIMAL_CSS, fill_height=True, theme=gr.themes.Soft()) as demo:
127
+ gr.Markdown("### NDMA Sachet — Minimal RSS Viewer")
128
+
129
+ with gr.Row():
130
+ url_in = gr.Textbox(
131
+ label="RSS URL",
132
+ value=DEFAULT_URL,
133
+ placeholder="Paste an RSS/Atom URL…",
134
+ max_lines=1
135
+ )
136
+ max_items = gr.Slider(5, 50, value=20, step=1, label="Items")
137
+ show_summaries = gr.Checkbox(value=True, label="Show summaries")
138
+ refresh = gr.Button("Refresh", variant="primary")
139
+
140
+ out_html = gr.HTML()
141
+ out_err = gr.Markdown(elem_classes=["footer"])
142
+
143
+ def _go(u, m, s):
144
+ return render_feed(u, int(m), bool(s))
145
+
146
+ # Initial load
147
+ demo.load(_go, [url_in, max_items, show_summaries], [out_html, out_err])
148
+ # Manual refresh
149
+ refresh.click(_go, [url_in, max_items, show_summaries], [out_html, out_err])
150
+
151
+ if __name__ == "__main__":
152
+ demo.launch()