diff --git a/app/connectors/direct_scrapers/porntrex_browse.py b/app/connectors/direct_scrapers/porntrex_browse.py
index 9da409a..ae39f19 100644
--- a/app/connectors/direct_scrapers/porntrex_browse.py
+++ b/app/connectors/direct_scrapers/porntrex_browse.py
@@ -32,7 +32,15 @@ _A_RE = re.compile(
)
_ALT_RE = re.compile(r'alt="([^"]*)"')
_THUMB_RE = re.compile(r'data-src="(//[^"]+\.(?:jpg|jpeg|webp|png)[^"]*)"', re.IGNORECASE)
-_DUR_RE = re.compile(r'class="duration">\s*([\d]{1,2}(?:\s*:\s*[\d]{2}){1,2})\s*<')
+# porntrex zmienił markup kafelka (audit 2026-07-26): `class="duration">MM:SS<` →
+# `
MM:SS
`. Stary wzorzec
+# przestał matchować → duration_sec=NULL dla ~95% świeżych scen, a mobile domyślnie
+# filtruje `min_duration_sec=60`, więc te sceny BYŁY NIEWIDOCZNE w apce (6479 scen w
+# 7 dni; user: "nie widzę nowych scen z porntrex"). Wzorzec obsługuje oba warianty:
+# liczbę mnogą, opcjonalną ikonę przed wartością i H:MM:SS.
+_DUR_RE = re.compile(
+ r'class="durations?"[^>]*>(?:\s*]*>\s*)?\s*([\d]{1,2}(?:\s*:\s*[\d]{2}){1,2})'
+)
def _parse_duration(text: str | None) -> int | None:
diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts
index 85b88fa..b63fd19 100644
--- a/mobile/src/changelog.ts
+++ b/mobile/src/changelog.ts
@@ -16,6 +16,14 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ id: '2026-07-26',
+ date: 'July 2026',
+ items: [
+ 'If your network or DNS blocks a site\'s video servers (some providers do), the app now falls back to streaming through the server instead of dropping you on the site\'s page full of ads.',
+ 'porntrex: new videos show up again. Their site changed, so we were saving new scenes without a length, and the "minimum length" filter was hiding them.',
+ ],
+ },
{
id: '2026-07-22b',
date: 'July 2026',
diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx
index 12ffb82..08793b9 100644
--- a/mobile/src/screens/PlayerScreen.tsx
+++ b/mobile/src/screens/PlayerScreen.tsx
@@ -316,6 +316,25 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
player.play();
return; // sukces → status zmieni się z 'error', łańcuch fallback nie ruszy
}
+ // Phone-resolve zadziałał, ale dał TEN SAM URL — czyli strona parsuje się dobrze,
+ // a pada samo pobranie z CDN. Typowo: DNS/ISP usera blokuje hosta CDN (audit
+ // 2026-07-26: sieć zgłaszającego nulluje `vid-*.eporner.com` → 0.0.0.0, podczas gdy
+ // `www.eporner.com` działa). Wtedy łańcuch fallback NIE ratuje: dla phone-resolved
+ // linków `fallbackProxyUrl === url` (resolver ustawia stream_url = direct_url), więc
+ // krok proxy jest no-opem i lecimy prosto do WebView = strona hostera z reklamami
+ // ("załadowała się reklama zamiast wideo"). Bierzemy więc PRAWDZIWE proxy z backendu
+ // (VPS pobiera swoim DNS-em i streamuje do nas). Tylko na ścieżce błędu.
+ if (!cancelled && !!resolvePageUrl && sceneId && params.playbackId) {
+ const res = await client.resolvePlayback(sceneId, params.playbackId);
+ const viaProxy = (res.links || [])
+ .map((l) => l.stream_url)
+ .find((u): u is string => !!u && u !== url);
+ if (!cancelled && viaProxy) {
+ player.replace(viaProxy);
+ player.play();
+ return;
+ }
+ }
} catch {
// ignore → łańcuch fallback przejmie (proxy/embed/error)
} finally {