Compare commits
4 commits
19483c026b
...
2f68b118c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f68b118c6 | ||
|
|
a5f355841a | ||
|
|
b3ddb67ced | ||
|
|
cc163e13a7 |
8 changed files with 138 additions and 13 deletions
|
|
@ -221,6 +221,12 @@ def _coalesce_canonical_fields(keep: Scene, drop: Scene) -> None:
|
||||||
if drop.title and len(drop.title) > len(keep.title or ""):
|
if drop.title and len(drop.title) > len(keep.title or ""):
|
||||||
keep.title = drop.title
|
keep.title = drop.title
|
||||||
keep.title_normalized = drop.title_normalized
|
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(
|
def _close_pending_candidates(
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,13 @@ _PAGE_CAP: dict[str, int] = {
|
||||||
"xvideoscom": 1800,
|
"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:
|
def _state_path() -> Path:
|
||||||
return Path(getattr(get_settings(), "deepcrawl_state_path", None) or _DEFAULT_STATE)
|
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()
|
t0 = time.time()
|
||||||
last_done = start - 1
|
last_done = start - 1
|
||||||
exhausted = False
|
exhausted = False
|
||||||
|
budget_hit = False
|
||||||
|
|
||||||
if cap is not None and start > cap:
|
if cap is not None and start > cap:
|
||||||
# kursor osiągnął per-tube cap → traktuj jak koniec katalogu (reset re-sweepuje od 1)
|
# 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:
|
except Exception:
|
||||||
counters["errors"] += 1
|
counters["errors"] += 1
|
||||||
last_done = page
|
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)
|
log.info("deep-crawl %s: reached page cap %d (exhausted)", sitetag, cap)
|
||||||
exhausted = True
|
exhausted = True
|
||||||
|
|
||||||
|
|
@ -152,7 +170,10 @@ def run_deep_crawl(*, pages_per_run: int = 60, sitetags: list[str] | None = None
|
||||||
_save_state(state)
|
_save_state(state)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"deep-crawl %s pages %d-%d: %s exhausted=%s (%.0fs)",
|
"deep-crawl %s pages %d-%d: %s exhausted=%s budget_hit=%s (%.0fs)",
|
||||||
sitetag, start, last_done, counters, exhausted, time.time() - t0,
|
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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,12 @@ from app.db import session_scope
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
_TELEMETRY_WINDOW_DAYS = 7
|
_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
|
_EVENT_RETENTION_DAYS = 30
|
||||||
# Richness: wagi składowych (suma=1.0). thumb to minimum higieny, canonical-bogactwo
|
# Richness: wagi składowych (suma=1.0). thumb to minimum higieny, canonical-bogactwo
|
||||||
# (desc/studio/tag/perf) waży więcej. dur średnio.
|
# (desc/studio/tag/perf) waży więcej. dur średnio.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,21 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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',
|
id: '2026-06-26',
|
||||||
date: 'June 2026',
|
date: 'June 2026',
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import React from 'react';
|
||||||
import { Pressable, Text, View } from 'react-native';
|
import { Pressable, Text, View } from 'react-native';
|
||||||
import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen';
|
import { AppLockSettingsScreen } from './screens/AppLockSettingsScreen';
|
||||||
import { BlacklistScreen } from './screens/BlacklistScreen';
|
import { BlacklistScreen } from './screens/BlacklistScreen';
|
||||||
|
import { DiagnosticBrowserScreen } from './screens/DiagnosticBrowserScreen';
|
||||||
import { DonateScreen } from './screens/DonateScreen';
|
import { DonateScreen } from './screens/DonateScreen';
|
||||||
import { FavoritesScreen } from './screens/FavoritesScreen';
|
import { FavoritesScreen } from './screens/FavoritesScreen';
|
||||||
import { MovieDetailScreen } from './screens/MovieDetailScreen';
|
import { MovieDetailScreen } from './screens/MovieDetailScreen';
|
||||||
|
|
@ -52,6 +53,8 @@ export type RootStackParamList = {
|
||||||
AppLockSettings: undefined;
|
AppLockSettings: undefined;
|
||||||
Blacklist: undefined;
|
Blacklist: undefined;
|
||||||
Donate: undefined;
|
Donate: undefined;
|
||||||
|
// In-app incognito browser do diagnostyki hostera (long-press na linku playbacku).
|
||||||
|
DiagnosticBrowser: { url: string };
|
||||||
Player: {
|
Player: {
|
||||||
url: string;
|
url: string;
|
||||||
sceneId: string;
|
sceneId: string;
|
||||||
|
|
@ -275,6 +278,11 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
|
||||||
component={DonateScreen}
|
component={DonateScreen}
|
||||||
options={{ title: 'Support project' }}
|
options={{ title: 'Support project' }}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="DiagnosticBrowser"
|
||||||
|
component={DiagnosticBrowserScreen}
|
||||||
|
options={{ title: 'Diagnostics (incognito)' }}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Player"
|
name="Player"
|
||||||
component={PlayerScreen}
|
component={PlayerScreen}
|
||||||
|
|
|
||||||
44
mobile/src/screens/DiagnosticBrowserScreen.tsx
Normal file
44
mobile/src/screens/DiagnosticBrowserScreen.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -575,7 +575,9 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
cancelHide();
|
cancelHide();
|
||||||
panStartTimeRef.current = player.currentTime || 0;
|
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(() => {});
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||||
})
|
})
|
||||||
.onUpdate((e) => {
|
.onUpdate((e) => {
|
||||||
|
|
@ -686,6 +688,9 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
// Priorytet: scrubber (palec na pasku) > pan-seek (swipe na video) > playback.
|
// Priorytet: scrubber (palec na pasku) > pan-seek (swipe na video) > playback.
|
||||||
const panSeekRatio = panSeekTarget !== null && dur > 0 ? panSeekTarget / dur : null;
|
const panSeekRatio = panSeekTarget !== null && dur > 0 ? panSeekTarget / dur : null;
|
||||||
const displayRatio = scrubbingRatio ?? panSeekRatio ?? progressRatio;
|
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 =
|
const displayedTime =
|
||||||
scrubbingRatio !== null
|
scrubbingRatio !== null
|
||||||
? scrubbingRatio * dur
|
? scrubbingRatio * dur
|
||||||
|
|
@ -775,11 +780,13 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.controlsCenter} pointerEvents="box-none">
|
{!isSeeking && (
|
||||||
<Pressable onPress={togglePlay} hitSlop={20} style={styles.playBtn}>
|
<View style={styles.controlsCenter} pointerEvents="box-none">
|
||||||
<Text style={styles.playBtnText}>{isPlaying ? '❚❚' : '▶'}</Text>
|
<Pressable onPress={togglePlay} hitSlop={20} style={styles.playBtn}>
|
||||||
</Pressable>
|
<Text style={styles.playBtnText}>{isPlaying ? '❚❚' : '▶'}</Text>
|
||||||
</View>
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.controlsBottom} pointerEvents="box-none">
|
<View style={styles.controlsBottom} pointerEvents="box-none">
|
||||||
<Text style={styles.timeText}>{formatTime(displayedTime)}</Text>
|
<Text style={styles.timeText}>{formatTime(displayedTime)}</Text>
|
||||||
|
|
@ -872,6 +879,22 @@ const INJECTED_JS = `
|
||||||
if (window.__goonPatched) return;
|
if (window.__goonPatched) return;
|
||||||
window.__goonPatched = true;
|
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 --------------------------------------
|
// -- 0. Anti-adblock detection bypass --------------------------------------
|
||||||
// Hostery sprawdzają czy ad-script się załadował (np. /js/dnsads.js ustawia
|
// 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
|
// \`window.cRAds\`). Blokujemy te requesty na poziomie AD_HOSTS, więc flag
|
||||||
|
|
|
||||||
|
|
@ -775,12 +775,15 @@ function PlaybackButton({
|
||||||
'What do you want to do with this link?',
|
'What do you want to do with this link?',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: 'Open in browser (diagnostics)',
|
text: 'Open in browser (incognito)',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
try {
|
try {
|
||||||
const url = source.page_url || source.embed_url || source.stream_url;
|
const url = source.page_url || source.embed_url || source.stream_url;
|
||||||
if (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 {
|
} else {
|
||||||
Alert.alert('No URL', 'This playback has no page_url to open.');
|
Alert.alert('No URL', 'This playback has no page_url to open.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue