Files
Melo/docs/superpowers/specs/2026-08-26-youtube-guest-quota-design.md
Hermes (Server)andClaude Sonnet 5 8611738124 Spec-Nachtrag: 100/Tag-Limit auch für Cloud-Accounts (nicht mehr unbegrenzt)
Dustin hat beim Durchsprechen der Proxy-Architektur bestätigt: auch
Dustin/Baka/Tinker bekommen ein Tages-Limit (100 statt 5 bei Gästen),
nicht mehr unbegrenzt. Serverseitiger Vertrag (cloud_quota-Tabelle,
cloud_remaining-Feld) und App-Seite (BakaAuth.verbleibend, UI-Text)
dokumentiert, analog zum bestehenden Gast-Limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012A2pmbnVNPiHdyf2GW8eLP
2026-08-29 13:17:35 +02:00

14 KiB

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.deyt_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 <JWT>, 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: <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:
    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 >= 100429 mit {"error": "Tages-Limit erreicht (100/Tag) — ab morgen wieder verfügbar", "cooldown": <Sekunden bis Mitternacht>}. Erfolgsantwort bekommt zusätzlich "cloud_remaining": <int> (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:

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": "<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 <JWT> 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 >= 5429 mit {"error": "Tages-Limit erreicht (5/Tag) — ab morgen wieder verfügbar", "cooldown": <Sekunden bis Mitternacht>}. 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": <int> (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/<file>

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:

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<String, String> get gastHeader =>
      {if (_token != null) 'X-Guest-Token': _token!};

  Future<String?> 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.