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>
Ostatnia klasa smieci: REALNA osoba o nazwie bedacej pospolitym slowem, do ktorej
tube-search dokleja wszystko, co ma to slowo w tytule. Rapture miala 30 z 72
przypisan falszywych (film "The Rapture" z Mimi Rogers, literowka "hymen raptured",
tytuly scen).
NIE MA na to sygnalu automatycznego. Przetestowalem piec i zaden nie rozstrzyga:
- sama provenance ze scrapera: realni tworcy spoza TPDB (Amouranth, MollyRedWolf)
tez maja 100 procent przypisan ze scrapera
- obsada z kanonu na scenie: backfill 0026 wypelnil tylko sceny jednozrodlowe, wiec
sceny mieszane maja NULL - 1 trafienie na 560 tys.
- udzial nazwy w tytule: u niszowych prawdziwych tez ~100 procent (Amouranth 100,
Clanddi 100)
- obce sceny ze slowem: myli generyczne uzycie z pominieta atrybucja
- stosunek tag/performer >=1: wyklucza 79 pozycji, ale samej Rapture BY NIE ZLAPAL
Zostaje czytanie tytulow. Skrypt robi z tego kilka minut zamiast pol godziny: listuje
komplet z provenance i kontekstem, liczy wskaznik pospolitosci nazwy, przyjmuje liste
prefiksow do odpiecia i DOMYSLNIE nie rusza przypisan potwierdzonych przez kanon.
Bez trybu rankingu wszystkich kandydatow: bez indeksu trigramowego na scenes.title to
skan 3,4 mln tytulow RAZY ~800 kandydatow. Sprobowalem, ubilem po 10 minutach.
Przy okazji przeglad zdemaskowal "Precious" (119 scen, 0 z kanonu) - wszystkie
przypisania to przymiotnik ("precious stepdaughter", "her ass is precious"). Usuniete.
Skala reszty: 1335 kandydatow z >=20 scenami, 539 z >=100. Nie ma sensu przerabiac
wszystkich - narzedzie jest na zglaszane przypadki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Po scaleniu 4647 par zostalo 7 nowych, POWSTALYCH przez ten przebieg. Merge
przenosi odciski na keepera, wiec scalona scena zbiera phashe wszystkich
wchlonietych (jedna miala ich 14) i zaczyna kolidowac z obcymi scenami przez
odziedziczony phash, ktory nie przedstawia nawet jej tresci.
Ocena recznie po obsadzie i studiach: 5 z 7 (71%) to byly falszywki, m.in.
Brazzers vs Reality Kings i AdultTime vs Teen Curves. W parach pierwszego rzedu
bylo 0 falszywek na 18. Czyli ponowne odpalenie skryptu bez zmian scalaloby
blednie.
Rozdziela je podobienstwo tytulu z duzym marginesem: prawdziwe duplikaty 92,1 i
84,8, wszystkie falszywki 23,0-52,4. Prog 0,70 lezy w srodku luki. Prefiks studia
progu nie rusza, bo token_set_ratio traktuje goly tytul jako podzbior tytulu z
prefiksem.
Po wpieciu: 2 prawdziwe scalone, 5 odrzuconych, kolejny przebieg nie generuje
nowych par. Zbiezne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The domain fallback built its guess from the REVIEW slug: when pdude.link
resolved to the porndudecams interstitial, "youperv-website-review" became
youperv-website-review.com, which of course refused the connection and the
candidate was filed as dead. That hit 15 of 144 entries, and among them were
youperv and pornbusy, which we later found independently and shipped at
4.5/5. Now the -website-review / -review suffix is stripped, and 13 of those
15 answer 200.
The audit also fetched with plain httpx, whose TLS fingerprint most
Cloudflare-fronted tubes reject, so timeouts and 403s were being read as
"dead" for sites that work fine in a browser. It now goes through
browser_get (curl_cffi), the same path our scrapers use, so the triage
finally measures what production sees.
Worth knowing before trusting the output: the heuristic score does NOT
predict a candidate's value to us. It matches cosmetic homepage markers,
and on sites we have since evaluated properly it scored youperv 0 and
pornbusy -1.5 (a false auth_wall) while giving 2.5 to hqfap, which we
removed as an orphan factory. Use the refreshed file as a list of live,
unexplored domains (95 of them) and judge each one on canonical match and
metadata quality, not on this number.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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).
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>
Missing-merge duplicates (same performer + identical normalized title + identical duration-to-the-second) that bulk_dedup misses — tube re-scrapes and cross-tube re-ingests like porn00 pulling a video already present from xnxx (reports 28fe8181/32df33b1). Extracted the proven merge_exact_title_duration logic into app/scheduler/title_duration_dedup.py (script now a thin wrapper), wired a 12h scheduler job (playback-only = what users actually see, GOON_SCHED_TITLE_DEDUP_HOURS). Signal is near-certain (two different videos don't share byte-identical title AND exact duration); no shared performer = not merged (over-match guard). Verified: job registers (jobs=14), backlog currently 0 after the one-shot global merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one-off cleanup merged ~13.5k same-video-different-title dupes, but they regrow as
these sibling tubes re-ingest under new titles. Wire the asset-id+duration merge into
the scheduler (every 12h, GOON_SCHED_THUMB_DEDUP_HOURS, 0=off) so it stays clean.
Shared logic lives in app/scheduler/thumb_dedup.py (run_thumb_asset_dedup); the one-shot
script now imports it. Same tight signature as the cleanup: family hosts only + identical
duration (the bare asset-id number is reused across unrelated CDNs, so cross-host/diff-
duration grouping is excluded). Reports 205b17d9 / 5a2944cb.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These sibling platforms share one video-id space and ingest the same video under
different titles, which bulk_dedup misses (different titles, no phash). Match by the
asset-id in the thumbnail path (/<bucket>000/<id>/) on img.hdporn.gg|fullmovies.xxx plus
identical duration, and merge. Hard host restriction + duration guard: the bare number
is reused for unrelated videos on other CDNs (verified via dry-run), so cross-host or
different-duration grouping is excluded. Run scoped (studio id) or global; dry-run by
default. Reports 205b17d9 / 5a2944cb. Ran on Parasited: 43 pairs merged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolve_post() now distinguishes "Post Not Found" (mark dead_at — the
link wouldn't play anyway) from a live page with no fresh poster (leave
untouched), on top of the existing thumbnail refresh. Batched into
refresh_batch() with refreshed/dead/untouched counters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CORRECTION to earlier "unrecoverable" call: the /post/<id> page is alive (200) and
DOES expose the scene's own fresh-signed poster via og:image / <video poster>
(post-id embedded, current timestamp) — only the STORED thumbnail URL had rotted.
Search/listings don't re-surface old posts (0 overlap), but per-post fetch works.
scripts/refresh_sxyprn_thumbs.py: iterate live sxyprn sources, fetch post page,
extract fresh og:image, UPDATE thumbnail_url (verified: refreshed URLs return 200).
_job_refresh_sxyprn_thumbs: every 12h refresh the 1200 least-recently-updated sources
(cycles the ~19k catalog within the expiry window). Pairs with the scene_resolver
overwrite fix so refreshed thumbnails stick.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final Polish-char print crashed with UnicodeEncodeError on Windows cp1252 stdout
AFTER a successful publish, making exit code 1 misleading. Reconfigure stdout/stderr
to UTF-8 up front.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--playback-only restricts to scenes with live playback (app-visible dupes only).
Progress print every 500 merges for long global runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
merge_scenes never reassigned playback_sources → ON DELETE CASCADE dropped them
with the absorbed scene. Cross-source (canonical) merges rarely had tube playback
so it hid, but tube-dup merges silently LOST playback links. Add _move_playback_sources
(global unique (origin,page_url) guarantees no collision on reassign).
+ merge_exact_title_duration.py: catches missing-merge dupes bulk_dedup misses
(same performer + identical normalized title + identical duration_sec, no phash).
Bad Bella had 25 such pairs (bug-report ef92809d "duplikat, te same miniatury").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'--workers 3' set limit=3 because the bare '3' also hit the isdigit() branch.
Skip flag-value positions when scanning for a positional LIMIT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xvideos renders the scene's models as `<a href="/models/slug">...<span class="name">
Display Name</span>...`. The old _MODEL_RE wanted text immediately after the anchor
`>` and never matched current markup → browse-scraped scenes landed with 0 performers
(bug-report 2026-06-07: "no actors, but Rebecca Johnson is on the page"). New regex
captures slug + nested span.name, bounded within the anchor. + backfill script for the
~11.9k existing zero-performer xvideos scenes (54% have a real /models/ link; resolver
merges names to canonical by name_normalized).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
audit_false_merges only auto-fixes n>=3 (majority disambiguates the outlier); n=2
was "needs human review" — but the merge-review UI is gone, nobody triages 500+.
Measured: of 535 n=2 duration-divergent scenes, ALL have a canonical scene.duration_sec
(TPDB/StashDB) and 531 have exactly one source matching canonical (±20%) + one >2x off
→ unambiguous false-merge. Kill the off source (works both directions since canonical is
corroborated by the matching keeper, unlike the Omar-case the n>=3 audit guards against).
Applied: 529 sources marked dead (4 ambiguous skipped). Reversible (dead_at).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bug-report 2026-06-03 ("ten sam czas, ta sama miniaturka, czemu się nie mergują"):
duplicate scenes not merged at ingest. Exact phash alone is noisy here (95% are
collisions on shared thumbnails/intro frames — different scenes; bulk_dedup scorer
correctly gives 0 auto-merge). The safe subset is exact-phash AND same duration
(±3s) AND shared performer/title — near-certain same scene. Same-duration is key:
it excludes the false-merge pattern (short-clip-vs-full has DIFFERING durations).
- scripts/merge_phash_exact_dupes.py: one-off, dry-run by default, per-pair re-fetch
(handles clusters). Applied: 30 merged.
- bulk_dedup: add `_pairs_exact_phash` (SQL O(N log N), not the O(N²) Hamming scan)
+ strategy "phash_exact" — gated by the normal scorer (surfaces review candidates,
no risky auto-merge), schedulable for ongoing exact-collision review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bug-report 2026-06-01 (48d6cc6b): scene shows canonical duration from TPDB
(real 22min studio scene) but the only live playback_source is a short tube
teaser (xnxx 21s) → "shows 22m, plays <1m". When ALL live sources are a tiny
fraction (<15%) of a known canonical (>300s), the scene has no real playback;
mark those sources dead → scene becomes orphan → hidden (has_playback=false),
consistent with the orphan-hiding policy. Reversible (dead_at), conservative
(skips scenes with any unknown-duration or full-length live source).
Applied on prod: 182 sources dead across 174 scenes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The umbrella Source.name for all direct tube scrapers (deep-crawl, browse-latest,
performer-driven) was "pornapp" — a misleading leftover from the removed external
porn-app API. It read like a dependency on a third-party "pornapp" service; it is
not — these are our own scrapers hitting 25+ tubes directly (kind=scraper,
origin tube:<sitetag>). Renamed to "tube-scraper" via a single SCRAPER_SOURCE_NAME
constant; DB row renamed in place (UPDATE name, same id) so all ingest_runs +
external_records history stays linked. No behavior change — external_id keying
(sitetag:url) and dedup are unaffected.
NOTE: playback_sources.origin "pornapp:<sitetag>" prefix is a separate legacy
format (resolve_playback parses it) and is intentionally left untouched.
Verified on prod: row renamed (0 stray "pornapp"), new runs land on "tube-scraper".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TPDB taxonomy emits numbered-duplicate tags (name "Bubble Butt2"); slugify
yields "bubble-butt2" (no separator before digit), so resolve_tag created a
separate tag alongside "bubble-butt". Tube scenes inherited the dup via
scene-merge → 75 pairs, ~10k scene_tags on the wrong tag.
- resolve_tag: canonicalize "<base>2" -> "<base>" when base exists (handles
current + future; trailing-"2"+alpha guard leaves milf-30/teen18 intact)
- scripts/merge_dup2_tags.py: one-off bulk merge (scene_tags + movie_tags +
blacklist) and taxonomy-count refresh
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ad-hoc research tool: for a list of candidate tubes, fetch a listing page, grab a scene
URL, and classify the detail — reachable / JSON-LD VideoObject / duration / performers /
tags. Used 2026-06-03 to evaluate deep-crawl candidates (redtube + drtuber look strong;
pornhub/spankbang/porntrex/hqporner/youporn rejected; nuvid/motherless bare).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
We ingested only ~3% of each browse tube's catalog (porndoe >62k scenes; we had 1959)
because tubes were hit only by performer-search + top-N browse. Pilot (porndoe pages
64-110): 1119 new scenes, 100% playable + 100% tagged, 0% canonical overlap (purely
additive — content not in TPDB/StashDB).
- app/scheduler/deep_crawl.py: round-robin over ALL_BROWSE_SCRAPERS, per-tube page cursor
in app/_state/deepcrawl_state.json (no DB migration), deep-paginate from the cursor,
idempotent (resolver skips known by raw_hash), mark 'exhausted' at catalog end then
reset cursors for an incremental re-sweep.
- _job_deep_crawl: hourly, 60 pages/run (~1860 scenes, ~22 min), wrapped in the 1h
hard-timeout; registered in build_scheduler (jobs=10).
- config: sched_deep_crawl_hours=1, deep_crawl_pages_per_run=60, deepcrawl_state_path.
- scripts/pilot_porndoe_deepcrawl.py: one-off pilot used to validate the approach.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Publishing the OTA from Windows git-bash failed at the scp step (2026-06-02):
- git-bash (MSYS) rewrote the /root/... env path to 'C:/Program Files/Git/root/...'
before Python saw it → upload targeted a bogus remote dir.
- scp local source 'C:\...\dist' is parsed as host 'C' (drive letter = host).
Fixes: default runtime 1.0→1.1 (active channel, app.json runtimeVersion=1.1); scp
source passed as '.' with cwd=DIST (no drive letter); MSYS_NO_PATHCONV=1 in subprocess
env; defensive un-mangle of a git-bash-converted VPS_BASE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scene.duration_sec was NULL for ~74% of playable scenes (tube duration lives on
playback_source, never propagated to Scene), so the mobile min_duration_sec=60 filter
(Scene.duration_sec >= 60; NULL fails) silently hid them — surfaced as '119 in favorites,
14 after entering the performer' (Safira Yakkuza).
- resolver: _effective_duration() falls back to max live playback_source duration when the
connector provides no scene-level duration (forward fix, used in create + update).
- scripts/backfill_scene_duration_from_playback.py: one-off idempotent backfill (recovered
204,014 scenes).
- taxonomy_counts: scene_count now counts playable AND duration_sec >= 60, matching the
always-60s-filtered scene lists, so favorites/performer/studio/tag badges agree with what
the scene screen actually shows (Safira: 39 == 39).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- paradisehill.fetch_movies compared release_date coerced to midnight against the
`since` timestamp, so the chronological crawl stopped at the first upload dated
the same calendar day as `since` and silently dropped most new movies (0-2 seen
per run; Movies tab stalled). Compare by DATE with a 1-day grace instead; idempotent
external_records upsert dedups the re-fetched recent window.
- scripts/backfill_paradisehill_movies.py: one-off no-delta deep crawl to recover the
backlog missed during the bug (idempotent, resumable).
- docs: correct stale 'raz dziennie/24h' browse-latest comments to 6h (4x/day), the
actual configured cadence (config.py sched_browse_latest_hours=6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolver/perf:
- find_by_phash_within: nearest match via Postgres bit_count over bit(64) XOR
instead of Python scan of all phash fingerprints (~20x faster per scene;
unblocks long delta runs that were killed mid-run before since advanced).
Scheduler/reliability:
- reap ingest_runs stuck in 'running' on worker startup (killed_by_restart).
- smoke_test: per-source ingest health, stuck-run and browse-freshness checks
-> Sentry; exclude killed_by_restart from the failed-run alarm.
Tags (ingest with tags + fill blanks):
- wire infer_tag_slugs into normalize_scene so tube scenes get title-inferred
tags (was dead code); union with connector tags.
- scripts/backfill_inferred_tags.py: keyset/batched/idempotent backfill for
existing tagless scenes (playable tag coverage 16% -> ~52%).
Clip-store:
- skip ManyVids/IWantClips/Clips4Sale/... from canonical sources at ingest
(GOON_SKIP_CLIP_STORE, default on) — permanent orphans, ~56% of canonical
ingest, never have a free-tube playback source.
Browse tubes:
- enable fullmovies + hdporn.gg: studio parsed from title prefix instead of
the /networks/ sidebar (which always yielded the first listed network);
drop phash compute (pilot: 0% canonical hit within Hamming 5 — auto-screenshots),
matching relies on title/performer/duration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in remediation for the duration-inconsistent scenes found by the audit.
Scope is deliberately narrow and reversible:
- only scenes with >=3 duration-bearing sources AND max/min ratio > 3x
- anchored on scene.duration_sec (the canonical value), never the median of
sources (a median is wrong when several bogus short clips outvote the real
full-length source)
- marks dead ONLY sources that are >2x SHORTER than the canonical — a falsely
merged source is almost always a short SEO clip/preview. Sources longer than
the canonical are left alone, since an over-long outlier more often means the
canonical duration itself is too low (so killing the long source would drop
the real video); those stay for manual review.
- guards that at least one live source remains
- dry-run by default; --yes to apply; sets dead_at (reversible), not delete
First run marked 514 short-clip sources dead across 228 scenes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only data-quality audit for scene merges made before the 2026-05-12
scoring hardening (which now caps weak-signal aggregator matches at 0.85 and
tightened the duration bump to <=3s). The auto-merge candidate log does not
record which external_ref was attached, so a merge cannot be reversed from the
log alone. Instead this detects false merges by their effect: a scene that
absorbed a different video ends up with playback_sources of inconsistent
durations (e.g. a 60s clip alongside a 2h source).
Reports counts + severity buckets by max/min duration ratio, can list the worst
offenders with a per-source breakdown, and can export suspects to JSON. Mutates
nothing — remediation (detach/mark-dead the outlier source) is left as an
explicit, separately-decided step because short durations can be legitimate
(previews) and n=2 scenes are ambiguous about which source is canonical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DIAGNOZA NA EMULATORZE (emulator-5554, goon-v0.1.9.apk):
Dwa błędne założenia z poprzednich sesji obalone empirycznie:
1. RUNTIME: APK ma EXPO_RUNTIME_VERSION="1.0" (NIE 0.1.9 — pomyliłem versionName
z runtime). App akceptuje TYLKO manifest runtime 1.0. Mój wcześniejszy
"fix" na 0.1.9 (211aaaa) był wstecz — app go ignorował. Cofnięte: app.json
+ publish_update RUNTIME_DEFAULT z powrotem na "1.0".
2. CRASH: prawdziwa przyczyna "nic się nie pojawia" — OTA bundle z expo-font
crashował: "Cannot find native module 'ExpoFontLoader'" → expo-updates
ErrorRecovery rollback. APK (build 22-maja) nie ma natywnego ExpoFontLoader
(expo-font dodany 30-maja, PO buildzie APK). OTA NIE MOŻE dostarczyć native
modułu. Potwierdzone: embedded bundle + served bundle grep = 0 ExpoFontLoader;
stary font-bundle crashował, font-stripped NIE.
FIX: usunięto useFonts z App.tsx + expo-font import; theme.fonts → undefined
(system font); SceneTile/MoviePosterCard/navigation/GoonWordmark fontFamily →
fontWeight. Wszystko inne (2-col grid, oxblood, logo SVG-RNSVG-jest-w-APK)
zostaje. Custom fonty wrócą przy rebuildzie APK z expo-font (option B).
ZWERYFIKOWANE: bundle d5b87e5c (runtime 1.0, 0 ttf) — emulator launch:
`ReactNativeJS: Running "main"`, zero JS errors, brak ExpoFontLoader crash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ROOT CAUSE wszystkich "znikajacych" OTA (2026-05-29..30, ~6 publishow w prozni):
zainstalowany APK ma EXPO_RUNTIME_VERSION=0.1.9 (AndroidManifest), ale app.json
mialo runtimeVersion "1.0" i publish_update.py defaultowal --runtime 1.0.
Updaty ladowaly w /expo-updates/1.0/, a app z headerem expo-runtime-version:0.1.9
dostawal HTTP 204 (no update) i nigdy nic nie aplikowal mimo "OK live".
Fix:
- app.json runtimeVersion "1.0" -> "0.1.9" (== APK)
- publish_update.py RUNTIME_DEFAULT "1.0" -> "0.1.9"
- Republished caly skumulowany bundle pod 0.1.9 (ce275235) — zweryfikowane:
manifest dla expo-runtime-version:0.1.9 zwraca 200 + runtimeVersion:0.1.9 +
bundle 4.76MB serwuje 200.
Stary /expo-updates/1.0/ (~40 nieaplikowanych updateow) do usuniecia osobno.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.