Commit graph

42 commits

Author SHA1 Message Date
goon-foss
09f389100a feat(watchdog): jakosc zrodel, progi adaptacyjne, dlugosci perverzija i mypornerleak, poprawki parserow
Some checks are pending
Backend tests / test (push) Waiting to run
- browse_runs (migracja 0056): jeden wiersz per scraper i przebieg browse-latest
- quality_watchdog: listing pusty albo bez nowych, skok lub zanik obsady, sceny bez
  dlugosci i miniatur, spadek udanych odtworzen per origin; fingerprint per zrodlo i rodzaj
- ingest_watchdog: prog swiezosci z mediany odstepow zrodla (3x, max 14 d) zamiast stalej
- duration backfill: resolvery perverzija (JSON-LD) i mypornerleak (itemprop), paczka
  po rowno z originow, 404/410 zamyka wiersz od razu
- parsery: galaxyporn data z JSON-LD, hentaimama dlugosc, javflix aktorki, fullmovies
  i hdporngg data z uploadDate
2026-10-14 12:13:13 +02:00
goon-foss
88f8729840 feat(survey): jednorazowa ankieta in-app (6 pytan), endpointy i podsumowanie
- backend: tabela survey_responses (0055), GET /survey/current z bramka po obejrzanych
  scenach, POST /survey/{id} (upsert per urzadzenie), GET /survey/{id}/summary
- apka: SurveyModal po tutorialu i po "What's new" (popupBus), raz na urzadzenie,
  pomijalna, flaga w SecureStore; wpis w changelogu
2026-10-12 17:34:19 +02:00
goon-foss
6540d4a009 feat(blacklist): ukrywanie stron per urzadzenie
Zgloszenie 3910b19b: brak przycisku ukrywania stron. Tabela hidden_sites
(0054), POST/DELETE /blacklist/sites/{origin}, pole sites w GET /blacklist.
Scena znika z list tylko gdy wszystkie jej zywe zrodla sa z ukrytych stron
(ta sama scena z innego serwisu zostaje); w szczegolach sceny przyciski
ukrytych stron znikaja, o ile zostaje inne zrodlo.

Cache pustosci blacklist trzyma teraz, ktore listy sa niepuste, wiec ukryte
strony nie doklejaja trzech pustych NOT EXISTS, i jest czyszczony przy kazdej
zmianie (wczesniej pierwszy wpis dzialal po 5 minutach). Kod synchronizacji
kopiuje hidden_sites oraz favorite_sites, ktorych tam brakowalo.

Sandbox na produkcyjnej bazie: strona porndoe 42 z 60 scen tylko z porndoe
przed ukryciem, 0 po; feed 0,34 s, duze tagi 0,3 do 0,8 s z dwiema ukrytymi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:34:52 +02:00
goon-foss
2feef620a1 feat(telemetry): playback_events.via, skad przyszedl adres w odtwarzaczu
Po przejsciu porndoe na resolver po stronie telefonu 35 urzadzen dalej
dostawalo wylacznie 403 i nie dalo sie odroznic, czy pada nowa droga, zapas
przez WebView, czy stara paczka. Apka wysyla teraz via: phone, backend,
hoster, webview, proxy, movie; '-re' po ponownym rozwiazaniu w tym samym
ekranie, '<droga>>wv' gdy adres wyciagnal WebView jako zapas. NULL = paczka
sprzed tej zmiany. Migracja 0053 (kolumna nullable, sama zmiana metadanych).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:25:56 +02:00
goon-foss
4666fcb04a feat(edge): nowa punktacja feedu z historii klipu u nas, 25% w grupie kontrolnej
Odtworzenie uczenia na 189 tys. zdarzen (wagi zdarzenie po zdarzeniu, bez
zagladania w przyszlosc, test na 30% najnowszych): stara punktacja porzadkowala
klipy w obrebie urzadzenia z AUC 0,576 (lajk albo >= 10 s vs ucieczka < 2 s).
Tagi RedGifs sa liczne i ogolne, wiec ich suma dominowala, a mowila malo; nisze
prawie nic, popularnosc z RedGifs slabo, tworca i przyjecie klipu u nas najwiecej.

Nowa: log(waga tagu x rzadkosc tagu) + 0,25 log(nisze) + 2,5 log(tworca)
+ 0,8 ln(1+lajki RedGifs) + 120 (odsetek lajkow u nas - 3%) - 15 (odsetek
ucieczek - 50%) + 0,3 za swiezosc. Na tescie 0,652, regresja na tych samych
cechach 0,654. Liczniki clips.feed_views/skips/likes (migracja 0052, wypelnione
z historii), bumpowane w POST /clips/events. 25% urzadzen (hash id) zostaje na
starej punktacji do porownania na zywym ruchu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:47:02 +02:00
goon-foss
27309d24f2 fix(movies): kolejka wzbogacania z TPDB pamieta, co juz sprawdzila
Zadanie (co 6 h po 200) bralo zawsze 200 najnowszych kandydatow. Przebieg
o 05:00 wzbogacal 14-19 swiezych filmow, trzy kolejne mielily te same 200
bez trafienia z wynikiem zero, a 12,9 tys. starszych kandydatow nie bylo
sprawdzonych ani razu. Teraz movies.tpdb_checked_at (migracja 0051): najpierw
niesprawdzone, film bez trafienia wraca po 30 dniach, blad nie oznacza.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:39:12 +02:00
goon-foss
8f8bb1c448 fix(tags): tag_vertical_counts bez klucza obcego, koniec zakleszczenia o 05:00
Refresh licznikow (co 3 h) i tag_tail_trim (raz dziennie) startuja razem
o 05:00. Kasowanie osieroconych tagow schodzilo kaskada do
tag_vertical_counts i czekalo na refresh, a refresh przy wstawianiu czekal
na blokade tego samego tagu przez sprawdzenie klucza obcego. Od 29.09
Postgres co rano zabijal przycinanie (Sentry GOON-7N). Tabela to kopia
liczona od zera, wiec klucz obcy nie byl potrzebny (migracja 0050).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:24:42 +02:00
goon-foss
77679b26bb perf(scenes): lista serwisu od najnowszych prosto z indeksu
Some checks failed
Backend tests / test (push) Has been cancelled
Ekran serwisu (/scenes?origin=tube:X&sort=release_date) dla serwisow,
ktorych scen nie ma wsrod najnowszych w katalogu (perverzija, hdporn.gg,
tubepornclassic, yesporn), sortowal wszystkie ich sceny: 9-19 s na
zimnym cache, 1-4 s na cieplym, 39 wejsc w 3 h.

Wzorem scene_tags.sort_date (GOON-64): data sortu powielona w
playback_sources i pilnowana wyzwalaczami (0048), wypelniona skryptem,
indeks (origin, sort_date DESC, scene_id DESC) WHERE dead_at IS NULL
(0049). API dolacza DISTINCT ON zgodny z indeksem, bo 190 tys. scen ma
dwa zywe zrodla z tego samego serwisu. Po zmianie 0,03-0,24 s, takze
dalsze strony.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:18:59 +02:00
goon-foss
c97af57979 perf(tags): liczniki tagow JAV i animacji liczone w tle
/tags?vertical=jav liczyl agregat po zrodlach i scene_tags na kazde
wejscie, dwa razy: 1 s na rozgrzanym cache, 17-24 s na zimnym. Teraz
czyta tabele tag_vertical_counts (migracja 0047), ktora odswieza
refresh_taxonomy_counts co 3 h. Zakladka JAV: 0,35 s. Gdy tabela jest
pusta dla wertykalu, zostaje stary agregat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:37:23 +02:00
goon-foss
45ee7a81e1 fix(api): szybkie szukanie po tytule dla popularnych i krotkich fraz
Some checks are pending
Backend tests / test (push) Waiting to run
Planer szacowal kazda fraze na 427 trafien (histogram 100 probek, dolny limit), wiec
zawsze szedl indeksem trigramow i sortowal wszystkie trafienia: "vo" 44 s (dwie
litery nie maja trigramu, indeks oddaje 4,3 mln wierszy), "mis" 6,5 s, "free" 1,9 s
(GOON-73/75).

- 0046: histogram title_normalized 1000 probek, szacunki bliskie prawdy.
- list_scenes: przy q i sorcie po dacie najpierw przejscie po indeksie sortu z
  limitem 300 ms (bez bitmap, w SAVEPOINT wycofywanym zawsze), gdy probka tytulow
  z pg_stats mowi, ze wystarczy przejsc do 30 tys. scen; inaczej zwykly plan.
  Frazy bez trigramu zawsze przejsciem (limit 3 s, potem pusto).

115 prawdziwych wyszukiwan z logow: wyniki identyczne, najgorszy 44 s -> 0,8 s,
srednie frazy dotykaja 3-7x mniej stron ("bad" 122 tys. -> 16 tys.).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:00:11 +02:00
goon-foss
d3713d64b9 fix: source-stats z probki scen, nizsze progi autovacuum
source-stats liczyl flagi tag/obsada dla 2,5 mln par serwis-scena, czyli ok. 5 mln
losowych odczytow indeksow; trwal 22-32 min i przekroczyl limit (GOON-V). Teraz
flagi z probki 3000 scen na serwis (stala, po md5), liczba scen, swiezosc i
miniatury dalej dokladnie. 1 min 51 s, gwiazdki bez zmian, procenty +-2 pkt.

Autovacuum na domyslnych 20% nie ruszal duzych tabel od tygodni (scene_performers
11% stron widocznych, statystyki scenes z 25.08). Migracja 0045 daje 2% dla
szesciu najwiekszych tabel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:00:11 +02:00
goon-foss
41ad7f1818 feat: automatyczne sprzatanie martwych zrodel sxyprn i eporner
Oba serwisy kasuja filmy po cichu: strona odpowiada 200 z "Post Not Found" albo "Video has
been deleted". Probka 2026-09-19: 57% zywych w bazie zrodel sxyprn w wieku 10-40 dni i 22%
eporner bylo juz usunietych; w apce to puste kafelki i "the tube removed" po kliknieciu.

- eporner: ekstraktor rozpoznaje usunieta strone i rzuca HosterDead, wiec resolve oznacza
  zrodlo zamiast oddawac "blad chwilowy" (82 takie wejscia w 48 h, nic nie oznaczal).
- proxy miniatur: "Post Not Found" na sxyprn oznacza zrodlo od razu, 404 na miniaturze
  eporner uruchamia w tle sprawdzenie strony (28/30 takich miniatur to usuniete filmy).
  Indeks hash po thumbnail_url tylko dla eporner (0044), zeby znalezc zrodlo po obrazku.
- job dead-reap co 6 h: po 1500 zywych zrodel na serwis, polowa z ostatnich 30 dni.
  Oznacza tylko przy pewnym sygnale; siec i timeouty zostawia.

Sprawdzone na produkcji: 10/10 miniatur eporner i 23/23 postow sxyprn oznaczone, 17 zywych
postow nietknietych.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:00:11 +02:00
goon-foss
7f2fdfaed0 perf(api): lista scen jednego tagu od najnowszych po indeksie na scene_tags (GOON-64)
Ekran tagu (jeden tag, sort=release_date, 200 scen) sortowal po dacie z `scenes`, a filtrowal
po `scene_tags`. Postgres szedl od najnowszej sceny calego katalogu i kazda sprawdzal; przy
tagach rzedu 40-60 tys. scen (`rough-sex`) to 46 tys. scen na strone, 7-15 s na zimnym cache.
Dwie przebudowy samego zapytania nie pomogly (2x albo 30 s na duzych tagach), cache tez nie,
bo wejscia rozkladaja sie na setki tagow.

`scene_tags.sort_date` (0042) trzyma ta sama date co `_effective_date_desc()`, pilnowana przez
wyzwalacze w bazie, indeks (tag_id, sort_date DESC, scene_id DESC) w 0043. Wypelnianie 23 mln
wierszy: scripts/backfill_scene_tags_sort_date.py, partiami z pauza.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:00:11 +02:00
goon-foss
2cdc3d69da perf(api): tanszy odsiew stub-scen i zwarcie sprzecznego filtra playbacku
Some checks failed
Backend tests / test (push) Has been cancelled
Trzy zmiany wokol listy scen z filtrem originu, gdzie kandydatow sa dziesiatki tysiecy.

1. Kolejnosc czlonow OR w stub_exclusion_clause. Postgres liczy OR po kolei i przerywa na
pierwszym trafieniu, a sam czlonow nie przestawia. "Ma kanon" (jedyny czlon z JOIN-em do
sources) stal przed tanszym "ma performera", wiec placilismy za niego takze za sceny,
ktore performera mialy. Po zamianie ten czlon liczy sie 5146 razy zamiast 16 191.

2. Indeks scene_external_refs (scene_id, source_id), migracja 0041. Podzapytanie "ma
kanon" dociagalo source_id z heapu, bo stary indeks zna tylko scene_id. Teraz idzie
Index Only Scanem: Heap Fetches 16 191 -> 1241.

Razem: sort=title + origin=hqporner, strona 5 czyta 251 tys. blokow zamiast 294 tys.,
a na rozgrzanym cache trwa 305 ms zamiast 371 ms (mediana z 3 rund z rotowana
kolejnoscia). Zysk jest skromny. Pierwsze wykonanie na zimnym cache trwa 3-5 s i tak
zostaje, bo trzeba dotknac ~2 GB; sprawdzilem tez wariant z materializacja scen originu
i CTE, czyta wiecej blokow (302 tys.), wiec obecny plan zostaje.

3. has_playback=false RAZEM z filtrem originu to warunek sprzeczny, bo origin wymaga
zrodla z dead_at IS NULL. Wynik jest pusty z definicji, ale Postgres przemielal caly
katalog, zeby to stwierdzic: 148 s. Teraz zwarte na false(), 4,8 ms, dalej zero wynikow.
Przy has_playback=true z originem pomijamy z kolei nadmiarowe EXISTS o zywym playbacku,
bo origin sam go wymaga, a kosztowalo sonde na kazdym kandydacie.

Wyniki bez zmian: strony 1-5 dla origin=hqporner i dla glownego feedu porownane pozycja
po pozycji ze stara wersja daja zero roznic, a sam predykat stub zgadza sie na probce
200 tys. scen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:11:17 +02:00
goon-foss
b6a075b28a perf(db): kasuje ix_scenes_title_normalized, zbedny prefiks indeksu z 0039
ix_scenes_title_created_id_desc zaczyna sie od tej samej kolumny, wiec obsluguje wszystko,
co obslugiwal stary indeks: lookupy `title_normalized = X` z resolvera i skan zakresu przy
sortowaniu po tytule. Trzymanie obu to 367 MB na dysku i jeden indeks wiecej do utrzymania
przy kazdym zapisie do scenes, a ingest pisze tam caly czas.

Sprawdzone na produkcji przez BEGIN; DROP INDEX; EXPLAIN ANALYZE; ROLLBACK, zeby zobaczyc
wybor planera bez starego indeksu, a potem jeszcze raz po samym dropie:

  exact title lookup        1,76 ms -> 0,07 ms, Index Scan po nowym indeksie
  OR studio + exact title   ten sam ksztalt planu (BitmapOr), 3,7 ms
  OR data + exact title     Seq Scan przy szerokim oknie dat, ale to decyzja o release_date:
                            z enable_seqscan=off BitmapOr siega po nowy indeks
  trgm ILIKE                bez zmian, swoj indeks GIN

W wariancie z OR planer i tak siegal juz po nowy indeks, jeszcze zanim stary znikl.
Indeksy scenes: 3544 MB -> 3177 MB. Listy scen po dropie bez zmian (tytul 22-54 ms,
studio 38-73 ms na stronach 1-5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:47:24 +02:00
goon-foss
470beb4aa6 perf(api): sortowanie scen po studiu i tytule bez sortowania calego katalogu
Obie listy chodzily na produkcji w dziesiatkach sekund, bo klucz sortu nie mial nic
wspolnego z filtrami widocznosci (zywy playback, odsiew wertykalow, odsiew stubow).

sort=studio: nazwa studia siedzi w dolaczonej tabeli, wiec zaden indeks scen jej nie
zna i planer materializowal caly przefiltrowany katalog (ponad 2 mln wierszy), po czym
sortowal go w calosci. Odwracamy zapytanie: idziemy po studiach w kolejnosci nazwy i dla
kazdego dobieramy jego sceny przez LATERAL po ix_scenes_studio_effective_date_desc.
Zewnetrzny skan jest posortowany, wiec LIMIT urywa go po kilkunastu studiach. Sceny bez
studia dobiera osobny ogon, tak jak wczesniej robilo NULLS LAST. Przy filtrze po studiach
zawezamy tez sam skan studiow, inaczej szedlby przez wszystkie 33 tys.

sort=title: nowy indeks na cale ORDER BY (title_normalized, created_at DESC, id DESC).
Sam title_normalized zostawial Incremental Sort, a ten przed oddaniem pierwszego wiersza
musi zebrac caly kubelek scen o tym samym tytule.

Pomiary na produkcji (2026-09-16, ten sam zestaw filtrow, po rozgrzewce):

  studio strona 1            483 s  ->   41 ms
  studio strona 5            ponad 5 min ->   38 ms
  studio + tag blowjob        71 s  ->  226 ms
  studio + min_duration       48 s  ->   42 ms
  studio + studio_slugs      255 ms ->   44 ms
  tytul strona 1             709 ms ->   12 ms  (81 wpisow indeksu zamiast 3064)
  tytul + tag blowjob        ponad 90 s -> 1,5 s

Wynik jest identyczny co do wiersza: pierwsze 251 pozycji pelnego katalogu porownane
pozycja po pozycji ze starym zapytaniem daje zero roznic, tak samo dwa okna alfabetyczne
studiow (w tym Brazzers, zeby sprawdzic wewnetrzny LIMIT).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:37:26 +02:00
goon-foss
37e6b7f03c perf(api): liczniki +N ulubionych z cache i indeksem, podobne sceny z najrzadszych tagow
- /favorites i /favorites/studios: liczniki w cache procesu (swiezy 5 min, potem
  odswiezanie w tle), mark_seen zeruje od razu; parametr counts=false dla ekranow,
  ktore chca tylko stan gwiazdki
- licznik studiow idzie indeksem ix_scenes_studio_effective_date_desc (migracja 0038)
  i konczy po 200 scenach na studio: 10-25 s -> kilkanascie ms
- okno licznika sortuje tak jak lista (data premiery albo dzien dodania), wczesniej
  rozjechane od 0033
- similar_scenes: pula z 4 najrzadszych tagow zamiast grupowania po wszystkich
  (do 5 s -> ponizej 0,4 s)
- enrich-tags: tagi jednym zapytaniem po slugach i jeden INSERT
- /scene-favorites: serializer wsadowy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:05:43 +02:00
goon-foss
0df2236b37 feat(clips): feed Swipe z afinicznoscia, pamiecia widzianych i telemetria (etap 3)
Tabele per urzadzenie (migracja 0037): clip_seen (wykluczenie, wiersz przy
podaniu klipu), clip_feed_events (append-only: impression z dwell, skip,
complete, like, unlike, not_interested, autoswipe), clip_affinity (wagi
tag/nisza/tworca w JSONB), clip_favorites, clip_blacklist. GIN na clips.niches.

GET /clips/feed: kursor base64 {seed, pk, page} zamiast offsetu; 80% z puli
afinicznosci (skany po tagu/niszy/tworcy od losowego uuid, punktacja
sum wag + 0,3 ln(1+lajki) + swiezosc, max 2 klipy tworcy na strone) + 20%
explore (okno 2000 id po PK, losowanie wazone lajkami, zawijanie na koncu).
Bez wag 100% explore. POST /clips/events: zapis, podniesienie outcome,
ulubione, delty wag (like +5, complete +3, dwell>=10 s +1, skip<2 s -1,
not_interested -4 i -8 na wskazany cel). Onboarding 3 tapow, opcje,
blacklista tag/nisza/tworca, reset feedu, ulubione. Import miedzy
urzadzeniami kopiuje clip_seen, clip_favorites i clip_blacklist.

Job clips-maintenance co 24 h: decay wag x0,97, retencja zdarzen 90 dni,
reaper (HEAD na 2000 najdawniej sprawdzanych). Logika bez bazy w
app/clips_feed.py z testami (kursor, delty, cap/decay, punktacja, sklad).
2026-09-14 20:29:49 +02:00
goon-foss
4f5de92bfc feat(clips): tabele klipow do feedu Swipe + ingest RedGifs (etap 2)
Nowy wertykal krotkich pionowych klipow tworcow, osobno od scen: inne kolumny
(tworca, audio, lajki, nisze), inny cykl zycia, inna taksonomia, brak filtra
60 s. Tabele `clips` (unikalne source+source_id) i `clip_tags` z indeksem
(tag, clip_id) pod skan po tagu od losowego uuid w przyszlym feedzie.

Zrodlo nr 1: RedGifs, oficjalne API v2 z tokenem tymczasowym (zwiazany z IP
i UA, na 401 odnowienie). Globalne `order=latest` bez search_text daje ~100
na strone, kazde zapytanie widzi maks. 10 000 wynikow, wiec katalog chodzi
sie po niszach (skrypt backfillu). Pliki media.redgifs.com sa publiczne i bez
podpisu (206 z VPS-a i z telefonu), do bazy wchodzi gotowy adres.

Scheduler: job clips-latest co godzine (5 stron), zatrzymuje sie na stronie
bez nowych. Reaper (HEAD, 403/404/410 -> dead_at) jako skrypt, upsert
odzywia klip, ktory zrodlo znow podaje. API: GET /clips i /clips/stats do
pilota; wlasciwy feed, pamiec widzianych i telemetria to etap 3.

Sprawdzone tego dnia i odlozone: nsfwswipe (lustro v.redd.it, bez audio,
stoi od 19.08), Pornhub Shorties (zrodlo nr 2, HLS z tokenem 2 h, Referer
na segmentach, 0/110 performerow z kanonu).
2026-09-14 16:32:57 +02:00
goon-foss
be6c18b3b4 feat(scenes): sortowanie losowe (seed, okno po PK) i po dlugosci (indeks partial), zgloszenie ae6a045b
- random: md5(seed) -> uuid startu, okno 2000 id (8000 dla wertykalow) po pk_scenes,
  filtry na oknie; bez okna planer robil Merge Anti Join po 2,3 mln wierszy (7,6 s)
- duration_desc/asc: bez NULL, do 10 h, indeks ix_scenes_duration (migracja 0035)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 15:13:13 +02:00
goon-foss
f1b8b8d466 feat(favorites): przypinane strony (favorite_sites) z endpointami list/add/remove
Prosba z apki 2026-09-02: ulubione strony maja siedziec na gorze zakladki Sites. Po
stronie serwera jak reszta ulubionych, bo kod synchronizacji to device_id i tabele
kluczowane po nim przechodza na nowe urzadzenie razem. Origin bez klucza obcego
(zrodla nie maja tabeli po origin), za to przy dodawaniu sprawdzany w playback_sources,
a sam napis ograniczony wzorcem, zeby nie dalo sie wpisac smieci.

Migracja 0034 (create table, bez blokad). Sprawdzone TestClientem w kontenerze api:
dodanie, powtorne dodanie (created=false), lista w kolejnosci dodania, nieznany origin
404, zly wzorzec 422, usuniecie 204.
2026-09-02 22:44:35 +02:00
goon-foss
fa7fb668b6 fix(sites): sortuj po dacie premiery z fallbackiem na date dodania + napraw miniaturki
Zgloszenie 1df50de3 ("hqporner: brak nowych scen ze strony, brak miniaturek").
Oba objawy mialy inne przyczyny i oba sie potwierdzily.

1. Kolejnosc. Sceny byly i ingest dzialal (91 nowych w 7 dni, ostatnia tego dnia),
ale tuby daty premiery zwykle NIE podaja: xhamster 99,2% scen bez daty, xnxx 97,7%,
porntrex 95,8%, hqporner 93,5%, sxyprn 85,7%. Przy release_date DESC NULLS LAST
wszystkie bezdatowe ladowaly ZA datowanymi, wiec 82 z tych 91 scen siedzialy za
3,4 tys. starszych pozycji. Teraz sortujemy po dacie premiery, a gdy jej brak, po
dniu trafienia do katalogu; sceny z prawdziwa data (kanon) bez zmian. Wyrazenie jest
immutable (jawne AT TIME ZONE 'UTC'), wiec wchodzi do indeksu, migracja 0033.

2. Miniaturki. Kafelki bez obrazka byly podpisane FULLPORNER, nie HQPORNER. Picker
bral pierwsze zrodlo alfabetycznie, a tube:fullporner < tube:hqpornercom. Dwie wady
naraz: czesc miniaturek fullporner to 153-bajtowa atrapa oddawana z kodem 200, a
czesc wskazuje CDN hqporner, ktory odrzuca Referer fullporner.com (ten sam plik:
Referer fullporner.com 403, wlasny host obrazka 200, 30 kB). Wiec: przegladany
serwis wygrywa wybor miniaturki, a proxy obrazkow po 403 ponawia raz z wlasnym
originem obrazka.

Pomiar na 40 kafelkach hqporner: przed 38 ladowalo i 1 nie, po 39 ladowalo i 0 nie,
wszystkie podpisane wlasciwym serwisem. Glowny feed bez zmian.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:25:53 +02:00
goon-foss
8f6fa8c732 perf(scenes): indeks pod sortowanie serwisu po dacie premiery + fix wyscigu zapisu wyszukiwania
Zgloszenie 4cccc70f: sxyprn na liscie sites ladowal sie ok 18 sekund. Odtworzone:
/scenes?origin=tube:sxyprncom&sort=release_date schodzilo 19,23 s, a to samo z
sort=created_at 0,08 s. Ekran serwisu sortuje po dacie premiery, czyli
release_date DESC NULLS LAST, created_at DESC, id DESC, a istniejacy
ix_scenes_release_date jest ROSNACY: czytany od tylu daje NULL-e na poczatku i nie
obsluguje kluczy rozstrzygajacych. Planner leial wiec pelnym skanem scenes (3,9 mln)
i playback_sources (3,7 mln). Po zalozeniu pasujacego indeksu strumieniuje w
docelowej kolejnosci i konczy po 25 trafieniach: sxyprn 19,23 s -> 0,11 s,
hqporner 7,99 s -> 0,87 s. Drugi indeks po originie, bo playback_sources nie mial
zadnego.

GOON-4Y: add_saved_search sprawdzal istnienie i wstawial w dwoch krokach, wiec dwa
rownolegle zapisy tego samego zapytania (podwojne tapniecie, ponowienie po
timeoucie) oba widzialy pusto i drugi dostawal 500 na uq_saved_searches_device_query.
Teraz IntegrityError konczy sie oddaniem istniejacego wiersza. Test: 6 rownoleglych
zapisow -> 6x200 i jeden wiersz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:00:58 +02:00
goon-foss
544de9c5ba feat(ingest): backfill brakujacej dlugosci scen
1 755 421 scen nie ma dlugosci ani na scenie, ani na zrodle, wiec wypadaja z
feedu: w SQL "duration_sec >= 60" nie jest prawda dla NULL, przez co scena o
nieznanej dlugosci znika tak samo jak dziesieciosekundowy smiec. Filtr zostaje,
bo odcina smieci, uzupelniamy dane.

Swiadomie waskie originy. Uniwersalny regex trafia w smieci: porntrex ma w HTML
"duration: 5" reklamy, mypornerleak dlugosc ciastka 720, a xhamster i porntrex
wypisuja dlugosci filmow z paska obok (naiwne dopasowanie dalo 198s zamiast 240s).
Zla dlugosc jest gorsza niz brak, bo dedup opiera sie na niej najmocniej.

- xvideos, xnxx: VideoObject LD-JSON, 5/5 na probce
- xhamster: videoEntity z kontrola zgodnosci id z URL-a
- porntrex pominiety: strona szczegolow nie podaje wlasnej dlugosci

Kolejka jak przy phashu (plan raz, mielenie paczkami, przezywa restart):
845 899 scen, 500 stron co 5 min, czyli ok. 6 dni. Plan powtarza sie co 24h,
zeby swieze sceny bez dlugosci tez trafialy do kolejki.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:10:22 +02:00
goon-foss
0fed952c5c feat(filtr): minimalna jakosc w filtrze scen (720p/1080p/4K)
Prosba uzytkownika (zgloszenie 17376139). Backend mial juz parametr `min_quality_p`,
ale w praktyce byl NIEUZYWALNY i to zajelo wiekszosc pracy.

Wydajnosc, mierzona na produkcji przez EXPLAIN ANALYZE, prog 1080p:

  EXISTS na playback_sources (stan zastany)   32 s   3 556 612 odwiedzonych scen
  + indeks funkcyjny per-scena                24 s
  JOIN z DISTINCT po jakosci                   1,1 s W IZOLACJI
  ten sam JOIN + reszta filtrow feedu         52 s   planer wraca do skanu po dacie
  kolumna scenes.max_quality_p (0029)          2,0-2,6 s
  + indeksy czesciowe pod progi (0030)         0,09-0,11 s

Wniosek, ktory kosztowal najwiecej: ksztaltem zapytania sie tego NIE dalo zalatwic.
Koszt to LICZBA odwiedzonych scen, nie pojedynczy test, a przy `ORDER BY created_at
DESC LIMIT 24` planer i tak wracal do przechodzenia scen po dacie, gdy tylko doszly
pozostale filtry (bramka wertykalow, heurystyka szkieletow, prog dlugosci). Stad
denormalizacja + indeksy czesciowe z predykatem progu, ktore daja gotowe sortowanie.

Semantyka SUROWA, swiadomie: `quality` zapisujemy oportunistycznie, zna ja 6,5% scen
(210 302 z 3 236 368), najlepsze zrodlo ma 26% pokrycia. Wariant lagodny, przepuszczajacy
sceny o nieznanej jakosci, odrzucony po zmierzeniu: odsiewalby 88 647 z 3,2 mln, czyli
nie realizowalby intencji "nie chce ogladac ziemniaka". Filtr pokazuje wiec tylko jakosc
POTWIERDZONA, a UI mowi to wprost podpisem pod przelacznikiem, bo inaczej gwaltowny
spadek liczby wynikow wyglada na zepsuta aplikacje.

Warianty tekstowe 4K/UHD usuniete z warunku: 0 wystapien na 238 452 zywe zrodla,
a `OR ilike` blokowal uzycie indeksu.

Odswiezanie w taxonomy_counts (co 3h), z czyszczeniem do NULL gdy scena traci zrodlo
z jakoscia. Backfill: 210 302 sceny w 2m56s.

Zweryfikowane na emulatorze: sekcja MIN QUALITY renderuje sie z podpisem ostrzegawczym.
Lista wyszla pusta, ale to build debugowy dostajacy 403 (podpis debug spoza allowlisty),
nie blad filtra: ten sam URL z poprawnym podpisem oddaje 50 wynikow w 0,12 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:06:34 +02:00
goon-foss
a16883d2f3 feat(dedup): backfill phasha z zapisanych miniaturek
Root cause zgloszenia 9d272101 ("5 duplikatow, czemu nie zmergowane?"):
phash mialo tylko 37,8% grywalnych scen. Licza go WYLACZNIE niektore
konektory w momencie ingestu, wiec najwieksze zrodla maja miniaturke dla
~100% scen, a odcisk dla 1-2% (xvideos 1,8%, xnxx 1,4%, eporner 1,0%).
Material jest zapisany, brakowalo tylko obliczenia.

W zglaszanym przypadku 7 wierszy o identycznej dlugosci 2324 s, a dwa
kafelki z IDENTYCZNA okladka nie mialy phasha wcale, wiec jedyny sygnal,
ktory by je sklail, nigdy nie powstal.

Zakres CELOWANY, nie caly katalog: sceny w klastrze (performer, dokladna
dlugosc) o licznosci >1, bez phasha, ale z zapisana miniaturka = 238 704
pozycje (wobec ~1,4 mln scen bez phasha). Reszta nie ma sie z czym skleic.

Kolejka jako TABELA, nie zapytanie w locie: wyznaczenie kandydatow to
ciezki agregat po calym katalogu, ktorego nie chcemy powtarzac przy kazdej
paczce, a kolejka przezywa restart workera i widac z niej postep.

Weryfikacja end-to-end na zglaszanej scenie: ce81a110 dostal phash
85c1bac2f8c60ecf, czyli DOKLADNIE ten sam, ktory mial juz 8373bbb5 (dwa
z kafelkow uzytkownika) -> istniejacy phash_exact je teraz sklei.

Nie zapisujemy wartosci z phash_blacklist (zaslepki dzielone przez setki
scen). Throttling swiadomy: 800 co 15 min, ~3 dni na nadrobienie.

Dwie pulapki SQL zapisane w kodzie: `referer` ma 255 znakow wiec origin
wycinamy w SQL (pelny page_url wywalal planowanie), a rzutowanie musi isc
przez CAST() bo text() SQLAlchemy nie podstawia parametru za ktorym stoi
`::`.
2026-08-13 10:49:12 +02:00
goon-foss
ef8c255e1f perf(scenes): indeks trigramowy na title + ranking kandydatow do przegladu
Kazde szukanie slowa w tytulach bylo sekwencyjnym skanem 3,46 mln wierszy. Blokowalo
to ranking over-attribution: metryka "ile scen ma te nazwe w tytule, ale NIE jest do
niej przypisanych" wymaga jednego skanu NA KANDYDATA, a kandydatow jest ~800. Proba
zostala ubita po 10 minutach.

GIN + gin_trgm_ops obsluguje LIKE, ILIKE i regexy, a pg_trgm sam normalizuje wielkosc
liter, wiec indeks na surowym title wystarcza. Efekt: 26 ms zamiast sekund na
zapytanie, ranking 800 kandydatow w 48 sekund zamiast godzin. Indeks 315 MB.

Zbudowany CONCURRENTLY w autocommit_block: zwykle CREATE INDEX trzymaloby lock
blokujacy zapisy na scenes przez cala budowe i zatrzymalo ingest.

Ranking od razu pokazuje dwie rozne rzeczy. Czyste smieci ("Pornhub" 105.9, "Monster"
92, "Pretty" 63, "Nice" 42) oraz SAME NAZWISKA (Devine, Starr, Dior, Cruz, West,
Knight, Johnson, James) - wpisy powstale z rozbicia imienia i nazwiska, ktore potem
lapia kazda scene z tym nazwiskiem, nalezaca do zupelnie innej osoby.

Usuniete przy okazji: "Pornhub" (108 scen, 0 z kanonu, wszystkie to "full video on
pornhub") i "Precious" (119 scen, 0 z kanonu, sam przymiotnik).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:59:52 +02:00
goon-foss
034ddd644c feat(dedup): provenance przypisan performerow do scen (scene_performers.source_id)
scene_performers nie zapisywalo, KTO przypisal performera do sceny. Przez to nie dalo
sie odroznic obsady z kanonu od performera doklejonego przez tube-search, ktory
dopasowuje wynik po JEDNYM tokenie zapytania. Stad przypadek "Rapture": 30 z 72 jej
przypisan bylo falszywych (film "The Rapture" z Mimi Rogers, literowka "hymen
raptured", tytuly scen) i trzeba bylo je przegladac recznie, bo nie bylo sygnalu.

Kolumna trzyma NAJBARDZIEJ wiarygodne zrodlo, jakie przypisalo. Regula idzie tylko w
gore: NULL wypelnia cokolwiek, kanon nadpisuje scraper, scraper NIGDY nie zamazuje
kanonu. Inaczej dowolny pozniejszy tube-search zatarlby informacje, ze obsade
potwierdzilo TPDB. To samo przy merge_scenes: przy kolizji przenosimy lepsza
provenance na keepera, zeby scalanie nie degradowalo potwierdzonej obsady.

Backfill wypelnil 4 147 128 wierszy (96,3 procent) wnioskowaniem, ktore jest pewne:
scena z refami z DOKLADNIE jednego zrodla ma cala obsade stamtad. Sceny wielozrodlowe
(67 tys.) zostaja z NULL-em, bo tam zgadywanie zepsuloby sens kolumny. Wsadowo, z
commitem na paczke: pojedynczy UPDATE na 4 mln wierszy trzymalby locki na
scene_performers i zablokowal ingest.

Rozklad: tube-scraper 3 219 677, tpdb 747 540, stashdb 180 355.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:04:10 +02:00
goon-foss
3de8489ffa fix(dedup): blacklista zdegenerowanych phashy + scalenie 4647 duplikatow
Audyt duplikatow po phashu miniatur wykazal dwa rozne zjawiska, ktore wygladaly
jak jedno.

1. Zdegenerowane phashe. 73 wartosci wystepowaly przy >=10 scenach kazda, lacznie
przy 4375 scenach; rekordzistka byla dzielona przez 892 sceny o 892 roznych
tytulach i 1886 roznych performerach. To zaslepki i czarne klatki, nie odciski
scen. find_by_phash_within bierze najblizsza wartosc z calej tabeli, wiec taka
zaslepka zawsze wygrywala z prawdziwym duplikatem (dist 0). Do tej pory bronila
nas bramka dur_prox i nic sie nie skleilo, ale to zabezpieczenie drugiej linii.

Blacklista jest tabela, nie jednorazowym DELETE, bo sam DELETE nic nie daje:
zaslepka wraca przy kolejnym ingescie. Trzeba pamietac, ze wartosc jest
bezuzyteczna. Job co 24h dopisuje nowe i czysci odciski. Po czyszczeniu zero
grup >=10, najwieksza pozostala ma 9.

2. Realne duplikaty. 4647 scalonych. Przyczyna byla jedna: 97 procent par ma
perverzije po dokladnie jednej stronie, bo pisze tytuly z prefiksem studia i
performera, a reszta tubow daje goly tytul. Wbrew mojej pierwszej diagnozie NIE
trzeba tu ruszac scoringu tytulu (token_set_ratio i tak radzi sobie z prefiksem,
a sciezka phash idzie przed composite): 80 procent par to dlug sprzed 60+ dni, a
biezacy wyciek to okolo 1 dziennie. Nadmiarowe wiersze: 18452 na 9449.

merge_phash_exact_dupes.py dostal wykluczenie blacklisty. Bez tego byl grozny:
sam klaster 892 scen dawal ~397 tys. par do rozwazenia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 12:47:20 +02:00
goon-foss
80ac621344 fix(tags): collapse spelling-variant duplicate tags + prevent regrowth
Ten sam tag z różnych tubów tworzył osobne wpisy (Blowjob/Blow Job, Big Tits/
Bigtits, deep-throat/deepthroat, creampie-1/creampie, mojibake CJK 'Japanese'…)
bo resolve_tag brał slug/slugify(name) verbatim. ~11k nadmiarowych tagów.

- resolve_tag: przed utworzeniem nowego tagu szuka istniejącego kanonicznego po
  alnum-kluczu nazwy (_resolve_by_altkey) i reużywa go → warianty nie regenerują.
- Indeks funkcyjny ix_tags_name_altkey (migracja 0028) pod ten lookup.
- scripts/merge_altkey_tags.py: jednorazowy backfill istniejących duplikatów
  (kanoniczny = max scene_count, scene_tags/movie_tags/blacklisted_tags przepięte
  z dedupem na PK, dropy skasowane, taxonomy_counts odświeżone).
2026-07-21 15:59:33 +02:00
goon-foss
e215b48255 fix(watchporn): re-resolve stale CDN URL in-player, drop WebView page fallback
The zload CDN URL (remote_control.php) is a valid faststart progressive mp4 that
ExoPlayer plays fine when fresh: 206, no IP/referer binding, ~2h token TTL,
verified from a residential IP. Real-device failures (player_error/gone
telemetry) come from the static URL going stale: a rotated or dead CDN node, or
an expired token.

Instead of falling back to the tube page in a WebView (removed, it papered over
the real issue), PlayerScreen now re-resolves the scene fresh via the backend on
initial-load error for backend-native KVS tubes (watchporn). That yields a live
node plus a fresh token and stays on the native direct stream (0 VPS bandwidth).
For these origins a CDN 'gone' (404) means stale URL, not deleted, so we
re-resolve on gone too; a genuinely deleted post raises HosterDead (410) and
falls through to the normal fallback chain.

Also: add playback_events.error_detail (raw ExoPlayer message) to pin down the
exact failure of tubes we cannot reproduce on the emulator. Keep FLAG_SECURE on
release builds only (!__DEV__) so debug builds stay screenshottable for local UI
verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 01:18:08 +02:00
goon-foss
15e7c1646d fix(review): player recovery deadlocks, quick-play re-fire, backfill gaps, em-dashes
Addresses the ultra-review findings on this branch:

Player (PlayerScreen.tsx): the new recoveryPending mirrored the fallback-chain guards
by hand and could deadlock into a permanent "Reconnecting" spinner with no way to Mark
broken — for gone (410) sources on IP-bound tubes (re-resolve bails before setting
reResolveDone) and for any post-load error on those tubes (re-resolve is initial-load
only). Derive one reResolveApplicable flag (IP-bound AND initial-load AND not-gone) and
use it for both the chain gate and the spinner, so gone/post-load errors fall through to
proxy/WebView or the terminal error card. Seek-recovery now falls through to the chain
when player.replace() throws instead of returning.

Quick-play (SceneDetail): the autoplay route param persisted and autoPlay={i===0} re-fired
when the source list reordered (e.g. after Mark broken drops the dead source), bouncing the
user into the player. Consume it once via onAutoPlayConsumed -> nav.setParams({autoplay:false}).

Backfill semantics: performer-driven direct-scraper "backward fill" now tags scenes
backfill=True (search-by-name pulls the whole old catalog); merge coalesces backfill
(keep AND drop) so a fresh scene merged into a dead dup keeps NEW; deep-crawl only tags
backfill on a tube's FIRST sweep (swept_once) so re-sweep catalog growth stays genuine;
pilot script tags backfill.

Perf/migration: migration 0026 is now idempotent (IF NOT EXISTS; prod got the column via
manual ALTER) and adds ix_scene_performers_performer_id (favorites count filtered
performer_id with no index); index also created on prod.

Cleanup: deleted dead FavoriteSceneRow (unused import in two screens, stale isNew without
the backfill guard); removed em-dashes from all lines this branch added (user CLAUDE.md
rule), including the user-facing changelog / Settings / player-overlay strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:20:40 +02:00
goon-foss
07cc3fdfba fix(favorites): exclude bulk-backfill scenes from "+N new" (tube fake dates)
Browse scrapers backfilling old catalogs stamp the tube's import/post date as
release_date, so old content (e.g. 83 MissaX classics via perverzija, ~3600/3 days
across eporner/youporn/etc.) fake-ranked as newest and flooded the favorites "+N".
NULLing the dates was a non-starter — the stub filter would hide 251k performer-less
scenes. Instead: a Scene.backfill flag marks bulk catalog imports; they stay visible but
never count as "new".

- scenes.backfill column (+ index, migration 0026); resolve_scene/_process_scene thread it.
- deep_crawl tags scenes from pages beyond the "latest" threshold (>2) as backfill;
  latest pages + TPDB/StashDB delta stay genuine. Cursor reset re-sweeps page 1 so real
  new content is always caught fresh.
- favorites +N (performers + studios) excludes backfill within the top-200 window.
- SceneOut exposes `backfill`; mobile NEW badge + NEW-first re-sort skip it (badge==count).
- Retroactive: tagged 439k existing scenes in bulk (>10 non-canonical / studio / day)
  clusters. Device check: favorite-studios +N 11626 (naive) -> 236.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:04:01 +02:00
goon-foss
0f1f25393e feat(sources): 0-5★ ranking on Sites (freshness/metadata/plays) + playback telemetry
Rates each source on three axes the user asked for:
- freshness: how recently/often new content arrives (newest age + 7d volume)
- richness: metadata coverage (thumbnail/tags/performers/description/studio/duration)
- plays: does it actually play — from real playback telemetry when available,
  else a proxy from the resolve mechanism. 0★ = offline (gates the overall stars,
  so a fresh+rich source that doesn't play still ranks bottom — the hqfap/4k69 case)

Backend:
- playback_events: fire-and-forget telemetry POST from the app per playback attempt
  (origin + success/error + time-to-first-frame), append-only, 30d retention
- source_stats: per-origin computed scores, refreshed by a scheduler job (6h);
  /sources joins it and sorts by stars
- models + local migration 0025; new GOON_SCHED_SOURCE_STATS_HOURS setting

Mobile:
- Sites rows show ★ rating; tap the stars for a breakdown (axes + metadata %, plus
  whether "plays" is measured or estimated)
- PlayerScreen reports playback success/failure per source (native path only —
  symmetric, conservative); origin threaded through Scene/Movie play callsites

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:00:59 +02:00
goon-foss
84f0e91cba feat(api): per-device saved searches (keyword favorites)
User-report (mobilism): scenes are often poorly titled, so saved keyword queries are a useful extra retrieval strategy. New saved_searches table (device-scoped via X-Device-Id, unique per device+query, 50/device cap) + GET/POST/DELETE /saved-searches. Migration 0024. Verified CRUD on prod: add trims+dedups idempotently, empty rejected 422, delete idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:52:18 +02:00
goon-foss
c43c305941 feat(bug-reports): two-way replies (device-scoped) + admin reply endpoint
Reports were anonymous and one-way. Tie each report to the submitting device
(X-Device-Id), add an admin response back-channel, and let the app fetch replies for
its own device:
- migration 0023: bug_reports gains device_id, response, responded_at, response_seen.
- create_bug_report captures device_id.
- GET /bug-reports/mine (device-scoped) returns this device's reports + unseen count.
- POST /bug-reports/mine/seen clears the unseen flag.
- POST /bug-reports/{id}/reply sets the admin response (authored during triage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:35:44 +02:00
goon-foss
fe842b3603 feat(api): device-scope user state (favorites/progress/blacklists)
Public instance has no accounts, so all user state was GLOBAL in DB — new users
saw/overwrote each other's (and Jan's) favorites, watched badges and blacklists
(bug 2026-06-10). Add device_id (VARCHAR 64) to 9 state tables with composite PK
(device_id, entity_id); app sends X-Device-Id header (get_device_id dep). All
favorites/scene-favorites/blacklist/watch + scene&movie list/detail (is_favorite,
watched, blacklist-hide) now filter by device. Existing rows backfilled to
'legacy-shared'; POST /me/adopt-legacy reassigns them to the caller once. Old
clients (no header) map to legacy-shared so they keep working until OTA updates.

Migration 0022: add col, backfill, composite PK. Verified on prod: 967 progress
rows preserved, device isolation holds (new device sees none of legacy state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:58:01 +02:00
goon-foss
4078f7185d migration(0021): raise scene_tags.tag_id statistics target to 1000
Completes the literal-tag_id perf fix — the planner's MCV stats on tag_id are what
let it pick the index-walk for common tags. Default target (100) covers only the
top ~100 tags; 1000 extends correct cardinality estimates to mid-tier tags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:12:22 +02:00
goon-foss
cb4bb6c408 perf(scenes): drop exact count on filtered lists; index scene_tags(tag_id)
The filtered scene-list endpoints (default feed sends min_duration_sec=60, plus
has_playback / tag / q filters) took ~4.5s — and an idle server. Profiling showed
the entire cost was the bounded COUNT subquery over the EXISTS filters: Postgres
would not reliably early-terminate at the cap under psycopg bound params, scanning
the whole matching set (~858k for has_playback). Counting over the PK and using a
literal LIMIT helped some cases but the plan stayed unstable.

Fix: stop computing an exact count for filtered lists entirely. The mobile client
paginates by has_more (per_page+1 fetch), never by total — total is only the "N+"
UI counter. Derive total as a lower bound from the page + has_more after the fetch.
This removes the count query from every filtered request.

Result (end-to-end, authenticated): default feed 4.5s -> ~0.1s, has_playback
4.4s -> ~0.1s, q/studio/normal-tag filters all <0.3s. Also added index
scene_tags(tag_id, scene_id) (PK led with scene_id, so tag->scenes did a seq scan).

Remaining: a single enormous tag (e.g. "anal", ~163k scenes) ordered by recency
still gathers-all-then-sorts in the fetch (~5s); normal tags are <0.5s. Tracked
in #22 for a denormalized recency-ordered approach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:00:36 +02:00
goon-foss
e2e7ab3e22 perf(taxonomy): denormalize scene_count for tags/performers/studios
Counts for /tags, /performers, /studios and /favorites were computed live
per-request by aggregating scene_tags / scene_performers with an EXISTS to
playback_sources. As the catalog grew to ~1.7M scenes (6.3M scene_tags) this
ran ~4.3s for /tags?order=popular (x2 incl. the total count) and ~950ms for
the default /scenes count, making those screens load in several seconds.

- migration 0019: add scene_count (+ DESC index) to tags/performers/studios
- background job _job_refresh_taxonomy_counts (every 3h) recomputes the counts
  in one UPDATE..FROM each (IS DISTINCT FROM to skip unchanged rows)
- /tags, /performers, /studios scenes path now read the column + ORDER BY the
  indexed scene_count; for_movies paths keep live aggregation (small tables)
- favorites read denormalized scene_count instead of a grouped EXISTS aggregate
- /scenes default count: 10-min in-process TTL cache (header is approximate)

Measured: /tags?order=popular&per_page=500 ~8s -> 66ms incl. serialization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:53:48 +02:00
goon-foss
a4457d98d2 feat(movies): watched/continue-watching tracking end-to-end
Bug-report b207ff17 2026-05-26 ("przydaloby sie oznaczenie filmow juz
obejrzanych" - sceny mialy watched badge + dim, filmom brakowalo).

Backend:
- alembic 0018_movie_play_progress: nowa tabela (mirror scene_play_progress)
- MoviePlayProgress SQLAlchemy model
- MovieOut schema dolane finished/position_sec/last_played_at
- POST+DELETE /movies/{id}/progress endpointy (upsert via pg ON CONFLICT)
- _movie_to_out wstrzykuje progress z DB

Mobile:
- RouteParams.entityKind: 'scene'|'movie' (default scene dla back-compat)
- PlayerScreen NativeVideoPlayer + EmbedWebViewPlayer dispatchuja
  upsertProgress vs upsertMovieProgress po entityKind
- MovieDetailScreen przekazuje entityKind='movie' do nav
- MoviePosterCard renderuje dim + check badge + progress bar
  (parity ze ScenesScreen pattern)

Wczesniej MovieDetail przekazywal movieId jako sceneId -> backend
/scenes/<movieId>/progress zwracal 404 (silently caught). Po dodaniu
dedykowanego movie endpoint proper routing dziala.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:24:06 +02:00
goon-foss
ad0284585b Initial commit
Goon — self-hosted aggregator for adult-content scene metadata.

Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites.
Cross-source deduplication via perceptual hash + Levenshtein distance.
FastAPI backend + APScheduler worker + React Native (Expo) mobile client.

FOSS, ad-free, donation-funded. See README for details.
2026-05-20 10:10:22 +02:00