HumbleRSS

Mein erster RSS-Service

1. Motivation

Ich habe ein Problem: Ich gucke regelmäßig auf HumbleBundle, ob es ein neues Book Bundle gibt. Lese ich immer als Bücher in den Bundles die ich kaufe? Haha, nein. Aber ich behalte die Seite trotzdem gerne im Auge, um nicht das eine Bundle zu verpassen. FOMO in Reinform, würde ich sagen.

Weil es immer und für alles eine technische Lösung gibt1 und weil ich in letzter Zeit immer mal wieder über die Idee stolpere, einen RSS-Feed zu implementieren, habe ich Humble RSS implementiert: Ein RSS-Feed, der immer die aktuellsten Book Bundles von Humble Bundle anzeigt.

Mit diesem Blog-Post möchte ich dir zeigen, wie ich das angestellt habe.

2. requirements.txt

Python war meine erste “richtige” Programmiersprache und seitdem Tool meiner Wahl, um alles mögliche zu implementieren. Es gibt für (fast) alles eine Library und durch die dynamische Typisierung kann ich auch ohne Probleme dynamischen Webcontent verarbeiten.

Weil die Standard-Library aber nicht für alles eine ideale Lösung hat, greife ich noch auf folgende Packages zurück:

  • beautifulsoup4: Zum Parsing von HTML-Dokumenten.
  • feedgen: Zum Erstellen des RSS-Feeds.
  • flask: Das Webframework, in dem ich den Web-Service implementiere.
  • flask-caching: Ein Cache, damit nicht jede Anfrage an den RSS-Feed eine HTTP-Anfrage zu HumbleBundle stellen muss.
  • html5lib: Eine Engine für beautifulsoup4.
  • pytz: feedgen braucht timezone-aware datetime Objekte; pytz hilft dabei.
  • requests: Der HTTP-Client, mit dem ich den Content von HumbleBundle abhole.
  • waitress: Der WSGI-Server meiner Wahl.

3. Let’s hack!

Humble RSS läuft in den folgenden Schritten ab:

  1. Hole die Daten von HumbleBundle.
  2. Sortiere die Daten nach Zeitstempel.
  3. Erstelle einen Feed basierend auf den Daten von HumbleBundle.
  4. Erzeuge eine RSS-Response, die dann von Flask bereitgestellt wird.

3.1. HTTP-Anfrage zu HumbleBundle

Wie eingangs erwähnt, benutze ich requests für die HTTP-Anfragen zu HumbleBundle (um genau zu sein zur Unterseite mit den Book Bundles):

resp = requests.get("https://humblebundle.com/books")
if resp.status_code != 200:
    return (
        f"Error: unexpected status code: {resp.status_code}",
        503,
    )

Korrektes Error-Handling ist immer eine gute Idee. Wenn nicht für die End-User*innen, dann wenigstens für die verwirrte Person, die hinterher in Server-Logs nach möglichen Fehlerursachen suchen muss. (ja, ich spreche aus Erfahrung)

3.2. Verarbeiten und sortieren der Daten

Da HumbleBundle meines Wissen nach keine API zur Verfügung stellt, sondern nur HTML-Responses liefert, muss der Content zunächst mit BeautifulSoup geparst werden:

soup = BeautifulSoup(resp.content, "html5lib")
raw_json = soup.find("script", {"id": "landingPage-json-data"}).contents[0]
data = json.loads(raw_json)
books = data["data"]["books"]["mosaic"][0]["products"]

Die ID für das <script>-Tag habe ich manuell aus dem Seiten-Quellcode gezogen. Alles noch Handarbeit hier. 😄

Damit die Posts später in einer vernünftigen Reihenfolge im RSS-Feed gelistet werden, müssen die Einträge erst sortiert werden. Ich habe mich hier für eine absteigende Sortierung nach Veröffentlichungszeitpunkt entschieden, weil die aktuellsten Bundles für mich die wichtigsten sind (und das wichtigste sollte immer zuerst kommen 😋):

sorted_books = sorted(
    books,
    key=lambda b: datetime.fromisoformat(b["start_date|datetime"]),
    reverse=False,
)

3.3. Feed erstellen

Um den Feed zu erstellen, benutze ich das feedgen Package. Das unterstützt per Extension auch die Dublin Core Ontologie, bis jetzt habe ich sie aber noch nicht in meinen RSS-Feed eingebaut.

Um mit dem Package einen Feed zu erstellen, definiere ich zuerst ein FeedGenerator-Objekt mit entsprechenden Metadaten:

fg = FeedGenerator()
fg.link(href="https://humblerss.herokuapp.com")
fg.title("Humble RSS")
fg.author({"name": "shimst3r", "email": "@shimst3r@chaos.social"})
fg.subtitle("Humble RSS - Your humble RSS feed for HumbleBundle news.")
fg.language("en")

Danach iteriere ich über die Bücher-Liste aus dem vorherigen Schritt und füge einzelne FeedEntrys hinzu:

for book in books:
    fe = fg.add_entry()
    fe.title(book["tile_short_name"])
    fe.link(href=f"https://humblebundle.com/{book['product_url']}")
    fe.content(book["detailed_marketing_blurb"])
    dt = datetime.fromisoformat(book["start_date|datetime"])
    fe.pubDate(pytz.utc.localize(dt))

Ein kleiner Fallstrick beim pubDate: Das Package erwartet timezone-aware datetime Objekte, also welche mit Zeitzoneninformation. Weil ich faul bin, benutze ich dafür die Funktion pytz.utc.localize(). Zeitzonen sind gruselig genug, um auf fertige Lösungen zurückgreifen zu dürfen. 😇

3.4. RSS-Response erzeugen und bereitstellen

Fast fertig! Der letzte Schritt ist das Erstellen der RSS-Antwort. Dafür erstelle ich erst eine flask.Response und setze dann händisch den richtigen Content-Type:

response = make_response(fg.rss_str(pretty=True))
response.headers.set("Content-Type", "application/rss+xml")

Die Response wird dann einfach per GET-Endpunkt (plus Caching) zur Verfügung gestellt:

@app.get("/")
@cache.cached()
def rss():
    books = get_books()
    fg = generate_feed(books)
    response = make_response(fg)

    return response, 200

Den ganzen Code findest du unter shimst3r/humble-rss auf GitHub. Die Struktur sieht da etwas anders aus, weil ich u.a. eine Application Factory und Blueprints verwendet habe.

4. Deployment

Damit der RSS-Service auch tatsächlich von anderen genutzt werden kann, hoste ich ihn auf Heroku. Es gibt viele Alternativen, Heroku war für mich nur die einfachste.

4.1. Setup

Heroku hat ein gutes CLI, heroku. Es gibt verschiedene Möglichkeiten, das zu installieren. Weil ich die App auf meinem Mac Mini entwickelt habe, habe ich sie mittels Homebrew installiert:

$ brew tap heroku/brew 
$ brew install heroku

4.2. App erstellen und umbenennen

Nachdem ich mich mit dem CLI eingeloggt habe, ist das Erstellen einer neuen App nur ein Befehl:

$ heroku create

Der Befehl gibt der App einen zufällig generierten Namen, also habe ich sie umbenannt:

$ heroku rename humblerss

Kurz die Daumen drücken und dann freuen, dass der Name noch nicht vergeben war. 😬

4.3. Preparing for liftoff!

Damit Heroku den RSS-Service auch tatsächlich betreiben kann, muss ich eine Procfile erstellen. In meinem Fall ist sie denkbar einfach:

web: waitress-serve --port=${PORT} humble_rss.wsgi:app

Erneut ein kleiner Fallstrick: Heroku vergibt den Port dynamisch, ich sollte in Zukunft das --port=${PORT} also besser nicht wieder vergessen. 😬

Danach habe ich noch einen Webhook für mein GitHub-Repository eingestellt (hier die Anleitung), du kannst aber auch einfach per git push deployen:

$ git push heroku main

5. Fazit

Ich habe dir in diesem Blog-Post vorgestellt, wie ich meinen RSS-Service HumbleRSS implementiert habe (und warum). Sowohl der Post, als auch der Service an sich haben mir wieder viel beigebracht; hoffentlich ging es dir auch so. 😌

In Zukunft werde ich noch ein paar Änderungen am Service vornehmen (u.a. Dublin Core umsetzen), aber ansonsten bin ich schon sehr mit dem Ergebnis zufrieden.

Bis zum nächsten Mal. Be gay, do crime! 🦄🏴

1

Wenn du glaubst, dass das stimmt, lies bitte Fairness and Abstraction in Sociotechnical Systems von Selbst et al., vor allem den Abschnitt zur Solutionism Trap. 🤓