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>
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>
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>
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>
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>
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>
Returning to the Scenes list from a scene caused a full reload + phone load spike (report 5df48551). Cause: invalidateQueries(['scenes']) in SceneDetail/Player/Performer/Studio handlers — including the silent auto-enrich-thumbnail that fires on opening any thumbnail-less scene — forces react-query to refetch EVERY loaded page of the infinite list. Added refetchType:'none' to all ['scenes'] invalidations: marks stale without refetching the active list, which refreshes on pull-to-refresh / filter change instead. Scene detail (['scene', id]) still updates immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(1) Scroll jank/device load on long scene lists (report 5b7ca1e1): SceneTile is now React.memo'd so typing in search no longer re-renders every mounted tile, and sceneGridProps bounds the render window (windowSize 7 etc.) — required because removeClippedSubviews stays false to avoid thumbnail blanking. Applies to all scene grids. (2) fpoxxx played an ad instead of the video via the WebView fallback (reports f79beefb/cfa207c7). fpoxxx is KVS with an IP-bound + session-bound get_file token (cross-IP 403 confirmed), so it must resolve phone-side: new fpoxxxResolver fetches the page + follows get_file on the device (KVS real_url port for the function/0 case), wired into SceneDetailScreen like sxyprn/eporner. Verified from a residential IP: get_file -> CDN returns 206 video/mp4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The button keyed on thumbnail_url presence, but a URL can be present yet broken (hqfap
404 → blank hero, no button — report ef0c6a5a). Tie it to the hero Image load state
(onLoad ok / onError broken / no url none) and show Refresh only when the image is
broken or missing. Reconciles 26c114ed (hidden for good previews) with ef0c6a5a.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Refresh thumbnail button appeared on every scene, which is noise for the majority
that already have a good preview (report 26c114ed). Show it only when no source has a
usable thumbnail or the only thumbnails are rotting (sxyprn/trafficdeposit), which is
exactly when a manual refresh helps (the original d3376a71 case).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- AppLockSettings: a "Source code" row linking the public OSS repo (report 4c5066b8) -
a trust signal for a sideloaded FOSS app (audit / self-host / contribute).
- SceneDetail: a "Refresh thumbnail" button (force) for scenes whose preview is broken
or stale (report d3376a71).
- changelog: new What's New entry for this batch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These CDNs bind their signed video URL to the IP that fetched the page, so a
server-side resolve hands the phone a URL bound to the server IP -- the device then
gets a placeholder/403 and falls back through the proxy, streaming the whole video
through the server. Resolve on the device instead (token binds to the phone IP) so
playback goes direct with zero proxy bandwidth.
Ports of the existing backend extractors:
- sxyprnResolver.ts: data-vnfo + boo/ssut51 transform
- epornerResolver.ts: vid+hash -> /xhr/video mp4 sources
- voeResolver.ts: mirror redirect + 7-step payload decoder
Wired into SceneDetailScreen.onPress (sxyprn/eporner) and MovieDetailScreen.playVoe (voe).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pornxp.ph serves direct <source> mp4 (360/720/1080p) on st.pornxp.sh whose path
token is IP-bound to whoever fetched the PAGE (verified 2026-06-07: VPS-resolved
URL → 403 cross-IP). Backend resolve was therefore impossible, so pornxpph fell
to the WebView fallback which black-screened (bug-report fd06cd86).
Fix: resolve on-device (same pattern as getfileResolver/doodstream) — the phone
fetches the page, so tokens bind to the phone IP and play natively. New
pornxpResolver.ts extracts the <source> mp4s into multi-quality StreamLinks;
SceneDetail short-circuits tube:pornxpph to it before backend resolve, feeding
the existing quality-picker + native player.
Verified on emulator (live OTA): pornxpph scene → quality picker (1080/720/360)
→ native playback PLAYING (no WebView, no ads, no black screen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Device logs (not assumptions) pinned the real cause of the hdporngg/fullmovies
flicker: the backend returns a get_file URL, but get_file is bound to the IP that
loaded the *page*. The backend (VPS) loads the page, so the get_file is VPS-bound;
the phone fetching that get_file gets HTTP 410 -> ExoPlayer errors -> falls back to
the proxy via nav.replace (the "flicker"), and ends up streaming through the proxy.
(My earlier "stateless/portable" test was from the VPS — same IP as the page load —
so it wrongly showed 206.)
Fix: when the direct_url is a get_file, the phone re-fetches the *page* itself
(resolveGetFilePage on source.page_url) so the get_file is bound to the phone IP,
picks the requested quality skipping 4K (dead on fpvcdn), follows to the CDN, and
hands ExoPlayer a working URL. On failure it keeps the original (proxy fallback).
Verified on device: [getfile] page-resolve -> get_file 206 -> ExoPlayer PLAYING,
position advancing, no error/proxy/flicker, real video frame rendered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hdporn.gg/fullmovies.xxx return an unresolved get_file direct_url that 302-redirects
to fpvcdn.com with the requester IP baked in. The backend can't resolve it (would
bind fpvcdn to the VPS IP -> mobile 403), so the phone must follow the redirect. But
ExoPlayer errors on that cross-domain get_file->fpvcdn redirect (drops Referer / won't
complete it) -> the native player falls back to the proxy via nav.replace, which the
user sees as a screen-reload "flicker" before playback (and means it's actually playing
through the VPS proxy, not direct).
Fix: resolve the get_file 302 in JS on the phone (so fpvcdn binds to the phone IP)
before navigating to the player, and hand ExoPlayer the final fpvcdn URL directly —
no redirect, no error, no flicker, no proxy. Uses the same redirect:'manual' +
Location-header pattern as the doodstream resolver (works on RN Android). On resolve
failure it keeps the original get_file URL (current behaviour with proxy fallback).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a host returns 404/410 at playback time (CDN gone, video removed) the
player previously showed only a raw error and a Back button — the user could
not tell it was a dead source or report it without going back to the detail
screen (bug a78cc3b6: "fpo i sxyprn to 404, którego apka nie potrafi
zidentyfikować").
- Thread playback_source.id into Player route params (scenes + movies).
- Native player error overlay: detect 404/410 in the ExoPlayer error, show
"Source no longer available" and a "Mark broken" button that marks the
source dead and returns. 403 is excluded (proxy/WebView fallback may save it).
- WebView player: add onHttpError; on a main-document 404/410 show the same
overlay (Mark broken / Try anyway / Back) instead of the host's 404 page.
Guarded to the loaded document (host+path) so same-host ad/subresource 404s
don't false-trigger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tłumaczenie wszystkich user-facing stringów PL→EN (bug-report 2026-05-31
"dalej wszystko po polsku"). Alerty, przyciski, placeholdery, labelki w 12
ekranach/komponentach: BugReportFAB, AppLock(Screen/Settings/PinEntry),
applock biometric prompts, doodstream error msgs, MovieDetail, PlaybackQuality,
Player, SceneDetail, ScenesFilter, SiteScenes. Komentarze w kodzie zostają PL.
Zmiany były WIP drugiego okna (uncommitted); wjechały do bundla 0.2.1 przy
buildzie (były w working tree) — apka zainstalowana już ma EN. Ten commit
utrwala je w gicie żeby nie zginęły. Czysto stringi, zero zmian logiki.
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.