Compare commits

...

3 commits

Author SHA1 Message Date
goon-foss
cfe72195ed fix(mobile): siatka tworcy w Edge laduje mala miniature zamiast plakatu
Some checks failed
Backend tests / test (push) Has been cancelled
Kafelek na 1/3 ekranu bral plakat RedGifs 1080x1920 (~120 KB), choc jest
-mobile.jpg 480x854 (~37 KB). Tuzin plakatow ladowal sie tak dlugo, ze
przyszlo zgloszenie z pusta siatka (a2fad93f). Jeszcze nie w OTA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:25:18 +02:00
goon-foss
8f8bb1c448 fix(tags): tag_vertical_counts bez klucza obcego, koniec zakleszczenia o 05:00
Refresh licznikow (co 3 h) i tag_tail_trim (raz dziennie) startuja razem
o 05:00. Kasowanie osieroconych tagow schodzilo kaskada do
tag_vertical_counts i czekalo na refresh, a refresh przy wstawianiu czekal
na blokade tego samego tagu przez sprawdzenie klucza obcego. Od 29.09
Postgres co rano zabijal przycinanie (Sentry GOON-7N). Tabela to kopia
liczona od zera, wiec klucz obcy nie byl potrzebny (migracja 0050).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:24:42 +02:00
goon-foss
8a9fea3eef chore: usuniete jednorazowe artefakty z maja
Analiza zrodel z theporndude (zrzuty JSON, raporty i piec skryptow,
ktore je czytaly), probka strony streamtape i nieuzywana strona
landing/ (goon-foss.org serwuje site/).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:26:51 +02:00
21 changed files with 49 additions and 10376 deletions

View file

@ -0,0 +1,40 @@
"""tag_vertical_counts bez klucza obcego do tags (zakleszczenie o 05:00, Sentry GOON-7N).
Codziennie o 05:00 rusza naraz refresh_taxonomy_counts (co 3 h) i tag_tail_trim (raz
dziennie). Przycinanie kasuje przypisania rule34video, a potem osierocone tagi; DELETE na
tags schodzi kaskada do tag_vertical_counts i czeka na refresh, ktory te wiersze wlasnie
podmienia. Refresh wstawia liczniki z migawki, w ktorej skasowane przypisania jeszcze sa,
a sprawdzenie klucza obcego czeka na blokade tego samego wiersza tags. Od 2026-09-29
Postgres co rano zabija tag_tail_trim.
Tabela to kopia liczona od zera co 3 h, wiec klucz obcy nic nie daje: wiersz po
skasowanym tagu znika przy nastepnym odswiezeniu, a /tags i tak zaczyna od tabeli tags.
Revision ID: 0050_tag_vertical_counts_no_fk
Revises: 0049_playback_origin_sort_idx
Create Date: 2026-09-30
"""
from __future__ import annotations
from alembic import op
revision = "0050_tag_vertical_counts_no_fk"
down_revision: str | None = "0049_playback_origin_sort_idx"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("SET LOCAL lock_timeout = '5s'")
op.execute(
"ALTER TABLE tag_vertical_counts DROP CONSTRAINT IF EXISTS fk_tag_vertical_counts_tag_id_tags"
)
def downgrade() -> None:
op.execute("SET LOCAL lock_timeout = '5s'")
op.execute("DELETE FROM tag_vertical_counts v WHERE NOT EXISTS (SELECT 1 FROM tags t WHERE t.id = v.tag_id)")
op.execute(
"ALTER TABLE tag_vertical_counts ADD CONSTRAINT fk_tag_vertical_counts_tag_id_tags "
"FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE"
)

View file

@ -2,13 +2,14 @@
Wypelnia `refresh_taxonomy_counts`, czyta /tags?vertical=. Ta sama definicja co dawny
agregat na zywo: sceny z >= 1 zywym zrodlem z originow wertykalu, bez progu dlugosci.
Patrz migracja 0047.
Patrz migracja 0047. Bez klucza obcego do tags (0050): z nim kasowanie tagow w
tag_tail_trim zakleszczalo sie z odswiezaniem tej tabeli.
"""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy import Integer, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@ -19,7 +20,5 @@ class TagVerticalCount(Base):
__tablename__ = "tag_vertical_counts"
vertical: Mapped[str] = mapped_column(String(32), primary_key=True)
tag_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True
)
tag_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
scene_count: Mapped[int] = mapped_column(Integer, nullable=False)

View file

@ -1,302 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="description" content="Goon — self-hosted adult content metadata aggregator. Mobile-first, FOSS, ad-free." />
<meta name="robots" content="noindex, nofollow" />
<title>goon · self-hosted adult content aggregator</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
bg: '#08090F',
bgElevated: '#11131C',
card: '#1A1D2A',
border: '#262A3D',
accent: '#8B5CF6',
accentGlow: '#A78BFA',
accentDeep: '#5B21B6',
good: '#10B981',
},
fontFamily: {
sans: ['-apple-system', 'BlinkMacSystemFont', 'Inter', 'system-ui', 'sans-serif'],
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'],
},
},
},
};
</script>
<style>
body { background-color: #08090F; color: #F4F4F8; }
.glow { box-shadow: 0 0 60px -10px rgba(139, 92, 246, 0.4); }
.grad { background: radial-gradient(60% 60% at 50% 0%, rgba(139,92,246,0.18) 0%, rgba(8,9,15,0) 70%); }
.card-hover { transition: transform 200ms ease, border-color 200ms ease; }
.card-hover:hover { transform: translateY(-2px); border-color: #8B5CF6; }
.pulse-dot { animation: pulse 2.5s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
</style>
</head>
<body class="font-sans antialiased">
<!-- AGE GATE -->
<div id="agegate" class="fixed inset-0 z-50 bg-bg flex items-center justify-center px-6">
<div class="max-w-md text-center">
<div class="inline-block w-3 h-3 rounded-full bg-accent glow pulse-dot mb-6"></div>
<h1 class="text-3xl font-extrabold tracking-tight mb-3">18+ Adults Only</h1>
<p class="text-sm text-gray-400 mb-8 leading-relaxed">
This site discusses software for indexing adult content. You must be of legal
age in your jurisdiction to enter.
</p>
<div class="flex gap-3 justify-center">
<button onclick="document.getElementById('agegate').style.display='none'"
class="px-6 py-3 rounded-xl bg-accent text-white font-bold hover:bg-accentDeep transition">
I'm 18 or older
</button>
<a href="https://www.google.com"
class="px-6 py-3 rounded-xl bg-bgElevated border border-border text-gray-300 font-semibold">
Take me out
</a>
</div>
</div>
</div>
<!-- HERO -->
<header class="grad relative pt-20 pb-24 px-6 overflow-hidden">
<div class="max-w-5xl mx-auto">
<div class="flex items-center gap-2 mb-8">
<div class="w-3 h-3 rounded-full bg-accent glow"></div>
<span class="text-sm font-bold tracking-widest text-gray-300 uppercase">goon</span>
</div>
<h1 class="text-5xl md:text-7xl font-extrabold tracking-tight leading-[0.95] mb-6">
Self-hosted<br>
<span class="text-accent">scene catalog</span><br>
for grown-ups.
</h1>
<p class="text-lg md:text-xl text-gray-400 max-w-2xl leading-relaxed mb-10">
Goon indexes scene metadata from TPDB &amp; StashDB, deduplicates across
30+ public tubes, and serves a fast mobile client. Zero ads. Zero tracking.
Download, open, browse — no account, no setup.
</p>
<div class="flex flex-wrap gap-3">
<a href="/goon-v0.1.9.apk"
class="px-6 py-4 rounded-xl bg-accent text-white font-bold hover:bg-accentDeep transition glow">
Download APK
</a>
<a href="#donate"
class="px-6 py-4 rounded-xl bg-transparent border border-border text-gray-400 font-semibold hover:text-accent hover:border-accent transition">
♥ Support project
</a>
</div>
<p class="text-xs text-gray-500 mt-6 font-mono">
Android 7+ · no setup, no login · 18+
</p>
</div>
</header>
<!-- FEATURES -->
<section class="px-6 py-20 bg-bgElevated/30 border-y border-border">
<div class="max-w-5xl mx-auto">
<h2 class="text-3xl font-extrabold mb-2 tracking-tight">What it does</h2>
<p class="text-gray-500 mb-12 max-w-xl">
Goon is not a tube. It does not host, transcode, or proxy content.
It is a metadata aggregator + mobile UI for finding scenes that are
already publicly available.
</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="card-hover bg-card border border-border rounded-2xl p-6">
<div class="text-accent text-2xl mb-3">⌬</div>
<h3 class="text-lg font-bold mb-2">Multi-source ingest</h3>
<p class="text-sm text-gray-400 leading-relaxed">
TPDB &amp; StashDB metadata + 30+ public tubes. Cross-source
deduplication via perceptual-hash thumbnails + title-Levenshtein matching.
</p>
</div>
<div class="card-hover bg-card border border-border rounded-2xl p-6">
<div class="text-accent text-2xl mb-3">⌖</div>
<h3 class="text-lg font-bold mb-2">Per-performer backfill</h3>
<p class="text-sm text-gray-400 leading-relaxed">
A continuous worker walks performers by staleness and back-fills tube scenes
for the longest-stale first. Completeness over recency.
</p>
</div>
<div class="card-hover bg-card border border-border rounded-2xl p-6">
<div class="text-accent text-2xl mb-3">⌥</div>
<h3 class="text-lg font-bold mb-2">Smart stream resolution</h3>
<p class="text-sm text-gray-400 leading-relaxed">
yt-dlp for mainstream tubes + P.A.C.K.E.R. unpacker for JWPlayer hosters
+ WebView fallback for IP-bound CDNs. Streams direct from source, no transcoding.
</p>
</div>
<div class="card-hover bg-card border border-border rounded-2xl p-6">
<div class="text-accent text-2xl mb-3">⌗</div>
<h3 class="text-lg font-bold mb-2">Mobile-first UI</h3>
<p class="text-sm text-gray-400 leading-relaxed">
React Native + Expo. Scene grid, performer pages, watch history,
favorites, hold-to-preview thumbnails. Built for thumb scrolling.
</p>
</div>
<div class="card-hover bg-card border border-border rounded-2xl p-6">
<div class="text-accent text-2xl mb-3">⌘</div>
<h3 class="text-lg font-bold mb-2">Privacy by default</h3>
<p class="text-sm text-gray-400 leading-relaxed">
App lock (PIN + biometrics), FLAG_SECURE screenshot block, age gate.
No analytics. No telemetry unless YOU configure Sentry with your own DSN.
</p>
</div>
<div class="card-hover bg-card border border-border rounded-2xl p-6">
<div class="text-accent text-2xl mb-3">⌬</div>
<h3 class="text-lg font-bold mb-2">Works out of the box</h3>
<p class="text-sm text-gray-400 leading-relaxed">
Download the APK and it connects automatically — no account, no
config. Power users can point it at their own self-hosted backend.
</p>
</div>
</div>
</div>
</section>
<!-- SCREENSHOTS -->
<section class="px-6 py-20">
<div class="max-w-5xl mx-auto">
<h2 class="text-3xl font-extrabold mb-2 tracking-tight">In the app</h2>
<p class="text-gray-500 mb-12">Screen-shots from a real install — censored where needed.</p>
<!-- Placeholder grid: replace with real screen-shots when ready -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="aspect-[9/19.5] rounded-2xl bg-card border border-border flex items-center justify-center text-gray-600 text-xs font-mono">
Scenes grid
</div>
<div class="aspect-[9/19.5] rounded-2xl bg-card border border-border flex items-center justify-center text-gray-600 text-xs font-mono">
Scene detail
</div>
<div class="aspect-[9/19.5] rounded-2xl bg-card border border-border flex items-center justify-center text-gray-600 text-xs font-mono">
Performer page
</div>
<div class="aspect-[9/19.5] rounded-2xl bg-card border border-border flex items-center justify-center text-gray-600 text-xs font-mono">
Favorites
</div>
</div>
</div>
</section>
<!-- GET STARTED -->
<section class="px-6 py-20 bg-bgElevated/30 border-y border-border">
<div class="max-w-3xl mx-auto">
<h2 class="text-3xl font-extrabold mb-3 tracking-tight">Get started</h2>
<p class="text-gray-500 mb-8">Three steps. No account, no server, no config.</p>
<ol class="space-y-4">
<li class="bg-card border border-border rounded-2xl p-5 flex gap-4">
<span class="text-accent font-extrabold text-xl">1</span>
<span class="text-sm text-gray-300 leading-relaxed">
<a href="/goon-v0.1.9.apk" class="text-accent font-bold hover:underline">Download the APK</a>
and open it. Allow "install from unknown sources" for your browser if Android asks.
</span>
</li>
<li class="bg-card border border-border rounded-2xl p-5 flex gap-4">
<span class="text-accent font-extrabold text-xl">2</span>
<span class="text-sm text-gray-300 leading-relaxed">
Open the app, accept the 18+ gate. It connects automatically — no login.
</span>
</li>
<li class="bg-card border border-border rounded-2xl p-5 flex gap-4">
<span class="text-accent font-extrabold text-xl">3</span>
<span class="text-sm text-gray-300 leading-relaxed">
Browse. That's it.
</span>
</li>
</ol>
</div>
</section>
<!-- DONATE -->
<section id="donate" class="px-6 py-20">
<div class="max-w-3xl mx-auto">
<h2 class="text-3xl font-extrabold mb-3 tracking-tight">Support the project</h2>
<p class="text-gray-400 mb-8 leading-relaxed max-w-xl">
Goon stays free and ad-free because donations cover the VPS, the
TPDB/StashDB tokens, and the time. <strong class="text-gray-200">Crypto
only</strong> &mdash; mainstream processors won't touch adult-adjacent
FOSS projects.
</p>
<div class="grid sm:grid-cols-3 gap-3">
<div class="bg-card border border-border rounded-2xl p-5">
<div class="text-orange-500 text-xs font-mono font-bold mb-1">XMR</div>
<div class="text-sm font-bold mb-3">Monero</div>
<p class="text-xs text-gray-500 leading-relaxed">
Privacy by default. Recommended. Get the address from the app or the project README.
</p>
</div>
<div class="bg-card border border-border rounded-2xl p-5">
<div class="text-yellow-500 text-xs font-mono font-bold mb-1">BTC</div>
<div class="text-sm font-bold mb-3">Bitcoin</div>
<p class="text-xs text-gray-500 leading-relaxed">
Mainstream &amp; easy to send. Use a fresh wallet if you care about on-chain anonymity.
</p>
</div>
<div class="bg-card border border-border rounded-2xl p-5">
<div class="text-blue-400 text-xs font-mono font-bold mb-1">LTC</div>
<div class="text-sm font-bold mb-3">Litecoin</div>
<p class="text-xs text-gray-500 leading-relaxed">
Fast and cheap (~$0.01 fee). Lower adoption than BTC but friendlier for small donations.
</p>
</div>
</div>
<p class="text-xs text-gray-500 mt-6">
Addresses + QR codes are shown in the app under Scenes &raquo; ♥.
</p>
</div>
</section>
<!-- FOOTER -->
<footer class="px-6 py-12 border-t border-border">
<div class="max-w-5xl mx-auto flex flex-col sm:flex-row gap-6 justify-between text-xs text-gray-500">
<div>
<div class="flex items-center gap-2 mb-2">
<div class="w-2 h-2 rounded-full bg-accent"></div>
<span class="font-bold tracking-widest uppercase">goon</span>
</div>
<p>Adult content metadata aggregator. FOSS, ad-free.</p>
<p>MIT license. No warranty. 18+ jurisdictions only.</p>
</div>
<div class="flex flex-col gap-1 text-right">
<a href="/goon-v0.1.9.apk" class="hover:text-accent transition">Download APK</a>
<a href="#donate" class="hover:text-accent transition">Support</a>
</div>
</div>
<p class="max-w-5xl mx-auto mt-6 text-[10px] text-gray-600 leading-relaxed">
Goon does not host, transcode, store, or distribute any media. It scrapes
publicly-available metadata and links out to the source. Users are
responsible for complying with local law.
</p>
</footer>
<script>
// Persist age-gate acknowledgement so it doesn't reappear every visit.
if (localStorage.getItem('agegate_v1') === 'ok') {
document.getElementById('agegate').style.display = 'none';
}
document.querySelector('#agegate button').addEventListener('click', function () {
localStorage.setItem('agegate_v1', 'ok');
});
</script>
</body>
</html>

View file

@ -122,9 +122,12 @@ export function ClipCreatorScreen() {
}
renderItem={({ item, index }) => (
<Pressable style={styles.tile} onPress={() => setPlaying(index)}>
{item.clip.poster_url || item.clip.thumb_url ? (
{/* Najpierw thumb_url: RedGifs -mobile.jpg to 480x854 (~37 KB), a plakat
1080x1920 (~120 KB) na kafelek szerokosci 1/3 ekranu; tuzin plakatow
ladowal sie tak dlugo, ze ktos zglosil pusta siatke (a2fad93f). */}
{item.clip.thumb_url || item.clip.poster_url ? (
<Image
source={{ uri: (item.clip.poster_url || item.clip.thumb_url) as string }}
source={{ uri: (item.clip.thumb_url || item.clip.poster_url) as string }}
style={StyleSheet.absoluteFill}
contentFit="cover"
transition={100}

View file

@ -1,29 +0,0 @@
"""Coverage check: ile tube'ów z theporndude.com mamy już w bazie."""
from app.db import SessionLocal
from sqlalchemy import text
def main():
with SessionLocal() as s:
# Wszystkie distinct origins (canonical + tube: + pornapp:)
rows = s.execute(text("""
SELECT origin, COUNT(*) AS n,
COUNT(*) FILTER (WHERE dead_at IS NULL) AS live,
COUNT(*) FILTER (WHERE dead_at IS NOT NULL) AS dead
FROM playback_sources
GROUP BY origin
ORDER BY origin
""")).all()
print(f"distinct origins: {len(rows)}")
by_kind = {}
for r in rows:
kind = r.origin.split(":")[0] if ":" in r.origin else "other"
by_kind.setdefault(kind, []).append((r.origin, r.n, r.live, r.dead))
for kind, items in by_kind.items():
print(f"\n=== {kind} ({len(items)} origins) ===")
for origin, n, live, dead in sorted(items, key=lambda x: -x[2]):
print(f" {origin:<35} n={n:>7,} live={live:>7,} dead={dead:>5,}")
if __name__ == "__main__":
main()

View file

@ -1,118 +0,0 @@
"""Cross-check 166 resolved theporndude domains vs nasze 25 tube origins."""
import json
from pathlib import Path
# Origins z DB (live + dead) + extractor REGISTRY w app/extractors/__init__.py
OUR_ORIGINS = [
# DB live + dead
"tube:0dayxxcom", "tube:epornercom", "tube:fpoxxx", "tube:freshpornoorg",
"tube:hqpornercom", "tube:latestpornvideocom", "tube:mypornerleakcom",
"tube:perverzijacom", "tube:porn00org", "tube:porndishcom", "tube:porndittcom",
"tube:pornhatcom", "tube:pornhubcom", "tube:porntrexcom", "tube:pornxpph",
"tube:redtubecom", "tube:sxylandcom", "tube:sxyprncom", "tube:xhamstercom",
"tube:xnxxcom", "tube:xvideoscom", "tube:youporncom", "tube:latestleaksco",
"tube:siskavideo", "tube:hdporn92com",
# REGISTRY only (extractor known, brak playback w live DB)
"tube:xmoviesforyoucom", "tube:watchporn", "tube:porn4dayspw",
"tube:paradisehillcc",
]
# Tylko realne TLD-y. NIE "tube"/"porn"/"xxx" bo to często części nazwy (redtube, pornhub, fpoxxx).
_TLD_RE = __import__("re").compile(r"(com|net|org|tv|cc|pw|co|to|ws|me|sx|info|biz)$")
def _strip_tld(s: str) -> str:
"""xvideoscom -> xvideos; pornhubcom -> pornhub; hdporn92com -> hdporn92"""
return _TLD_RE.sub("", s)
# Build sitetag → matching variants for fuzzy match
def origin_to_sitetag(origin: str) -> str:
return origin.replace("tube:", "")
def domain_to_sitetag(domain: str) -> str:
"""xvideos.com -> xvideoscom, porntrex.com -> porntrexcom"""
return domain.lower().replace(".", "").replace("-", "")
def match(slug: str, domain: str) -> str | None:
"""Match po `slug` (z theporndude review URL) lub `real_domain` (z pdude.link).
Slug to nazwa tube'a (np. 'xvideos', 'pornhub', 'paradisehill').
Origin format: tube:<sitetag>, gdzie sitetag = domain.replace('.', '').
Match na "slug pasuje do sitetag bez TLD" daje dobry recall.
"""
candidates = []
if slug:
candidates.append(slug.lower().replace("-", ""))
if domain:
candidates.append(domain_to_sitetag(domain))
if not candidates:
return None
for o in OUR_ORIGINS:
st = origin_to_sitetag(o)
st_no_tld = _strip_tld(st)
for c in candidates:
c_no_tld = _strip_tld(c)
if c_no_tld == st_no_tld and len(c_no_tld) >= 3:
return o
return None
def main():
data = json.loads(Path("theporndude_resolved.json").read_text())
have = []
new = []
error = []
for r in data:
if "error" in r and not r.get("real_domain"):
error.append(r)
continue
domain = r.get("real_domain", "")
our = match(r.get("slug", ""), domain)
r["our_origin"] = our
if our:
have.append(r)
else:
new.append(r)
print(f"=== Coverage ===")
print(f"Total theporndude top-porn-tubes: {len(data)}")
print(f" Already in our DB: {len(have)}")
print(f" NEW (potential candidates): {len(new)}")
print(f" Errors: {len(error)}")
print()
print(f"=== Already have (matched) — top 30 by theporndude rank ===")
for r in sorted(have, key=lambda x: x["rank"])[:30]:
print(
f" #{r['rank']:>3} score={r.get('theporndude_score') or '?':>4} "
f"{r['real_domain']:<28} -> {r['our_origin']}"
)
print()
print(f"=== NEW candidates (not in DB) — top 60 by theporndude rank ===")
for r in sorted(new, key=lambda x: x["rank"])[:60]:
print(
f" #{r['rank']:>3} score={r.get('theporndude_score') or '?':>4} "
f"{r.get('real_domain') or '?':<30} ({r['slug']})"
)
# Output detailed
summary = {
"total": len(data),
"already_have": [{"rank": r["rank"], "slug": r["slug"], "domain": r["real_domain"],
"score": r.get("theporndude_score"), "our_origin": r["our_origin"]}
for r in sorted(have, key=lambda x: x["rank"])],
"new_candidates": [{"rank": r["rank"], "slug": r["slug"], "domain": r.get("real_domain"),
"score": r.get("theporndude_score"),
"final_url": r.get("final_url", "")}
for r in sorted(new, key=lambda x: x["rank"])],
"errors": [{"rank": r["rank"], "slug": r["slug"], "error": r.get("error")}
for r in error],
}
Path("theporndude_coverage.json").write_text(json.dumps(summary, indent=2))
print(f"\n-> theporndude_coverage.json")
if __name__ == "__main__":
main()

View file

@ -1,203 +0,0 @@
"""Batch curl triage 144 nowych theporndude tubes:
- HEAD root domain (200/4xx/5xx/timeout?)
- GET / → check landing markers: video listing, sceny, login wall, redirect
- GET /latest, /videos, /tube/recent → check które listing path działa
- Wynik: per-slug status + landing markers + scene_url_pattern guess
"""
import asyncio
import json
import re
from pathlib import Path
from urllib.parse import urlparse
import httpx
COVERAGE_FILE = Path("theporndude_coverage.json")
OUT_FILE = Path("theporndude_triage.json")
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36"
# Common candidate landing paths
LANDING_PATHS = ["/", "/latest", "/latest-videos", "/recent", "/new", "/videos", "/category/new", "/top-rated"]
# Markers w HTML
SCENE_LINK_PATTERNS = [
r'<a[^>]+href="(/(?:video|videos|watch|v|scene|scenes|stream|movie|movies|view|play|porn|tube)/[^"]+)"',
r'<a[^>]+href="((?:https?:)?//[^/"]+/(?:video|videos|watch|v|scene|scenes|stream|movie|movies|view|play|porn|tube)/[^"]+)"',
]
META_MARKERS = [
(r'jsonld|json-ld|"@type"\s*:\s*"VideoObject"', "jsonld_video"),
(r'<meta\s+property="og:type"\s+content="video', "og_video"),
(r'<meta\s+name="description"\s+content="([^"]+)"', "meta_desc"),
(r'class="[^"]*\b(?:video|scene|episode)-?(?:item|card|tile|thumb)\b', "video_card"),
(r'class="[^"]*\b(?:performer|actress|model|pornstar)\b', "performer_marker"),
(r'class="[^"]*\b(?:studio|production|brand|channel)\b', "studio_marker"),
(r'class="[^"]*\b(?:duration|runtime|length)\b|<time\s+datetime=', "duration_marker"),
(r'\b(?:HLS|hls|m3u8|application/x-mpegURL)\b', "hls_marker"),
(r'(?:hlsmanifest|videoUrl|video_url|stream_url|streamUrl)\s*[:=]\s*["\']', "stream_url_marker"),
(r'login\s*required|create\s+account|sign\s+(?:in|up)|members\s+only|join\s+now\s+to\s+watch', "auth_wall"),
(r'<title>[^<]*\b(?:404|not\s+found|gone|domain)\b[^<]*</title>', "dead_404"),
(r'<meta[^>]+http-equiv="refresh"[^>]+url=', "meta_refresh"),
]
# Slug w feedzie to slug RECENZJI ("youperv-website-review"), nie domena. Bez odcięcia
# sufiksu fallback budował "youperv-website-review.com" → conn_refused → wpis lądował
# jako "dead/skip". Tak wypadło 15 ze 144 kandydatów, w tym youperv i pornbusy, które
# potem niezależnie oceniliśmy na 4.5-5/5 i dodaliśmy. Fix 2026-07-27.
_REVIEW_SUFFIX_RE = re.compile(r"-(?:website-)?review$", re.IGNORECASE)
def _slug_to_domain(slug: str) -> str:
return _REVIEW_SUFFIX_RE.sub("", slug.lower().strip())
async def fetch_one(cli: httpx.AsyncClient, url: str) -> tuple[int, str]:
"""Fetch z Chrome TLS (curl_cffi), NIE gołym httpx.
FIX 2026-07-27: pierwotna wersja szła czystym httpx, którego JA3 fingerprint
większość tubów za Cloudflare odrzuca. Efekt: 8 timeoutów + 4× 403 w wynikach,
czytane potem jako "dead/skip", choć strony działają w przeglądarce. `browser_get`
(curl_cffi, chrome120) przechodzi, to ta sama ścieżka, której używają nasze
scrapery, więc triage mierzy wreszcie to samo co produkcja.
"""
import asyncio as _asyncio
from app.extractors._fetch import browser_get
def _sync():
return browser_get(url, timeout=20.0, headers={"User-Agent": UA})
try:
r = await _asyncio.to_thread(_sync)
return r.status_code, (r.text or "")[:200_000]
except Exception as e:
name = type(e).__name__
if "Timeout" in name:
return -2, "timeout"
if "Connect" in name or "Resolve" in name or "DNS" in name:
return -1, "conn_refused"
return -9, f"{name}: {e}"[:120]
def analyze_html(html: str) -> dict:
found = {}
for pattern, name in META_MARKERS:
if re.search(pattern, html, re.IGNORECASE):
found[name] = True
# Scene link patterns
scene_links = []
for p in SCENE_LINK_PATTERNS:
for m in re.finditer(p, html, re.IGNORECASE):
scene_links.append(m.group(1)[:120])
if len(scene_links) >= 5:
break
if len(scene_links) >= 5:
break
if scene_links:
found["scene_link_samples"] = scene_links[:3]
# Unique pattern (path prefix po slash)
prefixes = set()
for link in scene_links:
parts = link.lstrip("/").split("/", 2)
if parts:
prefixes.add("/" + parts[0])
found["scene_path_prefixes"] = sorted(prefixes)
return found
async def audit_one(cli: httpx.AsyncClient, slug: str, domain: str) -> dict:
"""Audit pojedynczego tube'a."""
out = {"slug": slug, "domain": domain}
# Próbuj https://<domain>/ root
if not domain or not re.match(r"^[\w\.-]+\.\w+$", domain):
out["error"] = "no_valid_domain"
return out
root_url = f"https://{domain}/"
status, html = await fetch_one(cli, root_url)
out["root_status"] = status
if status not in (200, 301, 302):
out["root_error"] = html[:80] if isinstance(html, str) else None
return out
out["root_findings"] = analyze_html(html)
# Heurystyka score 0-3
f = out["root_findings"]
score = 0
reasons = []
if f.get("jsonld_video"):
score += 1
reasons.append("jsonld_video")
if f.get("og_video"):
score += 1
reasons.append("og_video")
if f.get("video_card"):
score += 1
reasons.append("video_card")
if f.get("performer_marker"):
score += 1
reasons.append("performer_marker")
if f.get("studio_marker"):
score += 1
reasons.append("studio_marker")
if f.get("duration_marker"):
score += 0.5
if f.get("hls_marker") or f.get("stream_url_marker"):
score += 0.5
if f.get("scene_path_prefixes"):
score += 1
reasons.append(f"scene_paths={f['scene_path_prefixes']}")
if f.get("auth_wall"):
score -= 2
reasons.append("auth_wall")
if f.get("dead_404"):
score -= 5
reasons.append("dead_404")
if f.get("meta_refresh"):
score -= 1
reasons.append("meta_refresh")
out["heuristic_score"] = round(score, 1)
out["reasons"] = reasons
return out
async def main():
cov = json.loads(COVERAGE_FILE.read_text())
new_candidates = cov["new_candidates"]
print(f"audytuję {len(new_candidates)} nowych kandydatów…")
timeout = httpx.Timeout(15.0, connect=8.0)
limits = httpx.Limits(max_keepalive_connections=20, max_connections=50)
async with httpx.AsyncClient(timeout=timeout, limits=limits, http2=False) as cli:
sem = asyncio.Semaphore(12)
async def worker(r):
async with sem:
# Use slug or guess domain (most slug.com)
domain = r.get("domain") or ""
# Jeśli pdude.link daje porndudecams.com (interstitial), użyj <slug>.com
if not domain or "porndudecams" in domain:
domain = f"{_slug_to_domain(r['slug'])}.com"
return {**r, **(await audit_one(cli, r["slug"], domain))}
results = await asyncio.gather(*[worker(r) for r in new_candidates])
OUT_FILE.write_text(json.dumps(results, indent=2))
# Stats
by_score = {}
for r in results:
s = r.get("heuristic_score", 0)
bucket = "5+" if s >= 5 else "3-5" if s >= 3 else "1-3" if s >= 1 else "<1"
by_score.setdefault(bucket, []).append(r)
print("\n=== Heurystyczny rozkład (canonical-fit) ===")
for b in ["5+", "3-5", "1-3", "<1"]:
if b in by_score:
print(f" {b:<5} {len(by_score[b])} tubów")
print(f"\n-> {OUT_FILE}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,234 +0,0 @@
"""Pełny pipeline dla theporndude /full-porn-movies-sites (94 tubes):
1. Resolve real domains (pdude.link follow, ale follow only 1 hop)
2. Coverage match vs nasze 25+ origins
3. Curl triage HTML markers
4. Per-tube scorecard
"""
import asyncio
import json
import re
from pathlib import Path
from urllib.parse import urlparse
import httpx
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36"
OUR_ORIGINS = [
"tube:0dayxxcom", "tube:epornercom", "tube:fpoxxx", "tube:freshpornoorg",
"tube:hqpornercom", "tube:latestpornvideocom", "tube:mypornerleakcom",
"tube:perverzijacom", "tube:porn00org", "tube:porndishcom", "tube:porndittcom",
"tube:pornhatcom", "tube:pornhubcom", "tube:porntrexcom", "tube:pornxpph",
"tube:redtubecom", "tube:sxylandcom", "tube:sxyprncom", "tube:xhamstercom",
"tube:xnxxcom", "tube:xvideoscom", "tube:youporncom", "tube:latestleaksco",
"tube:siskavideo", "tube:hdporn92com",
"tube:xmoviesforyoucom", "tube:watchporn", "tube:porn4dayspw",
"tube:paradisehillcc",
]
_TLD_RE = re.compile(r"(com|net|org|tv|cc|pw|co|to|ws|me|sx|info|biz)$")
def _strip_tld(s: str) -> str:
return _TLD_RE.sub("", s)
def match(slug: str, domain: str) -> str | None:
candidates = []
if slug:
candidates.append(slug.lower().replace("-", ""))
if domain:
candidates.append(domain.lower().replace(".", "").replace("-", ""))
for o in OUR_ORIGINS:
st = o.replace("tube:", "")
st_no_tld = _strip_tld(st)
for c in candidates:
c_no_tld = _strip_tld(c)
if c_no_tld == st_no_tld and len(c_no_tld) >= 3:
return o
return None
SCENE_PATH_RE = re.compile(
r'<a[^>]+href="((?:https?:)?//?[^"]*?/(?:video|videos|watch|v|scene|movie|movies|play|view|stream)/[^"]+)"',
re.IGNORECASE,
)
META_MARKERS = [
(r'"@type"\s*:\s*"VideoObject"', "jsonld_video"),
(r'<meta\s+property="og:type"\s+content="video', "og_video"),
(r'class="[^"]*\b(?:video|scene|movie|episode)-?(?:item|card|tile|thumb|block)\b', "video_card"),
(r'class="[^"]*\b(?:performer|actress|model|pornstar|cast)\b|href="[^"]*/pornstar', "performer_marker"),
(r'class="[^"]*\b(?:studio|production|brand|channel|network)\b|href="[^"]*/studio', "studio_marker"),
(r'class="[^"]*\b(?:duration|runtime|length)\b|itemprop="duration"', "duration_marker"),
(r'\b(?:HLS|m3u8|application/x-mpegURL)\b', "hls_marker"),
(r'(?:videoUrl|video_url|stream_url|streamUrl)\s*[:=]\s*["\']', "stream_url_marker"),
(r'(?:login\s+required|create\s+account|members\s+only|join\s+now)', "auth_wall"),
(r'<title>[^<]*\b(?:404|not\s+found|domain\s+for\s+sale|gone)\b', "dead_404"),
]
async def fetch_one(cli: httpx.AsyncClient, url: str, *, max_redirects: int = 5) -> tuple[int, str, str]:
try:
r = await cli.get(url, headers={"User-Agent": UA}, follow_redirects=False)
# Follow up to max_redirects but stop on cross-domain redirect-out (to detect pdude.link → ad)
hops = 0
first_external_domain = None
cur = r
cur_url = url
while cur.status_code in (301, 302, 303, 307, 308) and hops < max_redirects:
loc = cur.headers.get("location")
if not loc:
break
if loc.startswith("/"):
p = urlparse(cur_url)
loc = f"{p.scheme}://{p.netloc}{loc}"
cur_url = loc
hops += 1
# Track first external (non-pdude, non-theporndude)
host = urlparse(loc).hostname or ""
if first_external_domain is None and not host.endswith("pdude.link") and not host.endswith("theporndude.com"):
first_external_domain = host.replace("www.", "")
cur = await cli.get(loc, headers={"User-Agent": UA}, follow_redirects=False)
return cur.status_code, cur.text[:200_000] if hasattr(cur, "text") else "", first_external_domain or (urlparse(cur_url).hostname or "").replace("www.", "")
except httpx.ConnectError:
return -1, "conn_refused", ""
except httpx.TimeoutException:
return -2, "timeout", ""
except Exception as e:
return -9, str(e)[:120], ""
async def resolve_domain(cli: httpx.AsyncClient, slug: str) -> str:
"""Pdude.link follow z early-exit dla first external."""
try:
r = await cli.get(f"https://pdude.link/{slug}", headers={"User-Agent": UA}, follow_redirects=False)
loc = r.headers.get("location", "")
if loc:
host = urlparse(loc).hostname or ""
host = host.replace("www.", "")
# Jeśli pdude.link redirectuje na affiliate (anexo.link/awejmp.com/etc), wyciągnij subAffId
if "anexo.link" in host or "awejmp.com" in host or "porndudecams" in host:
# Try slug.com fallback
return ""
return host
except Exception:
pass
return ""
def analyze_html(html: str) -> dict:
found = {}
for pattern, name in META_MARKERS:
if re.search(pattern, html, re.IGNORECASE):
found[name] = True
prefixes = set()
sample = []
for m in SCENE_PATH_RE.finditer(html):
link = m.group(1)
sample.append(link[:100])
# Wyciągnij prefix
# Normalize: //host/path → /path; otherwise full match
if link.startswith("//"):
link = "/" + link.split("/", 3)[3] if "/" in link[2:] else "/"
if link.startswith("/"):
parts = link.lstrip("/").split("/", 2)
if parts:
prefixes.add("/" + parts[0])
if len(sample) >= 5:
break
if prefixes:
found["scene_path_prefixes"] = sorted(prefixes)
if sample:
found["scene_link_samples"] = sample[:3]
return found
def score_findings(f: dict) -> tuple[float, list]:
score, reasons = 0.0, []
if f.get("jsonld_video"):
score += 1.5; reasons.append("jsonld_video")
if f.get("og_video"):
score += 0.5; reasons.append("og_video")
if f.get("video_card"):
score += 1; reasons.append("video_card")
if f.get("performer_marker"):
score += 1; reasons.append("performer_marker")
if f.get("studio_marker"):
score += 1; reasons.append("studio_marker")
if f.get("duration_marker"):
score += 0.5; reasons.append("duration_marker")
if f.get("hls_marker") or f.get("stream_url_marker"):
score += 0.5
if f.get("scene_path_prefixes"):
score += 1; reasons.append(f"paths={f['scene_path_prefixes']}")
if f.get("auth_wall"):
score -= 2; reasons.append("auth_wall")
if f.get("dead_404"):
score -= 5; reasons.append("dead_404")
return round(score, 1), reasons
async def main():
movies = json.loads(Path("theporndude_movies.json").read_text())["all"]
print(f"audyt {len(movies)} tubów z full-porn-movies-sites…")
timeout = httpx.Timeout(15.0, connect=8.0)
async with httpx.AsyncClient(timeout=timeout, http2=False) as cli:
sem = asyncio.Semaphore(12)
async def worker(r):
async with sem:
slug = r["slug"]
# Resolve real domain z pdude.link first hop
domain = await resolve_domain(cli, slug)
if not domain or any(x in domain for x in ["anexo.link", "awejmp.com", "porndudecams"]):
domain = f"{slug.lower()}.com"
# Curl root + scene path heurystyka
status, html, _ = await fetch_one(cli, f"https://{domain}/")
findings = analyze_html(html) if status == 200 else {}
score, reasons = score_findings(findings)
our = match(slug, domain)
return {
**r,
"domain": domain,
"root_status": status,
"findings": findings,
"score": score,
"reasons": reasons,
"our_origin": our,
}
results = await asyncio.gather(*[worker(r) for r in movies])
# Aggregate
have = [r for r in results if r["our_origin"]]
new_promising = [r for r in results if not r["our_origin"] and r["score"] >= 2.5]
new_low = [r for r in results if not r["our_origin"] and 1 <= r["score"] < 2.5]
new_zero = [r for r in results if not r["our_origin"] and 0 < r["score"] < 1]
new_dead = [r for r in results if not r["our_origin"] and (r["root_status"] <= 0 or r["score"] < 0)]
new_no_signal = [r for r in results if not r["our_origin"] and r["score"] == 0 and r["root_status"] == 200]
print(f"\n=== Coverage /full-porn-movies-sites ({len(results)} tubes) ===")
print(f" already have: {len(have):>3}")
print(f" promising: {len(new_promising):>3}")
print(f" low value: {len(new_low):>3}")
print(f" no signal: {len(new_no_signal):>3}")
print(f" dead: {len(new_dead):>3}")
print()
print("ALREADY HAVE:")
for r in have:
print(f" {r['slug']:<20} -> {r['our_origin']}")
print()
print("PROMISING (score >= 2.5):")
for r in sorted(new_promising, key=lambda x: -x["score"]):
print(f" score={r['score']:>4} {r['domain']:<25} ({r['slug']:<20}) reasons={','.join(r['reasons'])[:60]}")
print()
print("LOW VALUE (1-2.5):")
for r in sorted(new_low, key=lambda x: -x["score"]):
print(f" score={r['score']:>4} {r['domain']:<25} ({r['slug']:<20}) reasons={','.join(r['reasons'])[:60]}")
Path("theporndude_movies_scorecard.json").write_text(json.dumps(results, indent=2))
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,87 +0,0 @@
"""Per 166 review slugs z top-porn-tube-sites:
1. Fetch review page → extract pdude.link Visit URL + rating + score badges
2. Follow pdude.link → real tube domain
3. Cross-check vs nasze 25 tube origins
4. Output JSON: { slug, name, theporndude_rank, theporndude_score, real_domain, in_our_db, our_origin }
"""
import asyncio
import json
import re
from pathlib import Path
from urllib.parse import urlparse
import httpx
REVIEWS_FILE = Path("theporndude_free_tubes.json")
OUT_FILE = Path("theporndude_resolved.json")
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36"
async def fetch_review(cli: httpx.AsyncClient, review: dict, rank: int) -> dict:
url = f"https://theporndude.com/{review['id']}/{review['slug']}"
try:
r = await cli.get(url, headers={"User-Agent": UA})
html = r.text
except Exception as e:
return {**review, "rank": rank, "error": f"fetch_review: {e}"}
# Wyciągnij score
score_m = re.search(r'class="rate__num">\s*(\d+(?:\.\d+)?)\s*<', html)
# Wyciągnij pdude.link visit URL
pdude_m = re.search(r'href="(https://pdude\.link/[\w\-\.]+)"', html)
# Wyciągnij <title> + meta description
title_m = re.search(r"<title>([^<]+)</title>", html)
desc_m = re.search(r'<meta\s+name="description"\s+content="([^"]+)"', html)
out = {
**review,
"rank": rank,
"theporndude_score": float(score_m.group(1)) if score_m else None,
"page_title": (title_m.group(1) if title_m else "")[:120],
"page_desc": (desc_m.group(1) if desc_m else "")[:200],
}
if not pdude_m:
out["error"] = "no_pdude_link"
return out
pdude_url = pdude_m.group(1)
# Follow pdude.link
try:
r2 = await cli.get(pdude_url, headers={"User-Agent": UA})
# Final URL po wszystkich redirectach
final_url = str(r2.url)
host = urlparse(final_url).hostname or ""
host = host.replace("www.", "")
out["real_domain"] = host
out["final_url"] = final_url[:200]
except Exception as e:
out["error"] = f"pdude_follow: {e}"
return out
async def main():
reviews = json.loads(REVIEWS_FILE.read_text())["reviews"]
timeout = httpx.Timeout(20.0, connect=10.0)
limits = httpx.Limits(max_keepalive_connections=10, max_connections=20)
async with httpx.AsyncClient(
timeout=timeout, limits=limits, follow_redirects=True, http2=False
) as cli:
sem = asyncio.Semaphore(8)
async def worker(rev, rank):
async with sem:
return await fetch_review(cli, rev, rank)
tasks = [worker(r, i + 1) for i, r in enumerate(reviews)]
results = await asyncio.gather(*tasks)
OUT_FILE.write_text(json.dumps(results, indent=2))
ok = sum(1 for r in results if r.get("real_domain"))
print(f"resolved {ok}/{len(results)} ({ok*100/len(results):.0f}%)")
print(f"out -> {OUT_FILE}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,104 +0,0 @@
"""Generuje końcowy scorecard JSON dla wszystkich 166 theporndude top-porn-tube-sites:
- coverage status (already_have/new/dead/low_value)
- canonical_value_score 0-5 (heurystyka + nasz ranking)
- recommendation: skip / consider / pilot / integrate
Plus markdown summary dla człowieka.
"""
import json
from pathlib import Path
COVERAGE = json.loads(Path("theporndude_coverage.json").read_text())
TRIAGE = json.loads(Path("theporndude_triage.json").read_text())
def main():
triage_by_slug = {r["slug"]: r for r in TRIAGE}
scorecards = []
for r in COVERAGE["already_have"]:
scorecards.append({
"rank": r["rank"],
"slug": r["slug"],
"domain": r["domain"],
"status": "already_have",
"our_origin": r["our_origin"],
"canonical_value_score": None,
"recommendation": "skip — already integrated",
})
for r in COVERAGE["new_candidates"]:
t = triage_by_slug.get(r["slug"], {})
score = t.get("heuristic_score", 0)
findings = t.get("root_findings", {})
reasons = t.get("reasons", [])
root_status = t.get("root_status", 0)
domain = t.get("domain") or r.get("domain") or f"{r['slug']}.com"
if root_status <= 0 or findings.get("dead_404"):
status = "dead"
rec = "skip — dead/unreachable"
elif findings.get("auth_wall") and score < 2:
status = "auth_wall"
rec = "skip — login required, no public scenes"
elif score >= 2.5:
status = "promising"
rec = "pilot — deep audit + write extractor"
elif score >= 1:
status = "low_value"
rec = "consider — basic metadata only, low priority"
else:
status = "no_value"
rec = "skip — no canonical-fit signal in HTML"
scorecards.append({
"rank": r["rank"],
"slug": r["slug"],
"domain": domain,
"status": status,
"our_origin": None,
"canonical_value_score": score,
"heuristic_reasons": reasons,
"findings": findings,
"recommendation": rec,
})
scorecards.sort(key=lambda x: x["rank"])
out = {
"source": "theporndude.com/top-porn-tube-sites",
"fetched_at": "2026-05-20",
"total": len(scorecards),
"summary": {
"already_have": sum(1 for s in scorecards if s["status"] == "already_have"),
"promising": sum(1 for s in scorecards if s["status"] == "promising"),
"low_value": sum(1 for s in scorecards if s["status"] == "low_value"),
"no_value": sum(1 for s in scorecards if s["status"] == "no_value"),
"auth_wall": sum(1 for s in scorecards if s["status"] == "auth_wall"),
"dead": sum(1 for s in scorecards if s["status"] == "dead"),
},
"scorecards": scorecards,
}
Path("theporndude_scorecard.json").write_text(json.dumps(out, indent=2))
# Pretty print summary
print("=" * 70)
print(f"THEPORNDUDE.COM CANONICAL-FIT SCORECARD ({out['total']} tubes)")
print("=" * 70)
for k, v in out["summary"].items():
print(f" {k:<15} {v:>4} ({100*v/out['total']:.0f}%)")
print()
print("PROMISING (score >= 2.5) — pilot candidates:")
for s in scorecards:
if s["status"] == "promising":
r = ",".join(s.get("heuristic_reasons", []))[:60]
print(f" #{s['rank']:>3} score={s['canonical_value_score']:>4} {s['domain']:<25} ({s['slug']}) {r}")
print()
print("LOW_VALUE (1-2.5) — defer:")
for s in scorecards:
if s["status"] == "low_value":
r = ",".join(s.get("heuristic_reasons", []))[:50]
print(f" #{s['rank']:>3} score={s['canonical_value_score']:>4} {s['domain']:<25} ({s['slug']}) {r}")
if __name__ == "__main__":
main()

View file

@ -1,54 +0,0 @@
# Streamtape sample do testu (50 URL, najświeższy last_seen_at)
Otwórz w przeglądarce, zaznacz w komentarzu które działają. Wszystkie zostały sprobed dziś z VPS Hetzner + lokalnego Win residential — `Video not found`.
1. Naughty Weddings 7 — https://streamtape.com/e/OA43pkp3Q3IZbJZ
2. Creampie Lover — https://streamtape.com/e/PLXrxXQ6wgIbwj
3. La Sextape — https://streamtape.com/e/8WRDwZDR81Iooqg
4. Lost And Found — https://streamtape.com/e/lr9YKv62MeI7QXk
5. Let's Post It 7 — https://streamtape.com/e/xl2xJ8QRejCkO9y
6. Let's Post It 7 (alt) — https://streamtape.com/e/3oVm799lxDIdQoZ
7. Sirens 5 — https://streamtape.com/e/dZljQpaWvoFkX7D
8. He's In Charge 5 — https://streamtape.com/e/8w4drQAGPZTlDP
9. Deep Into Blu — https://streamtape.com/e/ykb6r92gv1C1lKW
10. Anal Savages 10 — https://streamtape.com/e/BBbJXMX08bU2mA
11. Home Sweet Home — https://streamtape.com/e/xmaLM93WaJcogA
12. All Or Nothing: A Hailey Rose Showcase — https://streamtape.com/e/vPVmAK7rYMCJ7D
13. The Horny Babysitter — https://streamtape.com/e/Kzpqv0eLg1TJAV
14. Temple Of Flesh 5 — https://streamtape.com/e/6bO0P2wKAmU9Xlw
15. Come Home — https://streamtape.com/e/Bekv3DrORGSyL2O
16. My Husband Likes To Watch — https://streamtape.com/e/vQrJ0GRjyzubmv
17. Second Chances — https://streamtape.com/e/8dpQy06AG3I23Q
18. Brazzers 20th Anniversary — https://streamtape.com/e/JWbj4GqqRVCe7V
19. My Friend's Hot Mom 110 — https://streamtape.com/e/DoDbYAYaDgiLgW
20. The Best of Forbidden Scenes 10 — https://streamtape.com/e/QXD7yPGo4qI0mJk
21. Blacks On Cougars 19 — https://streamtape.com/e/aRWmG7BajqFxx97
22. Horny Hotwife 5 — https://streamtape.com/e/0Z64qYQ9oWTbPDL
23. Stepfamilies Exposed — https://streamtape.com/e/M9zqorkA3LIm7DK
24. I Love My Hotwife's Big Ass — https://streamtape.com/e/oZ9ygBBVRLSJa7q
25. Money Talks 6 — https://streamtape.com/e/QWkgrZA1mOt0040
26. Tight European Beauties 4 — https://streamtape.com/e/mPaD1Z6mkOfXg8
27. The Hunger — https://streamtape.com/e/D4jZYAYedLTk7Ow
28. The Swing Life — https://streamtape.com/e/kP9JrooyYXfr29
29. My Younger Lover 2 — https://streamtape.com/e/wGVyPGjB7lfJka2
30. Rest In Pieces — https://streamtape.com/e/KgAWJD3BlWtDra
31. The Lady In Charge Likes Girls — https://streamtape.com/e/G37Mj3kkXYHZAJ
32. Internal Love 4 — https://streamtape.com/e/JoBGBwDxB6HjDWX
33. Nuru Fixes Everything 2 — https://streamtape.com/e/2o8Pv3okdKTAAM
34. Let's Post It 7 (alt2) — https://streamtape.com/e/Mk9verWlGoU0MB
35. DP Granny! — https://streamtape.com/e/K0gy9kdk8Gi08Qj
36. Love Stories 3 — https://streamtape.com/e/oZBBQyWRlatJ6dm
37. Anne A Taboo Parody — https://streamtape.com/e/wVbPWMgdxxhJ1QZ
38. Pornogeist — https://streamtape.com/e/JY8JyqW2Opcj0vz
39. All Stacked All Natural X-Cut 20 — https://streamtape.com/e/dOqK9x0vJzhkmZY
40. The Wedding — https://streamtape.com/e/94meOvYPQkSDy2
41. Swingers Getaway 4 — https://streamtape.com/e/8DR666jbdXcGMK
42. Not So Innocent 3 — https://streamtape.com/e/9lKYWv84glhyoX
43. That Time I Got My Stepmom Pregnant 2 — https://streamtape.com/e/7X4OByaDoGTRxD
44. Il Gladiatore XXX Parody — https://streamtape.com/e/7ovJBY9p4ZFBa3
45. Mission XXX Impossible — https://streamtape.com/e/gRQQ7DyW4wuqW4X
46. Squirt Game Part: 2 — https://streamtape.com/e/PZqBZp4OomF0Q61
47. Father Figure 4 — https://streamtape.com/e/PvZL0jzVK6I0pLG
48. Homeward Bound — https://streamtape.com/e/pagAXXDlP0Sr39B
49. Gold Diggers — https://streamtape.com/e/GMbxK2v6kXH1wJR
50. Erotic Threesomes — https://streamtape.com/e/Dz0Ae4AvmjFkyr3

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
{"catsWithContent":24,"totalSlugs":180,"cats":{"top-porn-tube-sites":{"title":"Free Porn Tube Sites","n":2,"slugs":["xvideos","cumlouder"]},"free-onlyfans-accounts":{"title":"Free OnlyFans Accounts","n":2,"slugs":["skylar-mae-free-onlyfans-review","luna-star-onlyfans-review-3"]},"tiktok-porn-sites":{"title":"TikTok Porn Sites","n":1,"slugs":["kwiky"]},"asian-porn-premium-sites":{"title":"Premium Asian Porn Sites","n":5,"slugs":["javhd","erito","japanhdv","asiansexdiary","zenra"]},"top-asian-sex-cams":{"title":"Live Asian Sex Cams","n":2,"slugs":["stripchatasian","livesexasian"]},"adult-vod-sites":{"title":"Adult VOD Sites","n":1,"slugs":["adulttime"]},"premium-nude-celebrities":{"title":"Premium Nude Celebrities","n":1,"slugs":["bannedsextapes"]},"ai-porn-sites":{"title":"AI Porn Sites","n":17,"slugs":["candyai","ourdream","gptgirlfriend-aipornsites-review","spicychat","playbox-aiporn-review","juicychatai-review","fapify-review","edenaiworld","camsodaai-review","undressappai","clothoff-review","trynectar","goloveai","pornworks","createporn","dondiai-review","createhottie-aipornsites-review"]},"nsfw-ai-fantasy-sites":{"title":"NSFW AI Fantasy Sites","n":16,"slugs":["playbox-undressai-review","fapify-undressaisites-review","clothoff","nudeai-review","xundress-review","createhottie-undress-review","nudiva","nudifyai-review","undressxxx-review","nudefab-review","fapwise-review","facyai","ainudesite","leakifyhub-review","nodress","aiundresscc-review"]},"best-premium-hentai-sites":{"title":"Premium Hentai Sites","n":3,"slugs":["hentaipros","hentaiedpro","createhentai"]},"top-sex-cam-sites":{"title":"Live Sex Cam Sites","n":28,"slugs":["camsoda","stripchat","sinparty","livejasmin","streamate","bongacams","jerkmate","cam4","myfreecams","flirtify-review","flirtbate","amateurtven","xlovecam","imlive","skyprivate","coomeetchat","xcams","sextpanther","sexytalktv-review","cams","flirt4free","sakuralive","slutroulette","luckycrush","babestation","xtease-review","xpanded","sospoiltcams"]},"ai-porn-generator-sites-reviews":{"title":"AI Porn Generator Sites","n":16,"slugs":["playbox-review","createhottie","fapify-aiporngenerators-review","sugargenbox-review","lusychat-generator-review","kissyvid-review","seducedai","dpnode-review","createporn","leakifyhub-aiporngenerators-review","pornworks-aigenerator-review","musebox-review","clothoff-aiporngenerator-review","trynectar-aiporngenerator-review","pornface-review","pornmakerai-aiporngenerators-review"]},"best-vr-porn-sites":{"title":"VR Porn Sites","n":5,"slugs":["vrporn","povrcom","vrbangers","sexlikereal","virtualrealporn"]},"premium-cartoon-porn-sites":{"title":"Premium Cartoon Porn Sites","n":2,"slugs":["animatria","adulttimeanime"]},"free-cam-girl-video-sites":{"title":"Free Cam Girl Video Sites","n":1,"slugs":["recurbate"]},"free-sex-cam-sites":{"title":"Free Sex Cam Sites","n":5,"slugs":["stripchat","sinparty","myfreecams","bongacams","camsoda"]},"best-adult-chat-sites":{"title":"Sex Chat Sites","n":10,"slugs":["coomeet","luckycrush","porndudegirls","flirtify-review","candyai","sextpanther","sexytalktv-review","mirami-review","flirtbees","stripchat-cam2cam-review"]},"ai-sex-chat-sites":{"title":"AI Sex Chat Sites","n":5,"slugs":["candyaisexchat-review","ourdream-review","gptgirlfriend","spicychat-aisexchat-review","lusychat-aisexchat-review"]},"top-premium-sites":{"title":"Top Premium Porn Sites","n":33,"slugs":["brazzersnetwork","bangbrosnetwork","spicevids","iknowthatgirl","letsdoeit","adulttime","faphouse","candyaipremium","hentaiedpro-premiumpornsites-review","fapify-premiumpornsites-review","teamskeet","mylf","puretaboo","naughtyamerica","xvideosred","gptgirlfriendpremium","mofosnetwork","evilangel","vixen","adultprime","pornbox","familystrokes","swappz","czechav-review","freeuse","realitykings","pornplus","vixenplus","pervz-review","nubilesporn","digitalplayground","julesjordan","blacked"]},"best-dating-sites":{"title":"Hookup Sites","n":5,"slugs":["adultfriendfinder","benaughty","ashleymadison","fling","fuckbook"]},"amateur-premium-sites":{"title":"Premium Amateur Porn Sites","n":5,"slugs":["exploitedcollegegirls","netvideogirls","trueamateurs","backroomcastingcouch","czechcasting-review"]},"best-porn-games":{"title":"Best Porn Games","n":10,"slugs":["nutaku","hentaiheroes","lustgoddess","pornstarharem","lifeselector","erolabs","kingofkinks","projectqt","bootycalls","kamihime"]},"betting-sites":{"title":"Betting Sites","n":4,"slugs":["betonline","betus","bcgame","slotocash"]},"best-escort-sites":{"title":"Escort Sites","n":1,"slugs":["slixa"]}}}

View file

@ -1,65 +0,0 @@
# Theporndude.com canonical-fit audit (2026-05-20)
## Scope
- Source: `theporndude.com/top-porn-tube-sites` (kategoria "Free Porn Tube Sites")
- Method: 166 review-page slugs → resolve real domain (pdude.link follow) →
coverage check vs naszych 25 origins + 4 REGISTRY → curl HTML triage 144 nowych
→ Chrome DevTools deep audit dla 4 z heurystycznym score ≥ 2.5
- Files generated:
- `theporndude_resolved.json` — 166 tubes z real domains
- `theporndude_coverage.json` — already-have vs new
- `theporndude_triage.json` — curl HTML heurystyka per-tube
- `theporndude_scorecard.json` — final per-tube scorecard
## Final coverage
| Status | N | % | |
|---|---|---|---|
| **Already have** | 22 | 13% | xvideos, xnxx, pornhub, xhamster, eporner, hqporner, porntrex, redtube, youporn, hdporn92 (DELISTED), pornhat, porn00, porndish, paradisehill, watchporn, xmoviesforyou, perverzija, latestpornvideo, latestleaks, mypornerleak, sxyland, porn4days |
| **Promising** (score≥2.5) | 4 | 2% | porndoe, fullporner, porn300, vrsmash |
| **Low value** (1-2.5) | 13 | 8% | tnaflix, hitprn, porntop, pornxp, fakings, justfullporn, empflix, taxi69, sexu, definebabe, hd-easyporn, bigbuttshub, zhornyhub |
| **No value** | 46 | 28% | Generic landing/JS-rendered, brak strukturalnych markerów |
| **Auth wall** | 46 | 28% | Login required / member-only / premium |
| **Dead** | 35 | 21% | 404, conn refused, timeout |
## Deep audit 4 promising
| Tube | Title | Performers | Studio | Date | Duration | Tags | JSON-LD | Verdict |
|---|---|---|---|---|---|---|---|---|
| **porndoe.com** | ✅ rich | ✅ 4 named | ⚠️ stub | ✅ "May 20, 2026" | ❓ | ✅ 10 named | ✅ @graph | **HIGH — pilot candidate** |
| fullporner.com | ✅ | ✅ 3 named | ❌ | ❌ | ❌ | ⚠️ 3 hashtags | ❌ | LOW — title+performer only |
| porn300.com | ⚠️ broken h1 | ⚠️ stub "Pornstars" | ❌ | ⚠️ "27:05" niejasne | ⚠️ 0:00 | ⚠️ 8 z headerem | ❌ | VERY LOW — anti-bot / broken DOM |
| vrsmash.com | ✅ rich | ⚠️ 1 named | ❌ | ❌ | ❌ | ✅ 10 VR genre | ⚠️ WebPage | LOW — VR niche, brak studio |
## Werdykt
**Z 166 top free tubes na theporndude.com, mamy realnie 1 verified pilot candidate (porndoe.com).**
### Dlaczego tak mało?
1. **High-value mainstream tubes mamy już wszystkie** (xvideos/xnxx/pornhub/xhamster/eporner/redtube/youporn etc.).
2. **Wiele theporndude rekomendacji to ad-network / premium / cam / dating** — mimo że są w kategorii "Free Tubes". Theporndude monetyzuje partnerships, więc featured ≠ canonical-friendly.
3. **Reszta to:**
- dead/expired domains (35) — theporndude updates pozostają w tyle
- auth wall (46) — paysite teasers, scenes wymagają konta
- no-metadata SEO shells (46) — popup ads, no scene metadata
- low value (13) — basic title+thumb, brak studio/performer/date
### Recommendation
**Don't expand sources from theporndude beyond `porndoe`** (pre-public bandwidth/anonymity priorytet — patrz [[goon_bandwidth_strategy]]). Marginalny ROI vs ryzyko nowych extractor-bugów.
### Optional next steps
- **porndoe pilot** (~2-3h dev): rich metadata + named performers + dates + 10 tags + JSON-LD. Najlepszy candidate w całej liście. Warto napisać extractor jeśli kiedyś będzie potrzeba expansion.
- Skip pozostałe — niski ROI vs effort.
## Surowe pliki
```
theporndude_resolved.json — 166 entries z real domains
theporndude_coverage.json — split already-have/new
theporndude_triage.json — curl HTML heurystyka per-tube
theporndude_scorecard.json — final per-tube scorecard z status+recommendation
```

File diff suppressed because one or more lines are too long

View file

@ -1,92 +0,0 @@
# Theporndude.com FULL audit — 2 kategorie (2026-05-20)
## Scope
| Kategoria | URL | Tubes |
|---|---|---|
| Free Porn Tube Sites | `/top-porn-tube-sites` | 166 |
| Free Porn Video Sites | `/full-porn-movies-sites` | 94 |
| **Unique total** | — | **172** |
| Overlap między kategoriami | — | 88 |
## Coverage combined
| Status | N | % | |
|---|---|---|---|
| Already have | 26 | 15% | mainstream + REGISTRY tubes (xvideos, xnxx, pornhub, hqporner, porntrex, eporner, xmoviesforyou, paradisehill, watchporn, latestpornvideo, latestleaks, mypornerleak, sxyland, perverzija, porndish, porn00, porn4days, pornhat, redtube, youporn, xhamster, hdporn92, watchxxxfree...) |
| **Promising (verified ≥2.5)** | **1** | **0.6%** | **porndoe.com** |
| Low value (1-2.5) | 13 | 8% | tnaflix, pornxp, sexu, fakings, taxi69, definebabe, hd-easyporn, bigbuttshub, zhornyhub, hitprn, porntop, empflix, justfullporn, pornhd3x, veporn |
| No value / auth wall / dead | 132 | 77% | — |
## porndoe.com deep audit verdict
**Status: ❤️ HIGH-VALUE metadata-only source**
### Co ma w JSON-LD VideoObject (per scene)
```json
{
"@type": "VideoObject",
"name": "Alexx Bbc Craving Asian",
"uploadDate": "2026-05-20T14:55:13+00:00",
"duration": "T8M0S",
"thumbnailUrl": "https://p.cdnc.porndoe.com/image/movie/...",
"contentUrl": "https://porndoe.com/watch/pd7a3o0e8v2b",
"producer": {"@type": "Organization", "name": "Fantasy Girl Pass",
"url": "https://porndoe.com/channel-profile/fantasy-girl-pass"},
"publisher": {"@type": "Organization", "name": "Fantasy Girl Pass", ...},
"actor": [{"@type": "Person", "name": "Alexx Zen",
"url": "https://porndoe.com/pornstars-profile/alexx-zen"}],
"description": "Alexx Bbc Craving Asian"
}
```
### Co potwierdza wartość
- ✅ **Schema.org compliant** (JSON-LD VideoObject)
- ✅ **Named studios** ("Fantasy Girl Pass", "Japan Lust" etc.) — cross-matchowalne z TPDB
- ✅ **Named performers** ("Alexx Zen") z stable URL `/pornstars-profile/<slug>`
- ✅ **Precyzyjny timestamp** uploadDate (ISO 8601)
- ✅ **Duration ISO 8601** (parsowanie trywialne)
- ✅ **Stable scene URL pattern**: `/watch/<8-12 char id>`
- ✅ **Listing endpoint**: `/videos/most-recent?page=N` (paginacja działa)
- ⚠️ Stream URL: NIE inline w SSR HTML (player JS init po Play click) — trzeba follow embed lub player config
### Recommendation porndoe
**Dodaj jako metadata-only source (jak TPDB/StashDB)**, NIE jako playable tube.
| Aspekt | Verdict |
|---|---|
| **Canonical match** | ✅ HIGH — JSON-LD daje wszystko |
| **Bandwidth cost** | ✅ LOW — tylko HTML JSON-LD scrape (~50KB/page, ~30 scenes = ~1.7KB/scene) |
| **VPS anonimowość** | ✅ Bezpieczne — request standardowy curl/httpx |
| **Playback** | ❌ NIE — stream JS-rendered po click, lepiej skip |
| **Effort** | ~3-4h dev: nowy connector `app/connectors/porndoe.py` (paginacja + JSON-LD parser) + scene/performer/studio mapping |
**Value-add do bazy:** porndoe agreguje hundreds of named studios (Fantasy Girl Pass, Japan Lust, etc.). Może wypełnić luki TPDB dla mniejszych studios + amateur/niche content.
## /full-porn-movies-sites werdykt
**Brutto 0 nowych high-value kandydatów.** Z 94 tubes:
- 88 overlap z `/top-porn-tube-sites` (już zaaudytowane)
- 6 unique → wszyscy 6 redirectują pdude.link → theporndude.com (delisted/dead)
## Final recommendation
| Priorytet | Akcja |
|---|---|
| **(opcjonalne) Pilot porndoe** | Napisz `app/connectors/porndoe.py` — paginacja `/videos/most-recent?page=N` + JSON-LD parser → SceneDTO. **Metadata-only**, weight=0.6. Idealny dla orphan TPDB scenes. ~3-4h dev. |
| **Skip reszta theporndude** | Marginalny ROI. Mainstream już mamy, reszta to ad-network/dead/auth-wall. |
| **(pre-public)** | Zostań w obecnym scope. Bandwidth/anonymity > expansion. |
## Files
```
theporndude_resolved.json — 166 z top-porn-tubes
theporndude_movies.json — 94 z full-porn-movies-sites
theporndude_coverage.json — top-porn-tubes split have/new
theporndude_triage.json — curl heurystyka per-tube
theporndude_scorecard.json — final scorecard top-porn-tubes
theporndude_movies_scorecard.json — final scorecard movies
theporndude_final_report.md — raport top-porn-tubes
theporndude_full_audit_report.md — TEN raport (combined)
```

View file

@ -1 +0,0 @@
{"h1":"Free Porn Videos & XXX Sex Movies - Reviews","n":94,"all":[{"id":"1514","slug":"yourporn"},{"id":"2356","slug":"hqporner"},{"id":"6138","slug":"porntrex"},{"id":"6157","slug":"eporner"},{"id":"8849","slug":"pornhoarder"},{"id":"8746","slug":"yespornvip"},{"id":"3192","slug":"xmoviesforyou"},{"id":"6248","slug":"pornhits"},{"id":"3228","slug":"porn00"},{"id":"1761","slug":"pornhd3x"},{"id":"4740","slug":"xxxfiles"},{"id":"4423","slug":"porngo"},{"id":"2378","slug":"pornktube"},{"id":"1813","slug":"pornovideoshub"},{"id":"5065","slug":"brazzers3x"},{"id":"3517","slug":"paradisehill"},{"id":"3380","slug":"freeomovie"},{"id":"1423","slug":"xtapes"},{"id":"1736","slug":"porndish"},{"id":"7936","slug":"fullporner"},{"id":"6209","slug":"trendyporn"},{"id":"2507","slug":"pornhd8k"},{"id":"3415","slug":"xfreehd"},{"id":"21367","slug":"pornslash"},{"id":"1875","slug":"pornobae"},{"id":"2801","slug":"netfapx"},{"id":"2684","slug":"joysporn"},{"id":"5111","slug":"pornmz"},{"id":"7191","slug":"xmegadrive"},{"id":"2328","slug":"porn4days"},{"id":"5969","slug":"justfullporn"},{"id":"1412","slug":"watchxxxfree"},{"id":"6185","slug":"hdporn92"},{"id":"6311","slug":"yespornpleasexxx"},{"id":"7432","slug":"hitprn"},{"id":"5232","slug":"czechvideo"},{"id":"9062","slug":"fuxnxx"},{"id":"8610","slug":"watchporn"},{"id":"8281","slug":"latestpornvideo"},{"id":"7374","slug":"pornxp"},{"id":"16954","slug":"latestleaks"},{"id":"1528","slug":"pandamovie"},{"id":"16957","slug":"bananamovies"},{"id":"2884","slug":"letsjerk"},{"id":"3388","slug":"streamporn"},{"id":"1796","slug":"fullxxxmovies"},{"id":"1748","slug":"xxvideoss"},{"id":"7778","slug":"iceporncasting"},{"id":"1757","slug":"pornvibe"},{"id":"3401","slug":"mangoporn"},{"id":"4541","slug":"swingerpornfun"},{"id":"6206","slug":"perverzija"},{"id":"13692","slug":"pornx"},{"id":"3549","slug":"siska"},{"id":"1429","slug":"yourdailypornvideos"},{"id":"9060","slug":"whereismyporn"},{"id":"17940","slug":"allpornstream"},{"id":"8384","slug":"thepornarea"},{"id":"9885","slug":"pornvideobb"},{"id":"9057","slug":"teenagepornx"},{"id":"29266","slug":"speedporn-review"},{"id":"9107","slug":"pornoflix"},{"id":"15889","slug":"neporn"},{"id":"11245","slug":"pornezcam"},{"id":"10281","slug":"americass"},{"id":"10411","slug":"sextu"},{"id":"10954","slug":"galaxyporn"},{"id":"11698","slug":"pornseed"},{"id":"17542","slug":"ogporn"},{"id":"15967","slug":"bingato"},{"id":"18998","slug":"bibamax"},{"id":"13362","slug":"xfuntaxy"},{"id":"19323","slug":"xvgold"},{"id":"14555","slug":"brazz"},{"id":"14784","slug":"bigbuttshub"},{"id":"15284","slug":"cheemsporno"},{"id":"17485","slug":"mypornerleak"},{"id":"17546","slug":"coralxxx"},{"id":"24262","slug":"youperv-website-review"},{"id":"18347","slug":"porneec"},{"id":"18171","slug":"freepornsex"},{"id":"18338","slug":"amigosporn"},{"id":"18315","slug":"vepornnet"},{"id":"1548","slug":"veporn"},{"id":"20299","slug":"xleech"},{"id":"20745","slug":"sxyland"},{"id":"21910","slug":"brazzpw-review"},{"id":"21358","slug":"zhornyhub"},{"id":"24377","slug":"pornhd4k-website-review"},{"id":"24397","slug":"pornbusy-website-review"},{"id":"24698","slug":"8kporner-website-review"},{"id":"25496","slug":"saintporn-website-review"},{"id":"27856","slug":"4k69-review"},{"id":"28502","slug":"jerkmotion-review"}]}

View file

@ -1,954 +0,0 @@
[
{
"id": "1514",
"slug": "yourporn",
"domain": "yourporn.com",
"root_status": -1,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2356",
"slug": "hqporner",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:hqpornercom"
},
{
"id": "6138",
"slug": "porntrex",
"domain": "porntrex.com",
"root_status": -2,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:porntrexcom"
},
{
"id": "6157",
"slug": "eporner",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:epornercom"
},
{
"id": "8849",
"slug": "pornhoarder",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "8746",
"slug": "yespornvip",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3192",
"slug": "xmoviesforyou",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:xmoviesforyoucom"
},
{
"id": "6248",
"slug": "pornhits",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3228",
"slug": "porn00",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:porn00org"
},
{
"id": "1761",
"slug": "pornhd3x",
"domain": "pornhd3x.com",
"root_status": 200,
"findings": {
"video_card": true
},
"score": 1.0,
"reasons": [
"video_card"
],
"our_origin": null
},
{
"id": "4740",
"slug": "xxxfiles",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "4423",
"slug": "porngo",
"domain": "my.cams.bar",
"root_status": -1,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2378",
"slug": "pornktube",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1813",
"slug": "pornovideoshub",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "5065",
"slug": "brazzers3x",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3517",
"slug": "paradisehill",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:paradisehillcc"
},
{
"id": "3380",
"slug": "freeomovie",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1423",
"slug": "xtapes",
"domain": "xtapes.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1736",
"slug": "porndish",
"domain": "porndish.com",
"root_status": 403,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:porndishcom"
},
{
"id": "7936",
"slug": "fullporner",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "6209",
"slug": "trendyporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2507",
"slug": "pornhd8k",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3415",
"slug": "xfreehd",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "21367",
"slug": "pornslash",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1875",
"slug": "pornobae",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2801",
"slug": "netfapx",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2684",
"slug": "joysporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "5111",
"slug": "pornmz",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "7191",
"slug": "xmegadrive",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2328",
"slug": "porn4days",
"domain": "porn4days.com",
"root_status": 200,
"findings": {
"studio_marker": true
},
"score": 1.0,
"reasons": [
"studio_marker"
],
"our_origin": "tube:porn4dayspw"
},
{
"id": "5969",
"slug": "justfullporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1412",
"slug": "watchxxxfree",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "6185",
"slug": "hdporn92",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:hdporn92com"
},
{
"id": "6311",
"slug": "yespornpleasexxx",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "7432",
"slug": "hitprn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "5232",
"slug": "czechvideo",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "9062",
"slug": "fuxnxx",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "8610",
"slug": "watchporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:watchporn"
},
{
"id": "8281",
"slug": "latestpornvideo",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:latestpornvideocom"
},
{
"id": "7374",
"slug": "pornxp",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "16954",
"slug": "latestleaks",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:latestleaksco"
},
{
"id": "1528",
"slug": "pandamovie",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "16957",
"slug": "bananamovies",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "2884",
"slug": "letsjerk",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3388",
"slug": "streamporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1796",
"slug": "fullxxxmovies",
"domain": "fullxxxmovies.com",
"root_status": -1,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1748",
"slug": "xxvideoss",
"domain": "xxvideoss.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "7778",
"slug": "iceporncasting",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1757",
"slug": "pornvibe",
"domain": "pornvibe.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3401",
"slug": "mangoporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "4541",
"slug": "swingerpornfun",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "6206",
"slug": "perverzija",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:perverzijacom"
},
{
"id": "13692",
"slug": "pornx",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "3549",
"slug": "siska",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1429",
"slug": "yourdailypornvideos",
"domain": "yourdailypornvideos.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "9060",
"slug": "whereismyporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "17940",
"slug": "allpornstream",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "8384",
"slug": "thepornarea",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "9885",
"slug": "pornvideobb",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "9057",
"slug": "teenagepornx",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "29266",
"slug": "speedporn-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "9107",
"slug": "pornoflix",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "15889",
"slug": "neporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "11245",
"slug": "pornezcam",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "10281",
"slug": "americass",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "10411",
"slug": "sextu",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "10954",
"slug": "galaxyporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "11698",
"slug": "pornseed",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "17542",
"slug": "ogporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "15967",
"slug": "bingato",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "18998",
"slug": "bibamax",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "13362",
"slug": "xfuntaxy",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "19323",
"slug": "xvgold",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "14555",
"slug": "brazz",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "14784",
"slug": "bigbuttshub",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "15284",
"slug": "cheemsporno",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "17485",
"slug": "mypornerleak",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:mypornerleakcom"
},
{
"id": "17546",
"slug": "coralxxx",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "24262",
"slug": "youperv-website-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "18347",
"slug": "porneec",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "18171",
"slug": "freepornsex",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "18338",
"slug": "amigosporn",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "18315",
"slug": "vepornnet",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "1548",
"slug": "veporn",
"domain": "veporn.com",
"root_status": 200,
"findings": {
"performer_marker": true
},
"score": 1.0,
"reasons": [
"performer_marker"
],
"our_origin": null
},
{
"id": "20299",
"slug": "xleech",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "20745",
"slug": "sxyland",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": "tube:sxylandcom"
},
{
"id": "21910",
"slug": "brazzpw-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "21358",
"slug": "zhornyhub",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "24377",
"slug": "pornhd4k-website-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "24397",
"slug": "pornbusy-website-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "24698",
"slug": "8kporner-website-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "25496",
"slug": "saintporn-website-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "27856",
"slug": "4k69-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
},
{
"id": "28502",
"slug": "jerkmotion-review",
"domain": "theporndude.com",
"root_status": 200,
"findings": {},
"score": 0.0,
"reasons": [],
"our_origin": null
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff