From b485511f3f94bfd226dd3ba6f445465327e3fee5 Mon Sep 17 00:00:00 2001
From: goon-foss
Date: Mon, 27 Jul 2026 12:04:34 +0200
Subject: [PATCH] feat(fullporner): browse scraper pelnych scen + extractor bez
tokenu
Pelne sceny, nie klipy: mediana ~33 min, zero ponizej 10 min, ~140-240 dziennie,
listing chronologiczny mimo naglowka Featured.
Extractor sklada URL sam, bez tokenu i bez wygasania: id w iframe jest zapisane
wspak, a koncowka to maska bitowa jakosci (1/2/4/8 = 360/480/720/1080). Id bywa
numeryczne albo szesnastkowe, a to drugie to ~25% katalogu. Blokada CDN jest na
naglowku (bez UA 403, bez Referera 404), NIE na fingerprincie TLS, wiec telefon
przechodzi -> mobile_direct_ok. Zweryfikowane 206 + faststart do 1080p.
Metadane cienkie: brak studia, mediana 3 tagi, tytuly przepisane pod SEO. Za to
data jako unix timestamp i dlugosc co do sekundy. Bramka na obsade, bo sceny bez
performera scalily sie 0/23 w dry-runie.
Phash miniatury liczony celowo i to on robi robote: dry-run resolvera bez niego
dawal 31% scalen, pilot z nim dal 65% (seen 62, new 22, updated 40, errors 0).
85 z 89 performerow ma ref tpdb/stashdb.
Co-Authored-By: Claude Opus 5
---
app/connectors/direct_scrapers/__init__.py | 11 ++
app/connectors/direct_scrapers/fullporner.py | 167 +++++++++++++++++++
app/extractors/__init__.py | 5 +
app/extractors/tubes/fullporner.py | 77 +++++++++
4 files changed, 260 insertions(+)
create mode 100644 app/connectors/direct_scrapers/fullporner.py
create mode 100644 app/extractors/tubes/fullporner.py
diff --git a/app/connectors/direct_scrapers/__init__.py b/app/connectors/direct_scrapers/__init__.py
index 9834329..e2c376d 100644
--- a/app/connectors/direct_scrapers/__init__.py
+++ b/app/connectors/direct_scrapers/__init__.py
@@ -48,6 +48,7 @@ from app.connectors.direct_scrapers.galaxyporn import GalaxyPornScraper
from app.connectors.direct_scrapers.pornbusy import PornBusyScraper
from app.connectors.direct_scrapers.pornmike import PornMikeScraper
from app.connectors.direct_scrapers.kporner8 import KPorner8Scraper
+from app.connectors.direct_scrapers.fullporner import FullPornerScraper
from app.connectors.direct_scrapers.watchporn import WatchPornScraper
from app.connectors.direct_scrapers.youperv import YoupervScraper
from app.connectors.direct_scrapers.xhamster import XHamsterScraper
@@ -189,6 +190,16 @@ ALL_BROWSE_SCRAPERS: list[type[BaseBrowseScraper]] = [
# na obsadę przepuszcza 62/115, a z nich 97% performerów ma ref tpdb/stashdb.
# Pilot: seen 62, new 25, updated 37 — 60% dopina się do scen, które już mamy.
KPorner8Scraper,
+ # FullPornerScraper — dodany 2026-07-27. PEŁNE sceny (mediana ~33 min, zero poniżej
+ # 10 min), ~140-240 dziennie, listing chronologiczny mimo nagłówka „Featured".
+ # Playback najtrwalszy w portfolio: mp4 do 1080p bez tokenu i bez wygasania.
+ # Metadane cienkie: BRAK studia, mediana 3 tagi, tytuły przepisane pod SEO — za to
+ # data jako unix timestamp i długość co do sekundy. Dry-run resolvera na 120
+ # scenach: 31% dopina się do tego, co mamy (composite waży performera + długość),
+ # przeciek duplikatów ~15% po odjęciu tła kontrolnego. Bramka na obsadę, bo sceny
+ # bez performera scaliły się 0/23. phash liczony celowo — to jedyny sygnał dla
+ # bulk_dedup na te duplikaty, po tytule nie pójdzie.
+ FullPornerScraper,
# SexuScraper (`sexu.py`) — ZBUDOWANY 2026-07-27, ale NIE ingestujemy. Metadane ma
# najlepsze z całej serii (studio 97%, 96% performerów z refem tpdb/stashdb, dokładna
# długość, świeżość w godzinach), tylko jednostka treści jest zła: to **klipy 5-10
diff --git a/app/connectors/direct_scrapers/fullporner.py b/app/connectors/direct_scrapers/fullporner.py
new file mode 100644
index 0000000..4e86049
--- /dev/null
+++ b/app/connectors/direct_scrapers/fullporner.py
@@ -0,0 +1,167 @@
+"""fullporner.com — browse scraper. Dodany 2026-07-27.
+
+Pełne sceny, nie klipy: mediana długości 2067 s na stronie 1, 1911 s na 40 i 1969 s
+na 300, zero filmów poniżej 10 minut w próbce. To jest różnica, przez którą sexu
+odpadł, a ten wchodzi.
+
+Listing jest ściśle chronologiczny, mimo że nagłówek mówi „Featured Videos" (strona
+1-3 = dziś, 10 = doba, 50 = 9 dni, 500 = 86 dni), 24 sceny na stronę. Daje to
+~140-240 pełnych scen dziennie i katalog co najmniej 12 tys. scen wstecz.
+
+**Metadane są cienkie i trzeba to wiedzieć wprost.** Nie ma studia. Tagów jest
+mediana 3. Tytuły są w 100% przepisane pod SEO, więc nie niosą nic z tytułu
+studyjnego („POV Blowjob Queen Takes It Hard and Deep" tam, gdzie kanon ma „Blacked
+– Dylan Moore – Petite Blonde Squirt"). Za to data jest podana jako **unix timestamp**
+(dokładna, nie względna) i długość co do sekundy.
+
+Dry-run resolvera na 120 scenach (w rollbacku): 37 (31%) dopięło się do scen, które
+już mamy, przez `composite_auto` — composite radzi sobie mimo złych tytułów, bo waży
+performera i długość. Reszta to nowa podaż. Zmierzony przeciek duplikatów, po
+odjęciu tła z kontroli na przesuniętej długości: ~15% katalogu. Liczymy phash
+miniaturki właśnie po to, żeby późniejszy `bulk_dedup` miał czym te duplikaty łapać.
+
+**Bramka na obsadę.** Sceny bez performera (19%) w dry-runie scaliły się w 0 na 23
+przypadków — bez performera composite nie ma na czym pracować, a studia tu nie ma
+i tytuł jest zmyślony, więc taka scena nie ma ŻADNEGO sygnału kanonicznego. Wchodzą
+tylko sceny z obsadą.
+
+Playback: `xiaoshenke.net` (extractor `fullporner`), bezpośredni mp4 do 1080p bez
+tokenu i bez wygasania.
+"""
+from __future__ import annotations
+
+import html as html_mod
+import logging
+import re
+from datetime import UTC, datetime
+
+from app.connectors.base import (
+ RawFingerprint,
+ RawPerformer,
+ RawPlaybackSource,
+ RawScene,
+ RawTag,
+)
+from app.connectors.direct_scrapers._browse_base import (
+ BaseBrowseScraper,
+ compute_thumbnail_phash,
+)
+from app.normalize.text import slugify
+
+log = logging.getLogger(__name__)
+
+_BASE = "https://fullporner.com"
+_THUMB = "https://imgs.xiaoshenke.net/thumb/{}.jpg"
+
+_SCENE_HREF_RE = re.compile(r'href="(/watch/[a-f0-9]+)"')
+_TITLE_RE = re.compile(r"]*>\s*([^<]+?)\s*
")
+# `178508193736:33
` — data i długość
+# siedzą obok siebie w `video-info`, więc bierzemy je jednym wzorcem.
+_INFO_RE = re.compile(
+ r'class="create">(\d+)\s*\s*\s*([0-9:]+)\s*
'
+)
+_PERF_BLOCK_RE = re.compile(r"\s*Pornstars?:\s*(.*?)
", re.IGNORECASE | re.DOTALL)
+_PERF_RE = re.compile(r'href="/pornstar/[^"]+"[^>]*>\s*([^<]+?)\s*')
+_TAG_BLOCK_RE = re.compile(r'class="[^"]*tag-link[^"]*"[^>]*>(.*?)', re.DOTALL)
+_TAG_RE = re.compile(r'href="/category/[^"]+"[^>]*>#\s*([^<]+?)\s*')
+# `