Playback reports ("doesn't start", "audio lags") came in with no way to know the
device or which source/server was used, forcing a follow-up question. Now the bug
report auto-context also carries:
- device = phone model / Android version (Platform.constants, zero-dep)
- play = last-played origin/host, mode, position, and last player error
New in-memory lastPlayback store written by PlayerScreen on source/status change, read
by BugReportFAB when composing a report. Reset on app restart (current session only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User request (07-10): a quick way to grab the direct video URL to share or open
elsewhere. Added a link icon in the player top bar that copies the current playing
URL to the clipboard (direct CDN URL for most sources, so it pastes into VLC/MX/a
browser; proxy-wrapped/referer-bound sources are best-effort).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No-accounts app, so favorites/saved-searches/hidden-list/watch-progress live
per-device (keyed by X-Device-Id). Users asked how to move them to a new phone.
Backend: POST /me/import/{source_device} copies all device-scoped tables from a
source device to the caller (dedup via NOT EXISTS, new UUIDs for id-PK tables like
saved_searches). Copy not move, so the old phone keeps everything; idempotent. The
device_id is a random UUID so knowing it is the authorization (fine for a keyless app).
Mobile: Settings -> "Backup & sync" shows this device's sync code (copy button) and a
Restore field to paste the other device's code; invalidates favorites queries after.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hqfap.com's Cloudflare blocks the VPS (Hetzner) IP — the server times out fetching
the page (0 bytes), so backend resolve returns None (503) or a stale/IP-bound URL that
fails on the phone (35 player_errors / 3 days, the top error source). But the page loads
fine from residential IPs (200, 0.7s) and its okcdn/vstor/cdnde mp4 is portable cross-IP
(srcIp not enforced, verified 206 from residential).
Fix: phone-side resolve, same pattern as sxyprn/eporner/fpoxxx. New hqfapResolver.ts
(fetch page on device -> JSON-LD contentUrl -> direct mp4, mirror of hqfap.py) wired into
the proactive phone-resolve chain in SceneDetail and the on-error re-resolve in
PlayerScreen; hqfapcom added to PHONE_RESOLVE_ORIGINS. Backend hqfap.extract stays as a
last-resort fallback.
Note: sxyprn/eporner were checked and are NOT broken (95-96% play success; their "gone"
events are real deletions), so no change there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
supjav is an embed-aggregator (voe/streamtape/turbovid/fc2stream). It was playing
through the WebView fallback because none of its hosters had a native phone-side
resolver wired into the scene player. voe.sx does have one (voeResolver.ts, already
used for movies) — it just was not wired for scenes.
- PlayerScreen: wire isVoeUrl/resolveVoePage into the hoster routing (like
dood/filemoon), so a voe embed resolves on the phone to a direct m3u8
(token bound to the phone /16, plays direct, no WebView, no proxy).
- supjav extractor: return voe first (sorted by URL), and stop putting the raw
hostname in `quality` (the digit in "fc2stream" was parsed as quality 2 and won
_pick_best) — use a clean digit-free server label instead.
- SceneDetailScreen: for hoster-only scenes, open the first embed (the extractor
puts the natively-resolvable one first) rather than _pick_best's choice.
Verified: supjav's voe embed resolves to a real HLS manifest (200,
application/vnd.apple.mpegurl, #EXTM3U). WebView stays only as last-resort fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth JAV vertical source (origin tube:supjav, gated to JAV tab via JAV_ORIGINS).
Browse: CF-blocks datacenter IPs, so listing goes through the Bright Data ISP proxy
with retry (the proxy rotates IPs and CF only lets some through). Homepage-latest only
(~66 newest); deeper /page/N/ stays CF-403 even via proxy. Metadata parsed from the
listing cards (title, thumbnail, JAV code, year/month) rather than per-post details,
which are flaky and slow through the proxy.
Stream: supjav hides the real hoster behind a per-server data-link (hex). base.js loads
lk1.supremejav.com/supjav.php?l=<data-link>, which reverses the hex string and fetches
?c=<reversed>, 302-ing to the hoster (TV->turbovid, FST->fc2stream, ST->streamtape,
VOE->voe). The extractor reproduces that: fetch detail via proxy, reverse each data-link,
resolve through lk1 (reachable direct from the server), return the hosters as type=hoster
so the phone resolves them (dood/filemoon native, the rest via the WebView fallback on the
residential IP). lk1 needs no proxy; only the detail fetch does.
Backend-only, no mobile change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second JAV vertical source (origin tube:vjav, gated to JAV tab via JAV_ORIGINS).
Browse: SPA has no server-rendered listing, so id-walk over sitemap_vids
(newest = highest video id) feeding the rich JSON metadata API
(/api/json/video/1/<floor>/<id>/<id>.json): title, duration, post_date,
channel->studio, models->performers, categories+tags.
Stream: videofile.php returns video_url in two obfuscation layers, Cyrillic
homoglyphs (M/C/A/E) over a custom base64 alphabet (comma->slash, tilde->pad,
dash->plus). Decoded get_file is absolute (old shared-txxx videos) or relative
(new vjav-infra, prepend host); adding f=video.m3u8 yields a portable, time-bound
ahcdn HLS (referer=none whitelisted, not IP-bound). Returned as m3u8 +
mobile_direct_ok so playback routes it through /proxy/hls: manifest passthrough,
segments direct from the phone (verified 206 cross-IP, 0 VPS video bandwidth).
Backend-only, no mobile change (JAV tab + jav param already shipped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JAV is a distinct vertical (user decision): Asian codes/titles that do not dedup
against the western catalog, so they must not flood the main feed.
Backend (scenes.py): JAV_ORIGINS = {tube:javflix, tube:javguru, tube:vjav,
tube:supjav}; list_scenes gains a `jav` param. Default (jav=false) excludes any
scene with a live JAV-origin source; jav=true returns only those. The cached
default-count and _is_pure_default also exclude JAV so the main feed count matches.
JavflixScraper is now registered in ALL_BROWSE_SCRAPERS (scheduled ingest lands in
the JAV section, gated). Scraper hardened: requires a real server button
(class="myLink") so static pages (Terms/FAQ) are skipped, and unescapes HTML
entities in the title.
Mobile: a "JAV" top tab reuses ScenesScreen with { jav: true } (route param ->
listScenes jav=true). The 60s minimum-duration default is disabled in the JAV tab
because javflix does not expose duration (NULL >= 60 would hide the whole section).
Verified on prod: 16 javflix scenes appear only in the JAV feed and are excluded
from the 2.29M main feed; playback resolves to voe/doodstream/emturbovid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First site of the JAV vertical (user request, separate section). javflix.cc is a
WordPress JAV aggregator, VPS-reachable and server-rendered. Scraper parses itemprop
metadata (title, thumbnail, uploadDate; no duration) and the JAV code (BKD-368 style)
as a tag. Playback: server buttons are <a class="myLink" href="<embed>"> (streamtape,
voe, doodstream, emturbovid) whose href sits in the raw HTML; the generic _embed_iframe
extractor already resolves those via its anchor-hoster pattern, so javflix registers
under sitetag "javflix" with a thin wrapper that drops the players.mp4 placeholder.
Added emturbovid to the anchor-hoster host list.
Verified end-to-end on prod: scrape a listing -> RawScene with metadata + code tag,
resolve -> emturbovid/voe/doodstream hoster sources (all Goon-playable).
NOT registered in ALL_BROWSE_SCRAPERS yet. JAV is a separate vertical (user decision)
that must be gated out of the main scenes feed before ingest so it does not flood the
western catalog. Next: feed gating (exclude JAV origins by default) + a mobile JAV tab
+ on-device playback test, then enable scheduled ingest. jav.guru / vjav / supjav
follow the same pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regression from the 2x-hold mute (commit e30f683). Gesture.LongPress fires
onFinalize for FAILED/CANCELLED gestures too (a plain tap, or a scroll stealing the
finger), not just after a real 2x hold. onFinalize unconditionally restored
player.muted from preSpeedMutedRef, whose initial value is true, so any ordinary
screen interaction muted the audio while the UI still showed the unmuted icon
(report fad4b317 "Audio cuts out on screen interactions").
Track whether 2x actually started (speedStartedRef, set in onStart) and only touch
player.muted in onEnd/onFinalize when it did. A failed or cancelled long-press now
leaves the user's mute preference alone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
extract_stream_from_hoster fell through to yt-dlp for DoodStream clones (playmogo,
doply, myvidplay, dood.*), which yt-dlp does not support -> "ERROR: Unsupported URL"
spam dominating the movie-resolve logs, plus wasted seconds per resolve before the
caller returned the embed as type='hoster' anyway. Now detects the dood-clone family
up front and returns None immediately (same pattern as the filemoon SPA handling), so
the embed goes straight to the mobile doodstream.ts resolver (pass_md5 with the user's
residential IP, which clears the invisible Turnstile). Verified: playmogo/myvidplay/
doply URLs now return None with no yt-dlp attempt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Scenes feed repeated the same videos on scroll (report 304c9258): OFFSET
pagination + continuous ingest means new scenes shift the offset, so later pages
re-include earlier rows. Backend now appends Scene.id as a unique final tiebreaker
to every sort (deterministic order), and the mobile Scenes list dedups by scene id
when concatenating pages so a drifted overlap can never show a duplicate.
- Player: holding to fast-forward at 2x now mutes the sped-up (chipmunk) audio for
the duration of the hold and restores the user's real mute preference on release
(report 35bbf428).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hqfap was hard-removed 2026-06-25 when its entire CDN library started serving a
fixed /upload/videos/video_down.mp4 "server down" stub for every scene. The site
is back and now serves real video from a new CDN (d*.vstor.top/whlvid/...). Probe
of the live sitemap: 5/6 sampled scenes return a real mp4 (0 stubs), and the CDN
is portable cross-IP (HTTP 206 from a residential IP and from the VPS with the
hqfap referer).
Restores the PlayTube CMS browse scraper (HQFapScraper, sitetag hqfapcom) and the
JSON-LD contentUrl extractor, re-registered in both registries. The extractor now
sets mobile_direct_ok=True (vstor.top is not matched by _TIME_BOUND_CDN_RE, but is
verified portable) so mobile plays direct with zero proxy. The video_down.mp4 stub
guard is kept defensively. 4k69 stays removed (not re-checked).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bug-report clusters about missing thumbnails:
1. watchporn scenes had no thumbnail on the list, only appearing after opening
SceneDetail (which auto-enriches og:image). Coverage was 1.3% (463/36,915).
The browse scraper never captured a thumbnail. KVS stores the poster at a fixed
derivable path (contents/videos_screenshots/<id//1000*1000>/<id>/preview.jpg,
verified 8/8 loading). Scraper now sets thumbnail_url (og:image, else derived);
backfilled 36,687 existing rows -> 100% coverage.
2. SceneDetail showed no thumb where the list showed one: the mobile detail picks
the first source with a thumbnail_url (origin ASC often puts sxyprncom first),
and sxyprn/trafficdeposit stored thumbs rot to 404. The list already swaps those
for a live resolver (/proxy/sxyprn-thumb/), but the detail builder did not. It
now applies the same live-resolver swap and nulls other rotting thumbs so the
detail lands on a working image.
Both backend-only, no OTA needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The studio line in MovieDetail is now tappable and opens a new StudioMovies
screen listing every movie from that studio (studio_slugs filter, which the
backend already supports). Mirrors StudioScenes for scenes. Works for any studio,
including long-tail ones that are not in the top-40 chips of the Movies filter
sheet (where studio filtering already existed but was easy to miss).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generic movie titles ("Monster Tits", "Pirates") map to many different TPDB
films with identical titles (different studios/casts, all title-score 1.0). The
old matcher searched per_page=10 and ranked by title only, so for a generic title
the correct film was often not even in the top 10, and among same-title
candidates it picked arbitrarily. Result: a ~95% no_match rate and silent
misattribution (e.g. "Monster Tits" by Venom Digital Media would get Galaxy
Productions' TPDB entry).
_best_match now:
- searches per_page=40 (the right film for a generic title is often past top 10),
- ranks title-gate survivors by a composite of title + studio similarity +
performer overlap (our studio/cast from the primary source disambiguate which
same-title film it is),
- guards against misattribution: if we have a studio/cast signal and there is
more than one near-identical-title candidate but the winner shares neither
studio nor cast, return no_match instead of attaching a wrong same-title film.
Verified on prod data: a no_match-with-studio sample now matches 18/18 with the
correct studio (fixing Galaxy to Venom, Exquisite to Rodney Moore, and a no_match
to Cherry Boxxx), and an already-enriched sample keeps 16/18 identical picks with
the 2 differences being the same studio (benign TPDB duplicate). No wrong-studio
regressions observed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Two user reports (watchporn scenes 5716e1c5 + another): "resolver fails but the scene
plays in the diagnostic browser". Verified server-side the watchporn extractor is reliable
(8/8) and returns 2 clean links; the zload remote_control.php CDN URL is a valid
range-supporting mp4 (206) that plays cross-IP and via the /proxy fallback (206). So the
failure is purely the mobile native player choking on that URL, and native-mp4-only tube
sources carried NO fallbackEmbedUrl, so there was nothing left after native+proxy.
Now: when a resolved source has direct links but no embed, use the tube page_url as the
WebView fallback. Native + proxy are still tried first; only if both fail does it load the
site page in the WebView player (phone-side, residential IP, no VPS bandwidth) which is
exactly what the diagnostic browser does and the users confirmed plays.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From user bug reports (8f8c10c0 freeomovie, c156e4b7/8db71220 watchporn scene):
freeomovie (app/connectors/freeomovie.py):
- Parse the detail-page poster (<img class="rmbd">); RawMovie was built with no
poster_url, so title-only orphans (no canonical match) rendered blank cards. Re-ingest
backfilled existing blanks 11 -> 1.
- _host_label now returns the registrable domain (parts[-2]) not the subdomain, so
video.player4me.xyz labels as 'player4me' (was garbage 'freeomovie:video').
- Skip player4me (JS-SPA, no known resolver); pruned 28 dead freeomovie:video sources.
- myvidplay.com added to mobile DOOD_HOSTS (doodstream.ts): it 301s to playmogo.com
(doodcdn clone), so it now routes to the native dood resolver instead of a dead WebView.
watchporn: the reported 4 duplicate/broken quality rows were the API process serving a
STALE extractor registry (never restarted after the re-enable deploy) -> _embed_iframe
scraped 2 tokenless get_file (403) + 2 preview trailers (404). The running api now serves
the native extractor (2 clean playing links, verified). Hardened _embed_iframe to skip
preview/videos_screenshots clips and tokenless get_file so this class can't recur.
Also: DonateScreen em-dashes -> commas/semicolon (no-em-dash rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
freeomovie.to (WordPress bestia theme, not dooplay) publishes fresh
full-length DVD titles daily. New standalone BaseMovieConnector scoped to
/category/full-movie/ (homepage mixes scene-clips that would orphan as
movies). Parses the listing thumi items for clean titles, and the detail
var TABS array for playback hosters (voe/luluvid/vidhide resolve VPS-side;
myvidplay is a DoodStream clone, phone-side), skipping dead streamtape/
mxdrop. Genre tags come from articleSection (the part before "XXX Movies");
release_year is deliberately NOT set (datePublished is the post/upload date,
not the film's production year, which would poison year-scoring on canonical
match) and performers/studio are skipped (articleSection has no clean
delimiter -> junk-performer risk) -- the movie rides in as a title-trigram
mirror and TPDB enrichment supplies cast/studio/tags authoritatively.
Verified pilot: 10 movies -> 9 attached to existing canonical movies
(mirror playback), 1 new, 0 errors.
Follow-up (mobile, not required for playability since voe/luluvid cover it):
add 'myvidplay.com' to DOOD_HOSTS in doodstream.ts + OTA for a 4th hoster.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
watchporn.to was disabled 2026-05-12 (all iframes were DoodStream/CAPTCHA
-> black-screen WebView). The site has since been rebuilt as a KVS tube:
the DoodStream hosters are gone, scenes now expose flashvars get_file
direct mp4. Rewrote the dead ?s= search scraper as a /latest-updates/
browse scraper (og:title, JSON-LD duration/uploadDate, /models/ performers,
/tags/ tags, /categories/ studio) and added a KVS get_file extractor
(porntrex-style same-session 302 resolve; token not IP-bound, plays from
VPS). Moved to ALL_BROWSE_SCRAPERS, extractor swapped from _embed_iframe.
Verified: 35 fresh scenes/page (today), multi-performer, playback 1080p mp4 206.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
streamporn.nl went stale (0 new since 2026-05-23); streamporn.vip is the
live sibling on the same PsyPlay theme with a fresher catalog. Same
DooplayConnector works out of the box (verified: full title/studio/
performers/tags/poster + 11 playback sources per movie). New source name
'streampornvip' so origins don't mix with the dead .nl rows. Swapped into
get_movie_connectors in place of the dead .nl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enrich existing movies (from paradisehill/dooplay, which mostly lack cast)
with metadata from TPDB's /movies API: cast, categories (tags), studio,
director + a canonical TPDB UUID for dedup. Chosen over IAFD after a
source-comparison research pass — IAFD has strong cast/studio but ZERO
categories, while TPDB /movies has ~11 tags/movie, cast, studio, director,
a canonical UUID (+ sparse phash), is already an integrated API (no
scraping/anti-bot), and covers ~75-85% of our western-DVD-feature catalog.
Enrichment only ever augments EXISTING movies and never creates new ones
(TPDB has no playback, so a standalone TPDB movie would be unplayable).
Writes to movie_performers / movie_tags / movie.studio_id, which the movies
API + mobile detail already render, so no schema/API/UI change is needed.
- connectors/tpdb.py: search_movies() + fetch_movie() + _parse_movie()
reusing the existing _parse_studio/_parse_performer/_parse_tag.
- enrich/tpdb_movies.py: match our movie to a TPDB /movies result by
token_sort_ratio on normalized titles (sort, not set, to reject the
short-title-subset trap "Fantasies" -> "Tara's Fetish Fantasies") with a
+/-2yr guard; then attach cast/tags/studio/director. Incoming performers
deduped by external_id to avoid the performer_external_refs PK clash.
- resolve/movie_merge.py: merge_movies() mirror of scene_merge; two of our
movies mapping to the same TPDB UUID are the same film -> merge.
- scheduler: _job_tpdb_movie_enrich every 6h, batch 200, prioritizing
playable movies missing cast/studio.
Verified on a 150-movie batch: 119 enriched, 4 deduped, 26 no-match,
0 errors; matched titles/studios spot-checked correct (Big Butts Drive Me
Nuts 4 -> 33 tags, Seinfeld #2 -> 10 cast/17 tags, German BB Video titles
-> categories+studio).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up cleanup from the ultra-review (behaviour-preserving, verified equivalent):
- scenes.py: extract live_playback_exists / blacklist_clauses / stub_exclusion_clause;
list_scenes and favorites now share ONE definition of "visible scene" (was a hand-kept
copy in favorites -> the count-vs-list drift class). Verified identical: helper vs old
inline both count 1,853,327 scenes.
- favorites.py: replace the two copy-pasted count blocks with one _new_counts(kind=...)
that counts in SQL (count(*) FILTER over the windowed subquery) instead of streaming up
to N*200 rows to Python; joins the favorite table for per-row last_seen. Deployed
_new_counts verified == hand SQL (studios 364). Dropped now-unused imports.
- mobile: extract lib/newScenes.ts (isNewScene / sortNewFirst); SceneTile + Performer/
StudioScenes use it (was triplicated, already drifted once in the deleted FavoriteSceneRow).
- SceneDetail: fold tube:pornxpph into the phoneResolver map (was a verbatim-duplicated block).
- deep_crawl: persist the cursor after every completed page (was once at run end), so a
mid-page hard-kill past the soft budget can't lose progress (GOON-V hardening).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Tap the ▶ overlay on a tile to play the best source without opening SceneDetail first
(user request: fewer steps). It routes to SceneDetail with an autoplay flag that fires the
top source's existing resolve on mount — reusing all the phone-side/backend resolve,
quality-picker and fallback handling in place rather than duplicating it. With a default
quality set (Settings → Playback) this is one tap to video; otherwise the quality chooser
still appears. ▶ is hidden in duplicate-select mode; tap elsewhere on the tile = details.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Settings -> Playback lets the user pick a default (Ask / 4K / 1080p / 720p / 480p /
Lowest). When a resolved source has multiple qualities and one matches (largest height
<= target, else closest; 'lowest' picks the smallest), the app plays it directly instead
of popping the chooser. 'Ask' keeps the old behavior. pickByDefaultQuality lives next to
qualityToInt; wired into the three SceneDetail resolve sites. Movie parts are untouched
(separate partsPicker, never auto-collapsed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eporner/sxyprn/fpoxxx resolve phone-side; the native player errors on initial load, then
re-resolve swaps in a fresh URL ~1-3s later and plays fine — but the terminal error
overlay with "Mark broken" rendered during that gap (report dafa8cdb). Added a
recoveryPending flag (mirrors the telemetry's fallback-aware logic: re-resolve / in-place
seek recovery / proxy / webview still in flight) that shows a "Reconnecting…" spinner
instead, and only surfaces the terminal error + Mark broken when nothing is left to try.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lists sort by created_at desc, so scrapers add new scenes at the top; on refetch those
prepend above the viewport and the whole list slid down under the user's thumb (jitter/
tearing while browsing filtered results). Added maintainVisibleContentPosition to the
shared sceneGridProps() so RN pins the visible item and corrects the offset; a small
autoscrollToTopThreshold still surfaces new items when the user is at the very top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "+N new" badge counted every live-playback scene with created_at > last_seen_at,
but the performer/studio list the user opens applies the device blacklist AND only shows
the first page (per_page=200, sort release_date desc) — so a freshly-ingested scene with
an old release date, or a blacklisted one, inflated "+N" while being invisible ("+6, no
new scenes"). Count now mirrors the list: same visibility filters (live playback +
blacklist + stub-exclusion for studios) and the same top-200 window under release_date
desc. On the main device this dropped the naive total 1663 -> 1406 (phantom off-page
scenes), and blacklisted content no longer inflates counts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Player (report dccc05e4): swipe-seek no longer force-opens full controls (it has its own
±time bubble), and the center play/pause button is hidden while actively seeking (pan or
scrub-bar drag) — kills the big pause that popped up mid-seek and lingered ~3.5s.
Diagnostics "open in browser" now routes to an in-app WebView with incognito=true instead
of Linking.openURL: fresh sessionless view of the host page + no NSFW URL dumped into the
user's real browser history (fits the app's privacy stance). New DiagnosticBrowser route.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The NEW badge keys off scene.created_at. merge_scenes kept the survivor's created_at,
but the dedup caller may pick the freshly re-ingested mirror as keep_id — so deduplicated
old content got a recent created_at and showed up as NEW (report f17799b3). Coalesce to
min(keep, drop) so a merged scene keeps its first-seen date.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run_deep_crawl picks one tube/run and crawls 60 pages under _job_deep_crawl's 3600s
hard timeout. A detail-fetch scraper on a slow patch (per-scene page fetch, e.g. via
proxy) could exceed it → the run is killed mid-page, the cursor is never saved (orphan
thread), and that tube makes zero progress — recurring Sentry GOON-V. Added a 3000s
in-run budget that breaks after a completed page, saves the cursor, and returns cleanly;
the next run continues. budget_hit surfaced in the summary log to spot the slow tube.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
freshporno (5★ fresh+rich, verified working — 206/507MB) was labeled OFFLINE off 10
playback attempts that all failed in one unlucky window (a CDN-node blip; it resolves
fine now). 10 was too thin a sample to zero out a known-good source's stars. Raised
the telemetry-trust threshold 10→25; below it we fall back to the proxy/heuristic
health instead of declaring offline (user-report cb526949).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitHub goon-foss/goon stays soft-banned (whole namespace 404, 10 days after the
appeal, no response — flag abuse on the resolve-stream language, not DMCA). Migrated
the repo to a self-hosted Forgejo on our own VPS: zero ban risk, full control, and it
fits the project's self-hosted ethos. Settings → Source code now opens
git.goon-foss.org/goon-foss/goon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single request with 194 studio_slugs + 23 tag filters (each tag = a correlated
EXISTS) plus an ILIKE search built a query heavy enough that the OOM killer killed the
Postgres backend, triggering a full crash-recovery (~1s prod-wide outage, all in-flight
connections dropped). Any user could do this with a big enough filter. Cap studios to
50, tags to 15, performers to 15 (far above any real UI usage) and return 422 instead
of executing — bounding query complexity regardless of the planner's choice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sxyprn's video token is bound to the IP that fetched the post page; on mobile the
phone resolver works ~74% but ~26% fail when the egress IP shifts (CGNAT / network
switch) or the token goes stale → native player hung on a dead URL (18 reports, 26%
error rate in telemetry). Now on an initial-load error for these phone-resolved
tubes, the player re-fetches the page fresh (new token bound to the current IP) and
swaps the source before falling through to the proxy/WebView chain. Zero VPS
bandwidth. Gated by resolvePageUrl so other tubes are completely unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-check 2026-06-25 across the full id range confirmed both PlayTube tubes
serve only the fixed `/upload/videos/video_down.mp4` "server down" stub, never
a real file: hqfap 0/80 real (79 stub, 1 none), 4k69 0/40 real (38 stub, 2
none). Both were disabled 2026-06-22; CDN never came back, so removing entirely
(mirrors the pornhub/redtube/0dayxx/pornditt/pornhat removals).
Removed the extractor registry entries (hqfapcom, 4k69com) + module files and
the browse scrapers + imports. Prod DB data deleted separately (28,398
solo-orphan scenes + 46,196 playback_sources). `_playtube.py` kept: superporn
and neporn still use its JSON-LD helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_embed_iframe returned xtremestream's player endpoint (player/xs1.php?data=) labeled
type=mp4, but it's an IP-bound JS player page (403 cross-IP), not a real file — the
native player loaded it forever ("perverzija nie działa" / "loading w nieskończoność").
Added xtremestream.* to _IP_BOUND_CDN_RE so Stage 1 skips it and falls through to the
hoster fallback: the phone WebView loads the index.php player with its residential IP
and the stream plays in-session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Browse over /best/<YYYY-MM>/<page> (SSR; xnxx has no clean /new/ and its homepage is
JS-rendered) for a latest-feed freshness signal next to the performer-driven search
scraper. JSON-LD VideoObject only — xnxx detail (unlike its xvideos twin) doesn't
expose /models/ or /tags/ in SSR, so performers/tags come via canonical merge + the
search scraper. Title is html.unescaped (JSON-LD ships ,/! entities).
xhamster and sxyprn intentionally left search-only: xhamster Cloudflare-blocks the
VPS on listing pages (1KB challenge), sxyprn has no clean SSR listing (IP-bound) —
a flaky browse scraper would be worse than the working search + 168h watchdog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Browse over /browse/time/?page=<n> (SSR) for guaranteed latest-feed freshness next to
the existing performer-driven search scraper. JSON-LD VideoObject only (title /
duration / uploadDate / thumbnail) — deliberately NOT scraping performers/tags from
the detail page: JSON-LD has no actor field and the /pornstar//category links are
sidebar-polluted with no scene-scoped container, so a naive regex attached the same
2 pornstars to every scene. Performers/tags come via canonical merge + the search
scraper instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both were search-only — fresh only as long as the performer queue cycles and the
site search keeps working. Added browse scrapers next to the existing search ones
(xvideos/eporner pattern: search keeps performer back-catalog coverage, browse
guarantees latest-feed freshness → watchdog 48h instead of 168h):
- porntrex: KVS /latest-updates/<n>/ (title + thumb + phash)
- mypornerleak: WP REST /wp-json/wp/v2/posts?_embed=1 (title + date + studio from
category + performers from the actors taxonomy)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Watchdog flagged porndish as frozen (search ?s= stopped yielding new scenes
2026-05-07, 1151h). It's WordPress and the VPS can reach it, so converted to a browse
scraper over the WP REST API (/wp-json/wp/v2/posts?_embed=1), same pattern as
perverzija: title, date, featured thumbnail, studio (category — FreeUseFantasy /
I Have A Wife / … paysite content) and tags. Performers via canonical merge. Playback
unchanged (embed iframe → phone-side). 60 fresh scenes on first crawl.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Search (?s=) started returning 429 and the homepage is JS-rendered (no post links in
raw HTML), so the old search scraper got 0 (frozen since 2026-05-07). perverzija is
WordPress and the VPS can reach it (200, not CF-blocked), so converted to a browse
scraper over the WP REST API (/wp-json/wp/v2/posts?_embed=1): one structured call per
page gives title, date, featured thumbnail, studio (category — DadCrush/FamilyStrokes/
… TeamSkeet-family paysite re-ups) and genre tags. Performers via canonical merge
(stars taxonomy isn't REST-exposed; title carries names). Playback unchanged (embed
iframe → phone-side). 15 fresh + 45 refreshed on first crawl.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three orphan-factory tubes (0–0.2% canonical match — auto-screenshot thumbs and
slug titles that never match TPDB/StashDB) — to be replaced by better sources.
Removed scrapers (files + imports), extractors (registry + modules), the pornhat
entry from tag-enrichment priority lists and the 0dayxx display override, and purged
the DB (19,003 playback_sources + 9,904 solo-orphan scenes; shared mirror scenes keep
their other sources). The pornhat-based enrich_studio endpoint stays as a graceful
no-op (no pornhat sources → returns no studio).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fpo.xxx is a KVS site, not WordPress, so the old `?s=` search scraper matched
nothing (frozen since 2026-05-07). Converted to a browse scraper reading /new-<n>/
(title + duration + thumbnail + phash from the listing tile; performers via canonical
merge). Playback was already phone-side (KVS). 32 fresh scenes on first crawl.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both scrapers were disabled since 2026-05-12 (~0.4% canonical match — mostly short
amateur clips that never match studio content); their data sat frozen. Removed for
good: deleted the extractor registry entries, scraper files and imports, dropped them
from the tag-enrichment priority lists, and purged the DB (17,906 playback_sources +
122 scenes that had no other source; mirror scenes shared with other tubes just lost
the ph/rt link).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scrollTo/onScroll used the full screen width, but the ScrollView viewport is narrower
(card margins + padding), so the computed index desynced from the visible slide — the
last slide kept showing "Next"/"Skip" instead of "Start browsing". Measure the real
viewport width via onLayout and use it for paging, scrollTo and index. Caught on the
emulator (uiautomator dump — FLAG_SECURE blocks screenshots).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>