Commit graph

90 commits

Author SHA1 Message Date
goon-foss
21500837ab feat(bugreport): auto-attach device + last-playback context
Some checks failed
Backend tests / test (push) Has been cancelled
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>
2026-07-17 21:53:30 +03:00
goon-foss
b62d22370b feat(player): copy video link button
Some checks failed
Backend tests / test (push) Has been cancelled
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>
2026-07-14 22:48:16 +03:00
goon-foss
2cfb0bc12d feat(sync): device-to-device backup of favorites & state
Some checks are pending
Backend tests / test (push) Waiting to run
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>
2026-07-14 22:41:35 +03:00
goon-foss
92adaccf1b fix(hqfap): resolve on the device (site blocks the server IP)
Some checks are pending
Backend tests / test (push) Waiting to run
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>
2026-07-14 22:20:43 +03:00
goon-foss
7e2807b42e feat(supjav): resolve voe hoster to direct stream instead of WebView
Some checks failed
Backend tests / test (push) Has been cancelled
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>
2026-07-11 19:52:26 +02:00
goon-foss
4df1e01b31 feat(jav): separate JAV section (tab + feed gating) + enable javflix ingest
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>
2026-07-10 11:16:31 +02:00
goon-foss
2d3831bd53 fix(player): audio no longer cuts out on tap/scroll (2x-mute regression)
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>
2026-07-10 09:38:03 +02:00
goon-foss
e30f6830f4 fix(mobile): stop Scenes feed repeating on scroll + mute audio during 2x hold
- 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>
2026-07-07 23:59:28 +02:00
goon-foss
8ea8b94b4a feat(movies): tap studio in MovieDetail to see that studio's full filmography
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>
2026-07-05 12:14:13 +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
3eb57dd61d fix(mobile): WebView page fallback for native-mp4 tubes that fail in ExoPlayer
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>
2026-07-05 00:13:40 +02:00
goon-foss
0c18472a15 fix(feedback): freeomovie posters + dead hoster + myvidplay; watchporn _embed_iframe guard
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>
2026-07-03 13:02:47 +02:00
goon-foss
cb0b843f48 refactor(review): dedup favorites/list visibility + SQL-aggregate count, share mobile isNew
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>
2026-07-02 09:41:37 +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
17d82d465a feat(mobile): quick-play ▶ on scene thumbnails
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>
2026-07-01 10:45:50 +02:00
goon-foss
129c71cf95 feat(mobile): default playback quality — skip the quality chooser
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>
2026-07-01 10:37:20 +02:00
goon-foss
7cf338f812 fix(mobile): no false "Mark broken" while a video is still recovering
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>
2026-07-01 10:37:19 +02:00
goon-foss
c029b7c687 fix(mobile): anchor scene lists so prepended scenes don't jump the scroll
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>
2026-07-01 10:37:19 +02:00
goon-foss
c073acff03 fix(mobile): no center pause button while seeking + incognito diagnostic browser
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>
2026-06-29 11:21:41 +02:00
goon-foss
ac17350a67 fix(sources): require 25 telemetry attempts before a source can show OFFLINE
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>
2026-06-29 10:23:29 +02:00
goon-foss
8d03662fa0 chore(mobile): point Source-code link to self-hosted Forgejo (git.goon-foss.org)
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>
2026-06-26 16:36:14 +02:00
goon-foss
2a0e46e17d fix(mobile): re-resolve IP-bound tubes on playback error (sxyprn/eporner/fpoxxx)
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>
2026-06-25 11:11:21 +02:00
goon-foss
ce85d7d92e chore(ingest): hard-remove hqfap + 4k69 (entire CDN library gone)
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>
2026-06-25 11:07:47 +02:00
goon-foss
0ce4675574 fix(mobile): onboarding pager — measure page width so last slide shows "Start browsing"
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>
2026-06-22 10:21:19 +02:00
goon-foss
e1dd46c6f2 feat(mobile): first-launch tutorial (pages, features, long-presses, player gestures)
A 7-slide carousel shown once on first launch:
- the three tabs (Scenes/Movies/Sites)
- search, filters, saved searches, Performers/Tags/Favorites
- long-press actions (hide/duplicate a scene, remove a wrong performer, link diagnostics)
- player gestures (tap controls, double-tap ±15s, swipe to scrub, unmute)
- favorites, Hidden content, PIN lock, the ? report button, Sites ★ ratings

Gated by a SecureStore flag; replayable from Settings ⚙ → Replay tutorial (via a
tiny onboarding bus). Suppresses the What's-new popup for brand-new users (the tour
covers it) and marks the changelog seen on finish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:08:56 +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
8c8a3d5823 feat(mobile): movies — performer filter + 3-column grid
Two Movies-list reports. (1) 1044cd34 'do movies have a metadata base for performers/categories/studio/year': yes — 90% have year, 92% studio, 81% performers, 93% tags, and the filter already covered studio/genre/year. Added the missing dimension: a performer search-and-select in MovieFiltersSheet (backend listMovies + api.ts already accepted performer_ids; only the UI was missing). (2) 0200956f 'use the space better': Movies grid goes 2 -> 3 columns (poster card is flex:1, scales fine) so ~50% more films per screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 23:21:22 +02:00
goon-foss
e185b09e09 fix(mobile): double-tap seek no longer pops the center pause control
Report dc4e91fb: double-tapping to skip ±15s also called setControlsVisible(true), throwing the full controls (big center pause button) on screen for 3.5s. Seek already has its own ±15s hint overlay, so the controls pop was redundant — removed it. Single-tap still toggles controls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 22:27:08 +02:00
goon-foss
6d1d9dbd46 feat(mobile): strip .com/.org clutter from site names
User-report 18105d14: drop the TLD suffix from Sites list + SiteScenes header (hqporner.com -> hqporner, fpo.xxx -> fpo). Logos skipped (needs a per-site logo source) — TLD strip is the quick win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:38:45 +02:00
goon-foss
acbc543e2f feat(mobile): Hidden content screen — blacklist tags/performers/studios
User-report 86a9ec72 ('remove all gay scenes from randomly popping up'): there was no UI to hide a tag, nor to view/undo the blacklist — even though the 'Hide performer' alert promised 'undo from Settings -> Blacklist' (a screen that never existed). New BlacklistScreen: search-and-add any tag to hide (e.g. a category), plus manage/unhide all blacklisted tags/performers/studios. Reached via Settings -> Content -> Hidden content. Backend already drops blacklisted-entity scenes from every /scenes (device-scoped); this just exposes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:53:09 +02:00
goon-foss
a136159e64 fix(mobile): stop full scene-list refetch on back-navigation (perf)
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>
2026-06-20 14:14:07 +02:00
goon-foss
cf1575f51d fix(mobile): drop background ANR noise from Sentry (beforeSend)
Android Background ANRs captured via AppExitInfo (GOON-1D) are OS-side noise: the OS freezes a backgrounded app and reports it as not-responding, with zero JS/app frames and nothing to fix. beforeSend now drops events that are ANRs (ApplicationNotResponding) AND backgrounded (contexts.app.in_foreground === false). Foreground ANRs are kept (those can be real jank).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:07:32 +02:00
goon-foss
f3802ca5c6 fix(mobile): scene-list scroll perf + native phone-side fpoxxx resolver
(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>
2026-06-19 11:02:21 +02:00
goon-foss
66cd2e2cc4 feat(mobile): column toggle, duration filter, saved searches, screen protection (mobilism feedback)
Batch from user feedback: (1) Grid columns 1/2/3 setting (PreferencesContext, persisted) across all scene grids — default 2 was too small on phones. (2) Min-duration filter chips (5/10/20/30+ min) to hide ad-clips. (3) Saved-search chips + Save button (backed by /saved-searches). (4) Re-enabled screen-capture protection (Recents hide + screenshot block) for distributed users — verified active on emulator (screencap returns 0 bytes). (5) 'Checking for updates' gate before the PIN screen so a background OTA restart no longer causes a double PIN prompt. Changelog entry added. Published OTA runtime 1.1 (a9620b12).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:52:27 +02:00
goon-foss
b430811b93 fix(mobile): capture site/origin text params in bug-report auto-context
SiteScenes passes the tube as origin/name (strings), not UUIDs, so the existing UUID-only auto-context loop dropped them. Reports like 'ingest of this site has been stuck 2 days' (14f3a655) arrived without any site identifier. Add a second loop for known string identity params (origin/name/sitetag/tag/q), length-capped, so per-site/per-performer reports become actionable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 09:35:58 +02:00
goon-foss
0fae99f67c fix(mobile): show Refresh thumbnail when the hero image actually fails to load
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>
2026-06-14 14:18:44 +02:00
goon-foss
1c021daf33 feat(mobile): auto-accept age-gate modal in WebView fallback
4k69 (and similar) show an "Are you 18 or above? Yes/No" modal that blocks the jwplayer
from initialising, so the WebView fallback never extracts a stream. Click the age-gate
accept button by id (#pop_up_18_yes and id*=18_yes/age_yes variants) on the same loop as
the consent/play-poster auto-clickers. Verified on emulator: 4k69 age-gate clears and the
player initialises (ExoPlayer hands off). A VAST preroll is still grabbed instead of the
okcdn content for 4k69 specifically (report 5de3fbc5 stays open) - separate ad-filter work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:31:12 +02:00
goon-foss
5d359cd319 fix(mobile): show Refresh thumbnail only when preview missing or broken
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>
2026-06-14 11:17:18 +02:00
goon-foss
5bac6d9c16 feat(mobile): source-code link in Settings + Refresh thumbnail button
- 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>
2026-06-13 19:04:11 +02:00
goon-foss
dbeef3deb1 fix(mobile): remount Favorites lists on numColumns change (GOON-11)
The new Scenes tab uses a 2-column FlatList while Performers/Studios/Movies are
1-column. Switching tabs reused the same FlatList instance, so numColumns changed on
the fly and RN threw "Changing numColumns on the fly is not supported" (5 users).
Give the Scenes list a distinct key ("fav-scenes") from the shared single-column key
("fav-list") so React remounts a fresh FlatList across the 1<->2 boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:19:19 +02:00
goon-foss
c3ed0245e4 feat(mobile): "What's new" popup after OTA updates
After an OTA bundle is applied, show a one-time popup listing recent changes. The
changelog ships in the bundle (mobile/src/changelog.ts), so it is always in sync with
the code that just arrived. WhatsNewModal compares the newest entry id against the last
one seen (SecureStore); shows unseen entries, marks seen on dismiss, and stays quiet
until the next update adds an entry. First run shows only the newest entry (no history
dump). Mounted over the navigator when signed in.

Each OTA publish should prepend a new entry at the top of CHANGELOG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:41:54 +02:00
goon-foss
f1c1e60007 feat(mobile): "Your messages" inbox on bug FAB + geo-block playback hint
Bug FAB now has two tabs: "Report a bug" (existing) and "Your messages", which lists
this device's reports with any admin reply in a highlighted box. A badge dot on the FAB
shows unread replies; opening the tab marks them seen. Polls every 90s and on open.

PlayerScreen: when the WebView fallback (residential IP) cannot extract a stream within
25s and there is no 404/410, show a one-time hint that the source may be blocked in the
user's region or by their ISP (try another source or a VPN) - so a geo/network block on
the user's side does not read as a broken app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:35:44 +02:00
goon-foss
05f7cee92d feat(mobile): Favorites "Scenes" tab to view saved scenes
Users could heart individual scenes from SceneDetail, but the Favorites screen only
had Performers/Studios/Movies tabs, so saved scenes were invisible (bug report: got a
bunch of scenes saved but no way to see them). Add a Scenes tab (now the default)
listing favorited scenes as tiles via GET /scene-favorites, long-press to remove.
Adds client.listSceneFavorites().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:06:12 +02:00
goon-foss
1b8ff0197b feat(mobile): phone-side resolvers for IP-bound tubes (sxyprn, eporner, voe)
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>
2026-06-11 16:14:25 +02:00
goon-foss
864376c58b fix(mobile): English long-press action labels + clean thumb error placeholder
bug-report c25e9b55: long-press scene actions were in Polish — translate menu,
banner and confirm dialogs to English. Thumb 'error' state (e.g. expired sxyprn
thumbnail 404) now shows the same 🎬 placeholder as 'empty' instead of a ⚠ broken
glyph (bug 2026-06-10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:11:10 +02:00
goon-foss
8306b8e997 feat(mobile): send X-Device-Id, one-time adopt-legacy
GoonClient attaches a stable per-install device id (SecureStore, lazy UUID) on
every request so server-side user state is scoped per device. On first launch
after update, call /me/adopt-legacy once (SecureStore flag) to claim the previous
shared state onto this device — the instance owner should relaunch first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:58:02 +02:00
goon-foss
06c6dfbf24 feat(mobile): tile long-press actions (hide / mark-duplicate), drop dead preview
bug-report 5a6844db: the hold-to-preview animated gesture did nothing useful.
Replace it with a long-press action menu on scene tiles:
  - Ukryj scenę → POST /scenes/{id}/hide
  - Oznacz jako duplikat → enter selection mode; tapping another tile merges the
    long-pressed scene INTO the tapped one (POST /scenes/{keep}/merge/{drop}).
SceneActionsProvider holds the selection state + a bottom banner, so it works across
all 5 scene-list screens via the shared SceneTile (no per-screen wiring). Selecting
mode highlights tappable tiles and badges the pending duplicate. Animated thumbnails
kept only as a still-fallback image; has_animated_thumbnail filter removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:52:15 +02:00
goon-foss
7222f3dcf9 fix(performer): tag chips → in-place horizontal filter selector
Follow-up to 1a4bf258 feedback (a627637b + 0264a3ff): the flexWrap chip list ate
too much vertical space and tapping navigated away to TagScenes. Rework: single-row
horizontal scroll of toggle-chips that filter the performer's scenes IN-PLACE
(performer_ids + tags in one listScenes query, no navigation). Selected chip is
highlighted with a ✕ affordance; tap again clears. One line tall instead of N rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:25:02 +02:00
goon-foss
99e9dae4d1 feat(performer): replace dev Re-scrape button with top-tag chips
bug-report 1a4bf258: "Re-scrape mógłby zniknąć, za to tagi/kategorie by mogły".
Re-scrape was a dev-only bulk thumbnail/tag enrich — noise on the performer page
(per-scene enrich already happens on SceneDetail). Removed it; kept Search.

New GET /performers/{id}/tags aggregates scene_tags across the performer's
live-playback scenes (top N). PerformerScenes renders them as chips → tap navigates
to TagScenes. Search button widened to full row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:56:26 +02:00