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

8.3 KiB
Raw Blame History

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': isDoodStreamresolveDoodStream, isPackerHosterresolvePackerHoster, isFilemoonHosterresolveFilemoonHoster.
  • 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.