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>
86 lines
8.3 KiB
Markdown
86 lines
8.3 KiB
Markdown
# 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.
|