goon/CLAUDE.md
jtrzupek 953068f0db docs(claude): add resolve/playback findings + local debugging guide
Capture the durable Goon facts (phone-side resolve for IP-bound/Turnstile hosters,
DoodStream/playmogo pass_md5, _embed_iframe vs _vps_blocked_fallback, stable image
proxy tokens, paradisehill multipart, dedup/merge) and a local-debugging section
(prod psql/worker patterns, Windows real-Python gotcha, Android emulator AVD `goon`
+ FLAG_SECURE-off screencap + 2x OTA apply, Chrome DevTools port 9223 + CDP-blanking
+ hoster network signatures). No secrets/IPs/usernames — env-var forms only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:51:29 +02:00

86 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Goon
Self-hosted aggregator of adult-content scene metadata. Multi-source ingest (TPDB, StashDB, 30+ tubes) → dedup → on-demand stream resolution (yt-dlp + JWPlayer unpacker) → API + Expo mobile client.
Stack: Python 3.12 (FastAPI + SQLAlchemy + Alembic), Postgres 16, React Native (Expo), Docker Compose.
Git: github.com/goon-foss/goon (remote: `goonfoss`). Public OSS repo. 18+ — see `DISCLAIMER.md`.
---
## Zmiany mobilne (Expo) → publikuj przez OTA, nie zostawiaj w gicie
Klient `mobile/` (React Native + Expo) aktualizuje się przez **self-hosted Expo Updates (OTA)**, nie przez sklep ani rebuild APK. Zmiany JS (ekrany, komponenty, `api.ts`) lecą silent.
**KRYTYCZNE: `git commit` NIE publikuje OTA.** Sam commit (ani push) nie zmienia NIC na telefonie. Po zmianie w `mobile/` trzeba zbudować bundle i opublikować:
```
# z roota repo; konkretne wartości env w .env.local / prywatnej pamięci (NIE hardcoduj IP tutaj)
GOON_VPS_SSH=<root@vps> GOON_VPS_UPDATES_DIR=/root/goon/app/static/expo-updates \
GOON_PUBLIC_UPDATES_URL=https://api.goon-foss.org/expo-updates/asset \
python scripts/publish_update.py --runtime 1.1
```
Skrypt: `expo export` (build) → scp bundla na VPS pod `app/static/expo-updates/<runtime>/<update_id>/` → przełącza `current.json` (endpoint `GET /expo-updates/manifest` serwuje aktywny). Apka pobiera przy następnym launchu.
Reguły:
- **runtime = `1.1`** (== `runtimeVersion` w `app.json`/APK). Bump TYLKO przy native change (wtedy też nowy APK przez PackageInstaller). `RUNTIME_DEFAULT` w skrypcie = 1.1.
- **Apply dwustopniowy**: 1. launch pobiera w tle, 2. launch aplikuje → po publish **restart apki 2×**.
- **Publish odpala AI** (nie człowiek, nie git-hook). Skrypt działa one-shot też na Windows git-bash (fix 2026-06-02: scp source `.`+`cwd=DIST`, `MSYS_NO_PATHCONV`, defensywne odkręcanie zmanglowanej ścieżki).
- **Backend (`app/`) to OSOBNY deploy** — scp `app/`→VPS + `docker compose restart` (worker/api). NIE przez OTA. `./app` i `./scripts` są wolumenami w compose, więc restart wystarcza (bez rebuildu obrazu).
---
## Resolve / playback (kluczowe ustalenia)
Stream resolve jest **on-demand, per-source**. Nic nie jest pre-resolvowane do DB poza miniaturkami i metadanymi.
- **Registry ekstraktorów**: `app/extractors/__init__.py` `_REGISTRY` mapuje `sitetag` na funkcję. Natywny ekstraktor zwraca `list[StreamSource]`. `_vps_blocked_fallback.extract` = sygnał "resolve tylko w WebView na telefonie" (sprawdzaj `is_vps_blocked_fallback(sitetag)`).
- **IP-bound / CAPTCHA hosterzy resolwują się PO STRONIE TELEFONU**, nie na VPS. CDN tokeny i Cloudflare Turnstile wiążą się z IP requestera, więc VPS (Hetzner) dostaje 403 albo Turnstile gate, a residential IP telefonu przechodzi. Resolvery phone-side: `mobile/src/lib/{doodstream,packerHoster,filemoonHoster,getfileResolver,pornxpResolver}.ts`. `PlayerScreen.tsx` routuje URL `type='hoster'`: `isDoodStream``resolveDoodStream`, `isPackerHoster``resolvePackerHoster`, `isFilemoonHoster``resolveFilemoonHoster`.
- **DoodStream i klony** (playmogo, doply, dood.*): protokół `pass_md5` + infra `doodcdn.io` + **niewidzialny** Cloudflare Turnstile (przechodzi automatycznie w WebView z residential IP, to NIE interaktywny CAPTCHA). Tube który embeduje playmogo (sxyland, xmoviesforyou, siska) ma iść przez `_embed_iframe.extract` (wyłuskuje iframe, oddaje `type='hoster'`, telefon dood-resolwuje), NIE przez `_vps_blocked_fallback`.
- **Image proxy**: WSZYSTKIE miniaturki idą przez `/proxy/img/{token}/...` (backend dokłada Referer, którego expo-image nie wysyła → CDN by zwracał 403). Token MUSI być stabilny (`make_token(..., stable_bucket_sec=...)`), inaczej URL zmienia się co request → expo-image cache miss → re-download co fetch/launch.
- **Movies multipart** (paradisehill): backend parsuje `var videoList` i zwraca wszystkie części jako osobne `StreamLink`; mobile pokazuje je w **scrollowalnym Modalu** (nie `Alert.alert` — Androidowy AlertDialog renderuje max 3 buttony, stąd dawne "max 3 z 35").
- **Dedup**: `app/scheduler/bulk_dedup.py` (cross-source performer-shared + `phash_exact`). `merge_scenes` (`app/resolve/scene_merge.py`) przenosi refs/performers/tags/fingerprints/**playback_sources** i kasuje drop. Missing-merge bez phash: `scripts/merge_exact_title_duration.py` (ten sam performer + identyczny znormalizowany tytuł + długość co do sekundy). Over-attribution performerów bierze się z luźnego tube-searcha (hqporner wymaga WSZYSTKICH tokenów query w slug).
- **Bug reports z apki**: tabela `bug_reports` na prodzie (`screen_name`, `scene_id`, `movie_id`, `message`, `screenshot_b64`, `resolved`). To główne źródło triage'u. Screenshot dekodujesz z base64 i oglądasz.
---
## Lokalny debugging
Hosta VPS, klucze API i wartości env trzymaj w prywatnej pamięci / `.env.local`. NIE commituj ich tu (repo jest publiczne).
- **DB / worker na prodzie** (przez ssh na VPS): `docker compose exec -T db psql -U goon -d goon -c "..."` (pamiętaj `-U goon`, bez tego "role root does not exist"). Python w workerze: `docker compose exec -T worker python3 -c "..."` albo heredoc. `from app.db import session_scope` (NIE `app.db.session`).
- **Deploy backendu**: `scp app/...` na VPS + `docker compose restart worker api`. Wolumeny `./app` i `./scripts` → restart wystarcza, bez rebuildu.
- **Windows: prawdziwy Python**. `python`/`python3` na PATH to STUB ze Sklepu Microsoft (wypisuje "Python was not found" i wychodzi). Użyj realnego interpretera: `%LOCALAPPDATA%\Programs\Python\Python3XX\python.exe` (uruchamiaj przez PowerShell). `publish_update.py` po sukcesie kończy się czysto (UTF-8 stdout wymuszony 2026-06-08, koniec mylącego exit-1 na polskich znakach).
- **Emulator Android** (podgląd odtwarzania scen i UI):
- adb: `%ANDROID_HOME%\platform-tools\adb.exe` (`ANDROID_HOME` ustawiony; SDK NIE w domyślnym `%LOCALAPPDATA%\Android\Sdk`). AVD: **`goon`**.
- Start: `emulator -avd goon -no-snapshot-load -no-boot-anim -gpu swiftshader_indirect` (w tle). Czekaj na boot: `adb wait-for-device` + poll `adb shell getprop sys.boot_completed` == `1`.
- Pakiet apki: `com.goon.mobile`. Launch: `adb shell monkey -p com.goon.mobile -c android.intent.category.LAUNCHER 1`.
- **Screencap działa bo dev/emulator build ma `FLAG_SECURE` WYŁĄCZONE.** Release build ustawia `FLAG_SECURE` (blokuje screenshoty/nagrywanie ekranu treści NSFW) — w buildzie emulatora zostawiamy off, żeby AI mógł `adb exec-out screencap -p > out.png` i zweryfikować odtwarzanie/UI.
- **OTA na emulatorze**: po publish force-stop + launch **2×** (1. pobiera bundle w tle, 2. aplikuje).
- Gesty przez adb: tap `adb shell input tap X Y` (współrzędne device, sprawdź `wm size`, zwykle 1080x2400). Long-press: `adb shell input swipe X Y X Y 700` (ten sam punkt, >300ms = `delayLongPress`). Bounds elementów: `adb shell uiautomator dump` (RN czasem nie eksponuje tekstu, natywne Alert/dialogi tak).
- **Chrome DevTools (diagnoza hosterów/playbacku — sprawdzaj, nie zgaduj)**: odpal Chrome `--remote-debugging-port=9223 --user-data-dir=<temp>`; podłącza się `chrome-devtools` MCP. KVS/dood tubes **blankują się na wykrycie CDP** (strona → about:blank) → używaj `list_network_requests(includePreservedRequests=true)` zamiast DOM. Sygnatury w trace: `pass_md5` + `doodcdn.io` = DoodStream; `kt_player(` + `license_code` = KVS; `cdn-cgi/challenge-platform` + `turnstile` = Cloudflare (sprawdź czy invisible).
---
## Kanban (auto-aggregation)
Ten workspace ma własny Kanban (custom tracker type `kanban` w `.nimbalyst/trackers/kanban.yaml`) z kolumnami:
**Backlog → Następne → W trakcie → Czeka → Zrobione**
### Reguła auto-agregacji — WAŻNE
Kiedy podczas sesji pojawi się nowy temat / bug / pomysł / blocker, na który **nie ma teraz czasu** lub jest poboczny względem aktualnego zadania — **utwórz tracker item** zamiast tylko wspomnieć o nim w czacie:
```
mcp__nimbalyst-mcp__tracker_create(
type: "kanban",
title: "<krótki tytuł>",
status: "backlog",
description: "<1-2 zdania kontekstu + ścieżka pliku jeśli dotyczy>"
)
```
Powód: Jan często skupia się na pierwszej rzeczy z planu, a pozostałe znikają. Kanban jest gwarancją, że nic nie wypadnie. Jeśli wspomnisz coś w czacie i nie założysz ticketu — to się zgubi.
Status `next` = "wybrane na najbliższe dni" (3-5 max). `waiting` = zablokowane na zewnętrzne. `done` ustawia użytkownik, nigdy AI.