Motivation

RBTVOD ermöglicht es die YouTube Mediathek der Rocketbeans nach Bohnen-Kombinationen und weiteren Details exakt zu durchsuchen. Hier beantworten wir die Frage was haben Nils & Gunnar exklusiv gemeinsam moderiert??! Und als Beiprodukt kommt eine allgemeine Suchmaschine für den gesamten RBTV Content raus.

RBTVOD ist ein Tribut an eine entspannte & gute Zeit, die ich damalas mit RocketBeansTV hatte, ganz besonders als es sehr frisch war.

Vielliecht konnte ich damit etwas zurückgeben und ihr findet alte Perlen oder lässige Let’s Plays oder einfach nur Almost Daily/Plaily Kombinationen die ihr noch garnicht kanntet.

nils_gunnar_rbtvod.png

Table of contents

Architektur Vorschau

architektur

Stack: Next.js auf Vercel, Neon(postgresql), Upstash(Redis), Inngest(für lang laufende Prozesse), Posthog, RBTV Api

Genauere Details und Entscheidungen weiter unten. Gegen Ende.

Vorwort

Was haben Nils und Gunnar exklusiv zusammen moderiert?

Vor einigen Jahren (nahezu 10 puh..) habe ich das Let’s Play Uncharted 4: A Thief’s End mit Nils und Gunnar auf RocketbeansTV gesehen. Was in mir die Frage geweckt hat ‘Was haben Nils und Gunnar sonst noch zusammen moderiert’?

Zu dieser Zeit gab es noch keinerlei API oder irgend einen anderen Weg um herauszufinden was nun Nils und Gunnar gemeinsam gemacht haben außer natürlich, auf Youtube “Gunnar Nils RBTV” oder so zu suchen aber da kriege ich ja auch Episoden in denen auch andere Bohnen dabei sind bzw. was auch immer der Youtube Algo ausspuckt.

Budi alleine?

budialleine Oder falls ich die ÄLTESTEN Episoden sehen möchte in denen NUR BUDI zu sehen ist, kann ich dies ja nicht so auf Youtube suchen, auf RBTVOD schon (Industrie Skypeorama anyone??)!!

Kurz habe ich überlegt diese Metadaten selbst zu pflegen, allerdings gab es damals schon mehrere tausende Videos und heute gibt es etwa ~25k Videos. Unmöglich dies alleine umzusetzen ¯\(ツ)/¯ ..

RBTV API

Viele Jahre später hat RBTV eine eigene Software Abteilung gegründet und eine RBTV API AUF GITHUB veröffentlicht. WOW 🙏. Nun kann ich also die gesamten Metadaten der Mediathek durchforsten die jemand bei den Rocketbeans pflegt. Hier z.B. die Metadaten zu dem Lets Play von Gunnar und Nils https://api.rocketbeans.tv/v1/media/episode/2884

{
  "success": true,
  "data": {
    "bohnen": {
      "16": {
        "mgmtid": 16,
        "name": "Gunnar",
        "role": "external",
        "images": [
      ...

Vorschau aufs Haupt Feature: die Exakte Suche

Das Haupt-Feature ist die Suche in der man eine EXAKTE Kombination finden möchte, wie bei mir Nils & Gunnar!

Nun kann man auch recht obskure Kombinationen finden und bekommt eine Vorschau wer noch wieviele weitere Episoden als Schnittmenge hat. Hier suche ich z.B. exakt Etienne, Hannes (Cities Skylines war ein sehr cooles Format mit den beiden damals - das Combeback solcher lang laufenden Formate wäre schön) und es ist zu sehen, dass es noch Schnittmengen mit Nils, Andy, Gregor uvm. gäbe.

obskure

Hannes und Benjamin auf dem Klo

Hannes und Benjamin haben gemeinsam eine gute Chemie vor der Kamera (gehabt 🥲) und auch hier gibt es einige Perlen.

Buchstäblich auf dem Klo sitzend Toilet Tycoon spielen - nur als Beispiel.

toilettycoon toilettycoon_yt

Technische Umsetzung ab hier 👇

Ab hier spreche ich in erster Linie über die technische Umsetzung des Projektes. Wie die API von RBTV gestaltet ist, wie ich meine Architektur geplant und gebaut habe und welche Entscheidungen ich warum getroffen habe, wieso manche Episoden fehlen und wieso manche Bohnen fehlen.

Episoden der API lesen

thats budi with a mate

Wo ist Episode 1?

Ich habe erwartet, dass ich alle IDs von 1 bis X abfragen kann und auf der 1 das älteste Video auf dem RBTV Youtube Kanal finde Game One auf YouTube: RocketbeansTV ABER https://api.rocketbeans.tv/v1/media/episode/1 gibt es nicht, sondern dieser Fehler erscheint:

{
  "success": false,
  "message": "Die angeforderte Episode existiert nicht",
  "code": 3103,
  "data": {}
}

Wo ist also die “Game One auf Youtube: RocketbeansTV” folge? ICH WEISS ES NICHT! Ich konnte sie nicht in der API finden. Hier die ältesten “Game One” Einträge - also nix mit “Game One auf Youtube: RocketbeansTV”.

game one

Lücken

Ebenso bei 2,3,4….72 bis dann auf 73 endlich Gregor auftaucht als aller erster Eintrag in der Episode API https://api.rocketbeans.tv/v1/media/episode/73 #MoinMoin mit Gregor | 09.06.2015. Das ist nicht die einzige Lücke, es würde wieder und wieder passieren, daher habe ich einen anderen Weg benutzt. Später mehr.

Die letzte mir bekannte ID ist irgendwo in den ~50.000+ daher müsste ich also die RBTV API von 0 bis 50k+ durchiterieren und reihenweise {“success”: false} ohne Grund besuchen was unschön für den Betreiber ist (1x habe ich es doch gemacht um ein paar Sachen über verlorene Bohnen zu lernen sry sry).

Episoden synchronisieren

Alle. Bohnen. Bitte.

Nun da ich ja die Möglichkeit wollte gewisse Bohnenkombinationen zu suchen brauche ich erstmal alle Bohnen: https://api.rocketbeans.tv/v1/bohne/portrait/all gibt mir alle Bohnen mit ihren Bildern und ‘mgmtid’ in einem Request, z.B. Budi:

 {
      "mgmtid": 6,
      "name": "Budi",
      "role": "onair",
      "images": [
        {
          "url": "//static.rocketbeans.tv/img/f9394823-5717-493b-83d7-c8cad83165fe_orig.png",
          "name": "source",
          "height": 836,
          "width": 998
        },
        {
          "url": "//static.rocketbeans.tv/img/f9394823-5717-493b-83d7-c8cad83165fe_72x60.png",
          "name": "small",
          "height": 60,
          "width": 72
        },
        {
          "url": "//static.rocketbeans.tv/img/f9394823-5717-493b-83d7-c8cad83165fe_239x200.png",
          "name": "medium",
          "height": 200,
          "width": 239
        },
        {
          "url": "//static.rocketbeans.tv/img/f9394823-5717-493b-83d7-c8cad83165fe_597x500.png",
          "name": "large",
          "height": 500,
          "width": 597
        }
      ],
      "episodeCount": 1409
    }, ...

Nun synchronisiere ich mir diese Daten also in meine Postgres Datenbank damit ich sie nicht bei jedem Besuch von RBTVOD bei der Rocketbeans API abfragen muss. Das wäre langsam und würde bei den Beans viel Last erzeugen (theoretisch - falls es viele Besucher gäbe).

Daraus konnte ich dann dieses Dropdown Menu aufbauen (der episodeCount aus der API stimmt mmn nicht daher steht bei mir eine andere Zahl, später mehr dazu)

Bohnen Suche

bohnen_dropdown_menu

Initial Sync ‘byBohne’

Hier kommt der teuerste initial full sync mit dem Endpunkt “/media/episode/bybohne/:id” z.B. bei Budi https://api.rocketbeans.tv/v1/media/episode/bybohne/6?limit=3&offset=0 was mir 3 Episoden von Budi gibt. Wenn ich nun die nächsten 3 Lesen wollen würde müsste ich das Offset um 3 verschieben usw usw.

Das offizielle Limit der API für einen Call geht bis maximal LIMIT 50. Daher muss ich also um Budi komplett zu synchronisieren das Offset einige Male um 50 verschieben bis ich alle 1116 Episoden von Budi synchronisiert habe (hier zeigt sich übrigens diese große Diskrepanz zu den total 1409 oben).

{
  "success": true,
  "pagination": {
    "offset": 0,
    "limit": 3,
    "total": 1116
  },
    "data": {
    "bohnen": {
      "6": {
        "mgmtid": 6,
        "name": "Budi", 
        ...
    },
      "9": {
        "mgmtid": 9,
        "name": "Schröck",
        "role": "onair",
        ...
      }
    "episodes": [
  {
        "id": 53038,
        "showId": 330,
        "showName": "Best of Rocket Beans",
        "seasonId": 1278,
        "episode": 1,
        "title": "Best of Rocket Beans | Unsere HIGHLIGHTs im JANUAR'26 und DEZEMBER'25",
        "description": "Voilà, das BEST OF ROCKET BEANS für den JANUAR 2026..."
        "thumbnail": [...],
        "hosts": [6,9,14,15,28,31,42,66,72,81,128,155,167,168,188,393,407],
        "tokens": [
          {
            "id": 60715,
            "mediaEpisodeId": 53038,
            "token": "I71zKQ8pI-I",
            "type": "youtube",
            "length": 1698
          },
          ...
        ]
    ...
    }]
  }
}
}

Denormalisierung

Wozu ist Budi (und Schröck) und weitere Bohnen noch einmal unter dem “bohnen” Array bei Budis Episoden? Das Datenmodell hinter der API ist Denormalisiert! Die Episoden liefern hier noch einmal die Bohnen selbst mit die unten bei “hosts” erwähnt werden, damit man sich einen weiteren Lookup in der Datenbank sparen kann und das Array der hosts schmal hält anstatt noch einmal das Fette Bohne Objekt für jeden host mitzuliefern. Clever, auch wenn ich es niche benutze.

Allerdings könnte man sich nun zurecht Fragen ob ich diese Episode nicht erneut für Schröck und alle anderen Bohnen in der hosts Liste besuchen würde wenn ich wieder byBohne für Schröck abrufe.

Ja würde passieren aber ich speichere mir bereits besuchte EpidoseIds und besuche sie nicht erneut. Ich habe ja alle Infos schon für die “hosts”: [6,9,14,15,28,31,42,66,72,81,128,155,167,168,188,393,407] zu dieser Episode.

Neue Episoden

Wenn dieser Full-ByBohne-Sync fertig ist muss ich nur noch jede Stunde nach den aller neusten Episoden suchen https://api.rocketbeans.tv/v1/media/episode/preview/newest welche bis zu ~180 neue Folgen liefert und hier kann ich mich auf die neusten 20 oder so beschränken, weil manchmal noch Youtube Tokens oder hosts nachgepflegt werden upserte (einfach das neue inserten egal ob schon etwas besteht) ich einfach alles was reinkommt.

ALLE neuen Episoden

Was hier wiederum fehlt sind Shorts und Content der Astronauts.gg Channels. Also auch z.B. alle Streams von Nils, Eddy etc. unter ihren “Privaten” Youtube Channels. Das widerspricht etwas meiner originalen Idee bei der ich nur an Kombinationen von Bohnen interessiert war, aber wenn ich schon so weit gekommen bin… das kriege ich bei einem nächtlichen 3 Uhr morgens Sync aller noch unbekannten Ids von ByBohne sortiert nach Datum und ich suche nur bis ich eine bekannte Id finde.

Die Tokens liefern unter anderem den Link zum Youtube Video https://www.youtube.com/watch?v=I71zKQ8pI-I und würden auch den rbsc Video Link liefern, wenn er existiert aber den zeige ich nicht an.

Einige Episoden haben gar kein Video, diese blende ich aus. Für “Archiv” Zwecke könnte ich diese Option anbieten aber es sind sehr viele EpisodenIds hinter denen nur “Kein YT Video?” steht weil vermutlich etwas getestet oder falsch hochgeladen wurde und nachträglich entfernt wurde. Diese habe ich bspw. über den oben erwähnten Full-Full-Sync gefunden da sie auch keine Bohnen enthalten also von “byBohne” nicht gefunden werden.

Nicht gepflegte Episoden

database

Lücken zwischen den Lücken

Oben erwähntes “Ich iteriere alles von 0 bis 50-60k” hilft aber dabei, nicht vollständig gepflegte, alte Episoden zu finden. Da ich ja sonst nur Episoden finden würde die Bohnen enthalten (Erinnerung: ich suche ‘byBohne’) würden nicht gepflegte bzw. ungenau gepflegte Episoden nicht auffindbar sein.

Das habe ich “MoonshotEpisode” genannt, das sind die Lücken zwischen den Lücken in denen dann doch echte Episoden teilweise versteckt sind: ohne Bohnen, oder ohne Video etc. Auf RBTVOD Prod sind diese nicht vorhanden.

Falsch gepflegte Episoden

gregor_dima

Manche Episoden sind oder wirken falsch gepflegt. Das liegt entweder daran, dass ein Fehler bei der Eingabe passiert ist (zu viele oder zu wenige Bohnen) oder, dass manche Bohnen aus dem Archiv entfernt wurden, nachdem sie RBTV verlassen haben. Dazu zählt einer meiner alten lieblinge DIMA. Nun ist Dima noch in manchen Episoden mit anderen Bohnen gewesen und somit teilweise per Titel oder Description auffindbar wie in dem Bild oben.

Episoden ohne Bohnen

Dima auf DEV (nicht buDIMAn)

nur_dima

Allerdings gibt es auch Folgen in denen Dima alleine teilgenommen hat, diese sind dann natürlich aus RBTVOD Archiv Sicht nicht mehr auffindbar, solange ich nur byBohne suche. Man sieht es hier daran, dass kein mini Portrait unten Rechts ersichtlich ist! Für die Produktive Umgebung habe ich diese Episoden mal weggelassen, da das Konzept von RBTVOD ja war: Kombinationen aus Bohnen zu finden. Aktuell könnte man ja z.B. nach “Dima” suchen und Gregor als Bohne mit dazugeben (obacht das Wort Dima ist auch in buDIMAn enthalten 🤔 - also mit “Dima” suchen).

Alte und gelöschte Bohnen

Who is Bohne #13???

Bohne Nummer 13 ist aus der offiziellen Bohnen Liste entfernt worden, nicht allerdings aus der Denormalisierung alter Episoden. Diese Bohne habe ich dann auch nicht nach-synchronisiert, sondern ich nehme an, dass diese absichtlich nicht mehr in dem v1/bohne/portrait/all Endpunkt auffindbar war. Wer bis hier gelesen hat und rausgefunden hat wer das ist: schreibt mir eine email und ich erwähne euch HIER :p

Fehler melden

fehler_melden Wenn es irgendwo einen Fehler in den Daten gibt, kann man diese unten auf dem kleinen Bug Symbol melden. Dann erscheint ein kleiner Diff und man kann eintragen wer da wirklich reingehört. Diese speichere ich dann und falls RBTV daran intereissiert ist, kann ich einen Zugang ermöglichen.

Technik & Free Tiers

Kostenlos durchs Projekt :)

Ich habe mir als Aufgabe gesetzt alles so optimiert zu entwickeln, dass ich mit den Free Tiers von Datenbank + Caching bis Hosting auskomme. Dafür bedarf es ein paar kleiner Kniffe.

Vercel Hobby + cron-job.org

Vercel lässt beim Hobby Projekt nur 1 cron-job pro Tag zu. Ich würde neue Episoden aber gerne einmal die Stunde suchen. Das habe ich mit einem Endpunkt gelöst, den ich von außen von cron-job.org zusammen mit einem API-KEY triggere gelöst.

Zusätzlich lässt Vercel auch keine “lang laufenden” Prozesse zu, und da ich die rbtv api nicht unnötig schnell abfragen will, warte ich zwischen requests (Limit 50 + Offset X*50 bis keine Episoden mehr kommen, siehe oben), sodass die Komplette Synchronisierung gerne länger als die kostenlos zulässigen 30-60 Sekunden dauern kann.

Inngest für lange Prozesse

Das habe ich mit Inngest gelöst, dort kann man lang Laufende Prozesse hinschicken und quasi ein call-back Pingpong mit Inngest spielen bei dem jedes mal ein neuer Checkpoint erreicht wird.

Beispielsweise synchronisiere ich so maximal 10 Episoden am Stück, das ist schön im Free Tier von Vercel drin (dauert nur wenige Sekunden), dann meldet sich Inngest wieder und sagt: ‘oaky Zeit für die nächsten 10 Episoden an denen du nur ein paar Sekunden rumrechnen musst liebes Vercel’ und so weiter bis alles aus /newest synchronisiert wurde, was mich interessiert. Bei Inngest kann man theoretisch auch cron-jobs triggern lassen, womit cron-job.org obsolet werden würde aber naja.

Vielleicht gehe ich noch dazu über aber mit dem cron-job.org lifehack habe ich mich sehr klug gefühlt. Nachts um 3 mache ich dann noch einen 10 minütigen “Full-Sync” der einmal alles abholt was neu ist aber nicht in /newest drin ist aber natürlich alles skipped, was wir schon kennen, also keine unnötige Last bei rbtv erzeugt!

full-sync

Wieso nicht einfach SQLITE

SQLite konnte ich nicht nehmen, da Vercel keine schreibenden Operationen im File-System erlaubt/kann. Das wäre meine 100% präferierte Variante gewesen, und hätte mir sehr viel Kopfzerbrechen über folgende Schritte erspart + bei der zu erwartenden Last absolut ein Kinderspiel für SQLite.

Neon als kostenlose Postgres ein Overkill

Ein dicker Flaschenhals war die Datenbank als Free Tier. Ich habe zunächst Prisma ausprobiert, dort war der Free Tier sehr schnell aufgebraucht, ich habe ihn quasi alleine durchs Testen zum erliegen gebracht.

Nunja also ist es Neon geworden. Und Neon ist für meinen Usecase wirklich ein Overkill, denn es bietet so viele Features, die ich alle nicht brauche. Ich will nur eine kostenlose Datenbank mit einem generösen Free Tier. Und nun habe ich diese 700 Features die ich nicht benutze, wie z.B. Branches der Datenbank. Crazy.

Caching mit Upstash / Redis

Damit also Neon nicht zu viel Traffic erhält und RBTVOD ein wenig schneller lädt habe ich einen Cache-Layer dazwischen gezogen: Redis mittels dem Free Tier von Upstash.

Das führt zu weniger Requests an der Datenbank und sehr viel schnelleren Ladezeiten von bereits gesuchten Episoden.

Architektur pipapo

Also wenn jemand (Gelb) als zweites nach einer bereits bekannten Kombination sucht, dann ist das Ergebnis quasi INSTANT da. Das Problem ist Cache Invalidierung. Was passiert wenn sich etwas in der Datenbank ändert aber es anders im Cache steht? Dann bekommen User olle Datensätze zurück. Daher invalidieren wir jedes mal, wenn ein Inngest Event durchgelaufen ist.

In dem Bild fehlt noch der Vercel unstable_cache der auch noch einen Part in der Geschwindikeit und Ausnutzung verschiedener Free Tiers spielt. Dazu kommt noch Vercels ISR Incremental Static Regeneration welches die Seite in einem Okay-Frischem Zustand serviert, schnell aber evtl etwas veraltet. Aber das ist vermutlich ein eigener Deep Dive.

architektur

Features

beliebt

Unter Belibet findet man wie oft Episoden von der RBTVOD Seite aus geklickt wurden. Das ist vermutlich das kaputteste Feature weil es ja diese Klicks tracken muss aber ich will nicht zu schnell live tracken weil das ja meine Free Tiers ausreizen könnte, daher sind die Daten oft stale (oll) und manchmal lädt die Seite auch garnicht. Ist aber kein main Feature der Seite also lasse ich es erstmal so.

Ebenso gibt es Bohnen Kombinationen die man evtl. zusammen nicht so lieb gewonnen hat, daher habe ich eine komplette Logik implementiert (NICHT, UND, ODER). Hier als Beispiel zu sehen: Etienne && !Nils && !Simon && Budi

logik

Analytics

posthog

Auf Posthog tracke ich noch welche Kombinationen besonders gerne gesucht wurden. Natürlich im Free Tier der recht reichhaltig bei Posthog gestaltet ist.

Fazit

nils_gunnar_ende

Gunnar und Nils haben außerhalb vom großen Uncharted Let’s Play nahezu nichts miteinander gemacht :D

Najaaa. Das Projekt hat mir wahnsinnig viel Spaß gemacht. Von der Explorations Phase der rbtv api bis hin zu den Technischen Hürden der ganzen Free Tiers :P Damit Endet ein Projekt welches ich seit nahezu 10 Jahren umsetzen wollte. Die Roadmap umfasst noch sehr viele Kleinigkeiten, allerdings kommt nun langsam besseres Wetter und ich gehe ein wenig Gras berühren. Tschau.