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 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. 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 from __future__ import annotations
import uuid import uuid
from sqlalchemy import ForeignKey, Integer, String from sqlalchemy import Integer, String
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@ -19,7 +20,5 @@ class TagVerticalCount(Base):
__tablename__ = "tag_vertical_counts" __tablename__ = "tag_vertical_counts"
vertical: Mapped[str] = mapped_column(String(32), primary_key=True) vertical: Mapped[str] = mapped_column(String(32), primary_key=True)
tag_id: Mapped[uuid.UUID] = mapped_column( tag_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
UUID(as_uuid=True), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True
)
scene_count: Mapped[int] = mapped_column(Integer, nullable=False) 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 }) => ( renderItem={({ item, index }) => (
<Pressable style={styles.tile} onPress={() => setPlaying(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 <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} style={StyleSheet.absoluteFill}
contentFit="cover" contentFit="cover"
transition={100} 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