Compare commits

..

4 commits

Author SHA1 Message Date
jtrzupek
2f68b118c6 fix(mobile): no center pause button while seeking + incognito diagnostic browser
Some checks failed
Backend tests / test (push) Has been cancelled
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
jtrzupek
a5f355841a fix(merge): keep earliest created_at when merging scenes (no false NEW)
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>
2026-06-29 11:21:41 +02:00
jtrzupek
b3ddb67ced fix(deep-crawl): soft per-run time budget so a slow tube can't hit the hard kill
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>
2026-06-29 11:21:41 +02:00
jtrzupek
cc163e13a7 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
8 changed files with 138 additions and 13 deletions

View file

@ -221,6 +221,12 @@ def _coalesce_canonical_fields(keep: Scene, drop: Scene) -> None:
if drop.title and len(drop.title) > len(keep.title or ""):
keep.title = drop.title
keep.title_normalized = drop.title_normalized
# created_at = najwcześniejsze "first seen" obu scen. NEW badge (apka) bazuje na
# created_at; gdy dedup wybierze świeży re-ingest/mirror jako `keep`, stara treść
# dostawałaby fałszywe NEW po merdżu (report f17799b3). Bierzemy min → zdeduplikowana
# scena zachowuje datę pierwszego pojawienia, nie datę re-ingestu.
if drop.created_at and keep.created_at and drop.created_at < keep.created_at:
keep.created_at = drop.created_at
def _close_pending_candidates(

View file

@ -39,6 +39,13 @@ _PAGE_CAP: dict[str, int] = {
"xvideoscom": 1800,
}
# Miękki budżet czasu na run (s). Detail-fetch scrapery (per-scena fetch strony, np.
# przez wolne proxy) potrafią przekroczyć hard-timeout 3600s z _job_deep_crawl → run
# ubijany w locie, kursor NIE zapisany (orphan thread), tube zero postępu + alert
# GOON-V. Budżet < hard-timeout: przerywamy PO skończonej stronie, zapisujemy kursor,
# wracamy czysto — następny run kontynuuje. Margines 600s na dokończenie strony w toku.
_RUN_BUDGET_SEC = 3000
def _state_path() -> Path:
return Path(getattr(get_settings(), "deepcrawl_state_path", None) or _DEFAULT_STATE)
@ -119,6 +126,7 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
t0 = time.time()
last_done = start - 1
exhausted = False
budget_hit = False
if cap is not None and start > cap:
# kursor osiągnął per-tube cap → traktuj jak koniec katalogu (reset re-sweepuje od 1)
@ -141,7 +149,17 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
except Exception:
counters["errors"] += 1
last_done = page
if cap is not None and last_done >= cap:
# Miękki budżet: stop po skończonej stronie (kursor=last_done zapisany niżej),
# zanim hard-timeout ubije run mid-page (orphan thread, kursor zgubiony — GOON-V).
if time.time() - t0 > _RUN_BUDGET_SEC:
budget_hit = True
log.warning(
"deep-crawl %s: run budget %ds hit at page %d (%d/%d stron) — stop czysto, "
"kursor zapisany, kontynuacja w następnym runie",
sitetag, _RUN_BUDGET_SEC, page, page - start + 1, pages_per_run,
)
break
if not budget_hit and cap is not None and last_done >= cap:
log.info("deep-crawl %s: reached page cap %d (exhausted)", sitetag, cap)
exhausted = True
@ -152,7 +170,10 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
_save_state(state)
log.info(
"deep-crawl %s pages %d-%d: %s exhausted=%s (%.0fs)",
sitetag, start, last_done, counters, exhausted, time.time() - t0,
"deep-crawl %s pages %d-%d: %s exhausted=%s budget_hit=%s (%.0fs)",
sitetag, start, last_done, counters, exhausted, budget_hit, time.time() - t0,
)
return {"sitetag": sitetag, "start": start, "end": last_done, "exhausted": exhausted, **counters}
return {
"sitetag": sitetag, "start": start, "end": last_done,
"exhausted": exhausted, "budget_hit": budget_hit, **counters,
}

View file

@ -26,7 +26,12 @@ from app.db import session_scope
log = logging.getLogger(__name__)
_TELEMETRY_WINDOW_DAYS = 7
_TELEMETRY_MIN_ATTEMPTS = 10 # poniżej tego telemetria niemiarodajna → proxy
# Poniżej tego telemetria niemiarodajna → proxy. Próg podniesiony 10→25 (2026-06-29):
# freshporno (5★ fresh+rich, realnie gra — zweryfikowane 206/507MB) dostało health=0 →
# "OFFLINE" na podstawie 10 prób z jednego pechowego okna (CDN-node blip), user-report
# cb526949. 25 prób to sensowniejszy próg pewności zanim ogłosimy źródło offline; przy
# mniejszej próbce lecimy na proxy/heurystykę (nie zerujemy gwiazdek znanemu-dobremu).
_TELEMETRY_MIN_ATTEMPTS = 25
_EVENT_RETENTION_DAYS = 30
# Richness: wagi składowych (suma=1.0). thumb to minimum higieny, canonical-bogactwo
# (desc/studio/tag/perf) waży więcej. dur średnio.

View file

@ -16,6 +16,21 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-06-29b',
date: 'June 2026',
items: [
'Seeking (swipe or drag) no longer pops a big pause button into the middle of the screen.',
'Diagnostics "open in browser" now opens privately inside the app (incognito) — no cookies, no trace in your real browser.',
],
},
{
id: '2026-06-29',
date: 'June 2026',
items: [
'No more brief blast of sound when a video page is opening — it stays muted until you tap.',
],
},
{
id: '2026-06-26',
date: 'June 2026',

View file

@ -11,6 +11,7 @@ import React from 'react';
import { Pressable, Text, View } from 'react-native';
import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen';
import { BlacklistScreen } from './screens/BlacklistScreen';
import { DiagnosticBrowserScreen } from './screens/DiagnosticBrowserScreen';
import { DonateScreen } from './screens/DonateScreen';
import { FavoritesScreen } from './screens/FavoritesScreen';
import { MovieDetailScreen } from './screens/MovieDetailScreen';
@ -52,6 +53,8 @@ export type RootStackParamList = {
AppLockSettings: undefined;
Blacklist: undefined;
Donate: undefined;
// In-app incognito browser do diagnostyki hostera (long-press na linku playbacku).
DiagnosticBrowser: { url: string };
Player: {
url: string;
sceneId: string;
@ -275,6 +278,11 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
component={DonateScreen}
options={{ title: 'Support project' }}
/>
<Stack.Screen
name="DiagnosticBrowser"
component={DiagnosticBrowserScreen}
options={{ title: 'Diagnostics (incognito)' }}
/>
<Stack.Screen
name="Player"
component={PlayerScreen}

View file

@ -0,0 +1,44 @@
import { useRoute, type RouteProp } from '@react-navigation/native';
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { WebView } from 'react-native-webview';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
// Diagnostyczny in-app browser w trybie incognito (report dccc05e4). Long-press na
// linku playbacku → "Open in browser (diagnostics)" otwiera page_url hostera tutaj,
// żeby user ocenił czy faktycznie broken. `incognito` → zero cookies/cache/historii:
// świeży widok strony (jak u kogoś bez sesji) ORAZ nie zostawia NSFW URL-a w prawdziwej
// przeglądarce usera. Wcześniej Linking.openURL wrzucał to do Chrome z jego sesją.
export function DiagnosticBrowserScreen() {
const route = useRoute<RouteProp<RootStackParamList, 'DiagnosticBrowser'>>();
const { url } = route.params;
return (
<View style={styles.root}>
<WebView
source={{ uri: url }}
incognito
cacheEnabled={false}
thirdPartyCookiesEnabled={false}
startInLoadingState
renderLoading={() => (
<View style={styles.loading}>
<ActivityIndicator color={theme.fg} size="large" />
</View>
)}
style={styles.web}
/>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: theme.bg },
web: { flex: 1, backgroundColor: theme.bg },
loading: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.bg,
},
});

View file

@ -575,7 +575,9 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
.onStart(() => {
cancelHide();
panStartTimeRef.current = player.currentTime || 0;
setControlsVisible(true);
// NIE pokazujemy pełnych kontrolek przy swipe-seeku — pan ma własny popup
// ±czas (panSeekBubble). Wcześniej setControlsVisible(true) wyrzucał duży
// przycisk pauzy na środek i zostawiał go ~3.5s po puszczeniu (report dccc05e4).
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
})
.onUpdate((e) => {
@ -686,6 +688,9 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
// Priorytet: scrubber (palec na pasku) > pan-seek (swipe na video) > playback.
const panSeekRatio = panSeekTarget !== null && dur > 0 ? panSeekTarget / dur : null;
const displayRatio = scrubbingRatio ?? panSeekRatio ?? progressRatio;
// Aktywne przewijanie (palec na pasku albo swipe) — chowamy wtedy duży środkowy
// przycisk play/pauza (report dccc05e4): przy seeku nie chcesz wielkiej pauzy na ekranie.
const isSeeking = panSeekTarget !== null || scrubX !== null;
const displayedTime =
scrubbingRatio !== null
? scrubbingRatio * dur
@ -775,11 +780,13 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
</Pressable>
</View>
{!isSeeking && (
<View style={styles.controlsCenter} pointerEvents="box-none">
<Pressable onPress={togglePlay} hitSlop={20} style={styles.playBtn}>
<Text style={styles.playBtnText}>{isPlaying ? '❚❚' : '▶'}</Text>
</Pressable>
</View>
)}
<View style={styles.controlsBottom} pointerEvents="box-none">
<Text style={styles.timeText}>{formatTime(displayedTime)}</Text>
@ -872,6 +879,22 @@ const INJECTED_JS = `
if (window.__goonPatched) return;
window.__goonPatched = true;
// -- 0a. Mute autoplay until the FIRST user gesture (kills the brief unmuted-audio
// flash gdy WebView otwiera stronę żeby przechwycić stream — report dccc05e4).
// Override play() PRZED page JS: bez gestu → muted; po tapie usera → dźwięk
// dozwolony (zgodne z "dźwięk dopiero po geście usera"). Element-level mute
// niżej łapał za późno (autoplay z dźwiękiem zdążył ruszyć).
try {
var _goonOrigPlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
if (!window.__goonUserGestured) { try { this.muted = true; } catch (e) {} }
return _goonOrigPlay.apply(this, arguments);
};
var _goonAllowSound = function() { window.__goonUserGestured = true; };
window.addEventListener('touchstart', _goonAllowSound, { once: true, capture: true });
window.addEventListener('click', _goonAllowSound, { once: true, capture: true });
} catch (e) {}
// -- 0. Anti-adblock detection bypass --------------------------------------
// Hostery sprawdzają czy ad-script się załadował (np. /js/dnsads.js ustawia
// \`window.cRAds\`). Blokujemy te requesty na poziomie AD_HOSTS, więc flag

View file

@ -775,12 +775,15 @@ function PlaybackButton({
'What do you want to do with this link?',
[
{
text: 'Open in browser (diagnostics)',
text: 'Open in browser (incognito)',
onPress: async () => {
try {
const url = source.page_url || source.embed_url || source.stream_url;
if (url) {
await Linking.openURL(url);
// In-app incognito WebView (report dccc05e4): świeży widok bez sesji/
// cookies usera i bez śladu w prawdziwej przeglądarce. Zamiast
// Linking.openURL (otwierał Chrome z historią + sesją).
nav.navigate('DiagnosticBrowser', { url });
} else {
Alert.alert('No URL', 'This playback has no page_url to open.');
}