# YouTube-Tab: Gast-Zugang mit Tages-Limit Status: Approved (Dustin, 2026-08-26) — bereit für Implementierungsplan. ## Kontext Der YouTube-Downloader/die YT-Suche laufen über den Baka-Proxy (`yt.baka-net.de` → `yt_proxy.py`), der für **jede** Anfrage einen gültigen Baka-Auth-Bearer-JWT verlangt (`auth_common.verify_token`). Seit dem Fix vom 2026-08-25/26 melden sich Server-User (Dustin, Baka, Tinker) automatisch im Hintergrund an (`BakaAuth.autoAnmelden`, siehe `lib/services/baka_auth.dart`), Gäste sehen weiterhin den manuellen „Beim Baka-Konto anmelden“-Dialog. Neue Anforderung (Dustin, 2026-08-26): Jeder Cloud-Account-User (die baka-auth-User: baka, dustin, tinker — und künftig per Einladungscode registrierte weitere User) bekommt automatisch Dustins YouTube-Cookies und hat kein Limit. **Gäste ohne Cloud-Account sollen die App trotzdem nutzen können, aber mit einem Tages-Limit von 5 Downloads und ohne Cookies.** Heute ist das serverseitig unmöglich: Ohne gültigen Bearer-Token antwortet jeder Endpunkt mit 401. Es gibt keinen Weg, einen Gast über mehrere Anfragen hinweg wiederzuerkennen. ## Ziel Ein dritter, anonymer Zugangsweg („Gast-Token“) neben dem bestehenden Baka-Auth-JWT, mit serverseitig durchgesetztem Tages-Limit für Downloads. Suche bleibt für Gäste unbegrenzt. Cookies bleiben Cloud-Accounts vorbehalten. ## Nicht-Ziele - Kein Ersatz für das bestehende Baka-Auth-System — beide Wege bestehen parallel. - Kein Schutz gegen App-Neuinstallation/Token-Löschen — ein Gast kann sich durch Neuinstallation ein neues Kontingent holen. Das ist eine bewusste Hürde, kein harter Schutz (siehe Entscheidung unten). - Keine Änderung an „nur Audio“ — das gilt schon für alle User (yt-dlp extrahiert immer MP3, es gibt keine Video-Download-Option in der App). - Keine UI/Server-Änderung an bestehenden Cloud-Account-Flows außer der Cookie-Beschränkung (die war vorher implizit für alle gleich, jetzt explizit an den Zugangsweg gekoppelt). ## Entscheidungen (aus dem Brainstorming, mit Dustin abgestimmt) | Frage | Entscheidung | |---|---| | Gast-Identität | Geräte-gebundener, anonymer Token — kein IP-basiertes Limit, keine Einladungscode-Pflicht für Gäste | | UX-Fluss | Bewusste Wahl: Gast sieht „Anmelden“ UND „Als Gast fortfahren (5/Tag)“ als zwei Buttons, kein automatisches Freischalten | | Reset-Zeitpunkt | Kalendertag nach Server-Zeit (Aingrad), kein rollierendes 24h-Fenster | | Suche fürs Limit | Suche bleibt für Gäste unbegrenzt — nur `POST /api/yt-dl` zählt | | Cookies für Gäste | Nein — nur Cloud-Accounts bekommen Dustins YouTube-Cookies; der `cookies`-Parameter wird bei Gast-Token-Anfragen ignoriert | | Limit für Cloud-Accounts | **Nachtrag 2026-08-29:** 100 Downloads/Kalendertag je Nutzer (Dustin/Baka/Tinker je einzeln gezählt) — ersetzt das ursprüngliche „kein Limit". Grund: dieselbe Absicherung wie beim Gast-Limit, nur großzügiger, da Cloud-Accounts vertraute Nutzer mit Cookie-Zugriff sind. | ## Architektur Drei Zugangsklassen beim Proxy: 1. **Cloud-Account** (`Authorization: Bearer `, wie heute) — **100 Downloads/Kalendertag je Nutzer** (Nachtrag 2026-08-29, ersetzt „unbegrenzt"), Cookies automatisch, Suche unbegrenzt (wie bei Gästen). 2. **Gast** (`X-Guest-Token: `, neu) — 5 Downloads/Kalendertag, Suche unbegrenzt, keine Cookies. 3. **Kein Zugang** — weder Bearer noch gültiger Gast-Token → 401 wie heute. ## Nachtrag 2026-08-29: 100/Tag-Limit auch für Cloud-Accounts Dustin hat beim erneuten Durchsprechen der Architektur (nach den Login-Race-Fixes) bestätigt, dass auch Cloud-Accounts (Dustin, Baka, Tinker) ein Tages-Limit bekommen sollen — 100 statt der ursprünglich geplanten 5, aber nicht mehr unbegrenzt. Grund: dieselbe Missbrauchs-/Kosten-Absicherung wie beim Gast-Limit (YouTube-Rate-Limits, Serverlast), nur mit großzügigerem Kontingent, weil Cloud-Accounts vertraute, bekannte Nutzer sind und zusätzlich Cookie-Zugriff haben. **Übernommene Entscheidungen des Gast-Limits (analog, keine neue Diskussion nötig):** Reset zum Kalendertag (Server-Zeit), Suche zählt nicht, nur `POST /api/yt-dl` zählt, Klartext-429-Fehlermeldung ohne automatischen Retry. **Unterschied zum Gast-Limit:** Cloud-Accounts zählen **pro Nutzername** (aus dem verifizierten JWT), nicht pro Gerät/Token — ein Nutzer, der die App auf zwei Geräten installiert hat, teilt sich also ein gemeinsames Kontingent (folgerichtig, da derselbe Baka-Account). ### Server-seitige Änderungen (zusätzlich zu den bestehenden Abschnitten oben — Hermes/`claude-server`-Worktree) - Neue Tabelle (oder Erweiterung von `guest_quota`, falls strukturell identisch — Entscheidung liegt bei der Server-Session) analog zu `guest_quota`, aber mit `username` statt `token` als Schlüssel: ```sql CREATE TABLE IF NOT EXISTS cloud_quota ( username TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0, count_date TEXT NOT NULL ); ``` - `POST /api/yt-dl` bei Cloud-Zugriff (gültiger Bearer-JWT): dieselbe Tageswechsel-Reset-Logik wie bei Gästen, aber Schlüssel = Nutzername aus dem JWT. Bei `count >= 100` → `429` mit `{"error": "Tages-Limit erreicht (100/Tag) — ab morgen wieder verfügbar", "cooldown": }`. Erfolgsantwort bekommt zusätzlich `"cloud_remaining": ` (nur bei Cloud-Zugriff, analog zu `guest_remaining` bei Gästen — beide Felder schließen sich gegenseitig aus, nie beide gleichzeitig gesetzt). - `GET /api/search` bleibt für Cloud-Accounts unverändert unbegrenzt (wie für Gäste). ### App-seitige Änderungen (diese Seite implementiere ich) - `BakaAuth` (`lib/services/baka_auth.dart`) bekommt — analog zu `GastZugang.verbleibend`/`.merkeVerbleibend(n)` — ein neues Feld `int? verbleibend` (Getter) und `void merkeVerbleibend(int n)`, das den zuletzt vom Server gemeldeten Kontingent-Stand hält (nur fürs Anzeigen, `null` vor dem ersten Download in dieser Session). - `YtDownloadService.herunterladen()` (`lib/services/yt_download_service.dart`): liest nach einem erfolgreichen `POST /api/yt-dl` zusätzlich `daten['cloud_remaining']` aus, und ruft bei `!gastAktiv` (also Cloud-Zugriff) `auth.merkeVerbleibend(cloudRest)` auf — analog zur bestehenden `gast!.merkeVerbleibend(gastRest)`-Zeile. Ein `429` läuft bereits durch den bestehenden generischen `_fehlerText(antwort)`-Pfad (kein neuer Fehlerfall nötig, der zeigt jeden Server-`error`-Text aus dem 200-fremden Statuscode an — funktioniert für 429 genauso wie für 401 außerhalb des Spezialfalls). - UI (`_YouTubeBereich` in `lib/downloads/downloads_screen.dart`, `YoutubeSearchScreen` in `lib/downloads/youtube_search_screen.dart`): zeigt analog zum bestehenden Gast-Text („Als Gast unterwegs — noch N von 5 heute") jetzt auch für angemeldete Cloud-Accounts einen Text „Noch N von 100 heute", sobald `auth.verbleibend != null` — platziert an vergleichbarer Stelle wie der Gast-Text, aber nur sichtbar wenn `auth.istAngemeldet` (nicht für Gäste, die haben ihren eigenen Text). ### Tests (Nachtrag) - **App:** `BakaAuth.merkeVerbleibend`, `YtDownloadService` liest `cloud_remaining` bei Cloud-Zugriff (nicht bei Gast-Zugriff — die beiden Felder dürfen sich nicht vermischen), Widget-Test für den neuen „Noch N von 100 heute"-Text in beiden Bildschirmen. - **Server** (Hermes): analog zu den bestehenden Gast-Tests, plus ein Test, der sicherstellt, dass Gast- und Cloud-Kontingente unabhängig voneinander gezählt werden (ein Cloud-Login verbraucht kein Gast-Kontingent und umgekehrt — sie sind ohnehin durch unterschiedliche Auth-Wege getrennt, aber das schadet nicht, explizit zu testen). ## Server-seitige Komponenten **Betrifft `/home/dustin/scripts/yt_proxy.py` und ggf. `auth_common.py` — liegt im `claude-server`-Worktree, nicht in `~/mello-dev/app`. Diese Spec beschreibt den Vertrag, den die App-Seite braucht; die Umsetzung macht die Server-Session.** ### Neuer Endpunkt: `POST /api/guest-token` Kein Auth-Header nötig. Erzeugt einen neuen, zufälligen Token (`secrets.token_hex(24)`, 48 Hex-Zeichen) und legt eine Zeile in einer neuen SQLite-Tabelle an: ```sql CREATE TABLE IF NOT EXISTS guest_quota ( token TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0, count_date TEXT NOT NULL, -- 'YYYY-MM-DD', Server-Zeit created_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` Antwort `200`: `{"guest_token": ""}`. Speicherort: neue Datei `/home/dustin/yt-cache/guest_quota.db` (analog zum bestehenden `DOWNLOAD_DIR`-Cache, getrennt vom Lieder-Register und von `baka_auth.db`, da funktional unabhängig). ### `_checke_auth()` erweitert Bisher: `True`/`False`. Neu: gibt zurück, welche Zugriffsart vorliegt (z. B. ein kleines Ergebnis-Objekt `{"cloud": bool, "guest_token": str|None}`) statt nur ja/nein: - `Authorization: Bearer ` gültig → Cloud-Zugriff (wie heute). - Sonst, falls `X-Guest-Token` gesetzt und in `guest_quota` vorhanden → Gast-Zugriff mit dem jeweiligen Token. - Sonst → kein Zugriff, 401 wie heute. ### `GET /api/search` Cloud- **und** Gast-Zugriff erlaubt, keine Zählung für Gäste. ### `POST /api/yt-dl` - Cloud-Zugriff: wie heute, `cookies`-Parameter aus dem Body wirksam. - Gast-Zugriff: `cookies` wird serverseitig auf `False` erzwungen (Body-Wert ignoriert). Vor dem Download: `count_date` der Token-Zeile mit dem heutigen Server-Datum vergleichen — weicht es ab, `count` auf 0 und `count_date` auf heute zurücksetzen (Tageswechsel). Ist danach `count >= 5` → `429` mit `{"error": "Tages-Limit erreicht (5/Tag) — ab morgen wieder verfügbar", "cooldown": }`. Sonst: Download wie bisher, bei Erfolg `count += 1` in derselben Transaktion wie die Antwort (kein Doppelzählen bei Netzwerkfehlern nach dem Schreiben — Erfolg zuerst prüfen, dann zählen). - Erfolgsantwort bekommt zusätzlich `"guest_remaining": ` (nur bei Gast-Zugriff; bei Cloud-Zugriff entfällt das Feld), damit die App den Zähler ohne Extra-Anfrage aktualisieren kann. ### `GET /api/dl/` Cloud- und Gast-Zugriff erlaubt (die Datei wurde beim `yt-dl`-Aufruf schon gezählt, hier nur Auslieferung). ### Fehlerfall: unbekannter/ungültiger Gast-Token Antwort wie „kein Zugriff“ (401 mit `{"error": "Unauthorized"}` — nicht von einem abgelaufenen Cloud-Token zu unterscheiden nötig, die App behandelt beide gleich: neuen Gast-Token holen, siehe unten). ## App-seitige Komponenten (diese Seite implementiere ich) ### Neuer Service `GastZugang` (`lib/services/gast_zugang.dart`) Analog zu `BakaAuth`, aber ohne Passwort: ```dart class GastZugang extends ChangeNotifier { String? _token; int? _verbleibend; // zuletzt vom Server gemeldeter Stand, nur fürs UI bool get hatToken => _token != null; int? get verbleibend => _verbleibend; Map get gastHeader => {if (_token != null) 'X-Guest-Token': _token!}; Future holeToken(); // POST /api/guest-token, speichert Token sicher void merkeVerbleibend(int n); // aus der yt-dl-Antwort übernehmen } ``` Token-Speicherung: `flutter_secure_storage`, eigener Schlüssel `guest_token` (gleiches Muster wie `BakaAuth`/`TokenSpeicher`). ### `YtDownloadService` / `YtSearchService` Bekommen einen optionalen zweiten Header-Lieferanten: statt nur `auth.authHeader` können sie auch `gast.gastHeader` verwenden, je nachdem welcher Zugang aktiv ist. `herunterladen()` liest bei Gast-Zugriff `daten['guest_remaining']` aus der Erfolgsantwort und ruft `gast.merkeVerbleibend(...)`. Ein `429` wird wie die bestehenden Fehlerfälle über `_scheitere(text)` gemeldet (Klartext aus der Server-Antwort, kein neuer Fehlerpfad nötig). ### UI (`_YouTubeBereich` in `downloads_screen.dart`, `YoutubeSearchScreen`) Für echte Gäste (`!_serverUser && !auth.istAngemeldet && !gast.hatToken`): zwei Buttons nebeneinander/untereinander — „Beim Baka-Konto anmelden“ (bestehend) und „Als Gast fortfahren (5 Downloads/Tag)“ (neu, ruft `gast.holeToken()`). Nach erfolgreichem Gast-Token: Adressfeld/Suchfeld wie gewohnt freigeschaltet, zusätzlich ein kleiner Text „noch N von 5 heute“ (aus `gast.verbleibend`, `null` vor dem ersten Download = kein Zähler sichtbar, erst nach dem ersten Download bekannt). Der bestehende Cookie-Schalter (`SwitchListTile`, „YouTube-Cookies des Servers verwenden“) wird für Gäste ausgeblendet — der Server ignoriert den Wert ohnehin, ein sichtbarer aber wirkungsloser Schalter wäre irreführend. ## Fehlerfälle (App-Seite) - **Gast-Token vom Server abgelehnt** (401 trotz gesetztem `X-Guest-Token`, z. B. weil die Server-DB zurückgesetzt wurde): App holt automatisch einen neuen Token und meldet den fehlgeschlagenen Versuch wie einen normalen Fehler („bitte erneut versuchen“) — kein automatischer Retry des Downloads selbst, um keine Doppel-Downloads auszulösen. - **429 Tages-Limit erreicht**: Klartext-Fehlermeldung aus der Server-Antwort, kein Absturz, kein automatischer Retry. - **App-Neuinstallation**: neuer Gast-Token, neues Kontingent — bewusst akzeptiert (siehe Nicht-Ziele). - **Netzwerkfehler bei `POST /api/guest-token`**: wie bestehende „Proxy nicht erreichbar“-Fehler behandelt. ## Tests - **Server** (schreibt die `claude-server`-Session): Unit-Tests für `guest_quota`-Zählung, Tageswechsel-Reset, Cookie-Ausschluss bei Gast-Zugriff, 429 bei erreichtem Limit, Suche ohne Zählung. - **App** (schreibe ich, TDD): `GastZugang` (Token holen, speichern, `merkeVerbleibend`), Widget-Tests für die zwei Gast-Buttons und den Zähler-Text, `YtDownloadService`/`YtSearchService` mit Gast-Header statt Bearer-Header, 429-Fehlertext-Anzeige. ## Offene Abhängigkeit Diese Spec beschreibt einen Vertrag zwischen App und Server. Die App-seitige Implementierung (Service, UI) kann erst sinnvoll gegen den echten Server getestet werden, wenn `POST /api/guest-token` und die erweiterte `_checke_auth()`-Logik in `yt_proxy.py` existieren. Bis dahin entwickle ich die App-Seite gegen einen gemockten Proxy (wie bei `BakaAuth`/`YtDownloadService` schon üblich) und dokumentiere die exakten Endpunkt-Erwartungen oben so genau, dass beide Seiten unabhängig implementiert werden können.