diff --git a/trending.py b/trending.py
new file mode 100644
index 0000000..7a55db1
--- /dev/null
+++ b/trending.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+import argparse, datetime as dt, html, json, pathlib, re, urllib.parse, urllib.request
+from html import unescape
+
+ROOT = pathlib.Path(__file__).resolve().parent
+PERIODS = [("daily","today"),("weekly","this week"),("monthly","this month")]
+
+def fetch(period):
+ url = "https://github.com/trending?" + urllib.parse.urlencode({"since": period})
+ req = urllib.request.Request(url, headers={
+ "User-Agent": "notplanned-trending/0.1",
+ "Accept": "text/html,application/xhtml+xml",
+ })
+ with urllib.request.urlopen(req, timeout=20) as r:
+ return r.read().decode("utf-8", "replace")
+
+def clean(fragment):
+ fragment = re.sub(r"<[^>]+>", " ", fragment)
+ return " ".join(unescape(fragment).split())
+
+def parse(period, body):
+ articles = re.findall(r'
]+class="[^"]*color-fg-muted[^"]*"[^>]*>(.*?)
', article, re.S|re.I) + if pm: + desc = clean(pm.group(1)) + lang = "" + lm = re.search(r'itemprop="programmingLanguage"[^>]*>(.*?)', article, re.S|re.I) + if lm: + lang = clean(lm.group(1)) + stars = None + sm = re.search(r'([0-9][0-9,]*)\s+stars\s+' + re.escape(period_phrase), clean(article), re.I) + if sm: + stars = int(sm.group(1).replace(",","")) + rows.append({ + "repo": repo, + "url": f"https://github.com/{repo}", + "rank": rank, + "description": desc, + "language": lang, + "stars_gained": stars, + }) + return rows + +def build(): + now = dt.datetime.now(dt.timezone.utc).isoformat() + periods = {} + for period, _ in PERIODS: + periods[period] = parse(period, fetch(period)) + + merged = {} + for period, rows in periods.items(): + for row in rows: + x = merged.setdefault(row["repo"], { + "repo": row["repo"], + "url": row["url"], + "description": row["description"], + "language": row["language"], + "daily_rank": None, "weekly_rank": None, "monthly_rank": None, + "daily_stars": None, "weekly_stars": None, "monthly_stars": None, + }) + x[f"{period}_rank"] = row["rank"] + x[f"{period}_stars"] = row["stars_gained"] + if not x["description"] and row["description"]: + x["description"] = row["description"] + if not x["language"] and row["language"]: + x["language"] = row["language"] + + for x in merged.values(): + x["periods"] = sum(x[f"{p}_rank"] is not None for p, _ in PERIODS) + # Mechanical prominence only: preserve GitHub rank, favor appearing in more windows. + x["prominence"] = sum( + (26 - min(x[f"{p}_rank"], 25)) * w + for p, w in [("daily",3),("weekly",2),("monthly",1)] + if x[f"{p}_rank"] is not None + ) + + items = sorted(merged.values(), key=lambda x: ( + -x["periods"], + x["daily_rank"] if x["daily_rank"] is not None else 999, + x["weekly_rank"] if x["weekly_rank"] is not None else 999, + x["monthly_rank"] if x["monthly_rank"] is not None else 999, + x["repo"].lower(), + )) + return {"generated_at": now, "source":"https://github.com/trending", "periods": periods, "merged": items} + +def card(x): + def badge(label, rank, stars): + if rank is None: return "" + gain = f" · +{stars:,}" if stars is not None else "" + return f'{label} #{rank}{gain}' + badges = "".join([ + badge("day", x["daily_rank"], x["daily_stars"]), + badge("week", x["weekly_rank"], x["weekly_stars"]), + badge("month", x["monthly_rank"], x["monthly_stars"]), + ]) + desc = html.escape(x["description"]) + lang = html.escape(x["language"] or "—") + repo = html.escape(x["repo"]) + url = html.escape(x["url"], quote=True) + return f""" +{desc}
+