#!/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}