fix(feedback): freeomovie posters + dead hoster + myvidplay; watchporn _embed_iframe guard

From user bug reports (8f8c10c0 freeomovie, c156e4b7/8db71220 watchporn scene):

freeomovie (app/connectors/freeomovie.py):
- Parse the detail-page poster (<img class="rmbd">); RawMovie was built with no
  poster_url, so title-only orphans (no canonical match) rendered blank cards. Re-ingest
  backfilled existing blanks 11 -> 1.
- _host_label now returns the registrable domain (parts[-2]) not the subdomain, so
  video.player4me.xyz labels as 'player4me' (was garbage 'freeomovie:video').
- Skip player4me (JS-SPA, no known resolver); pruned 28 dead freeomovie:video sources.
- myvidplay.com added to mobile DOOD_HOSTS (doodstream.ts): it 301s to playmogo.com
  (doodcdn clone), so it now routes to the native dood resolver instead of a dead WebView.

watchporn: the reported 4 duplicate/broken quality rows were the API process serving a
STALE extractor registry (never restarted after the re-enable deploy) -> _embed_iframe
scraped 2 tokenless get_file (403) + 2 preview trailers (404). The running api now serves
the native extractor (2 clean playing links, verified). Hardened _embed_iframe to skip
preview/videos_screenshots clips and tokenless get_file so this class can't recur.

Also: DonateScreen em-dashes -> commas/semicolon (no-em-dash rule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-07-03 13:02:47 +02:00
parent 393dbe548a
commit 0c18472a15
5 changed files with 38 additions and 6 deletions

View file

@ -50,9 +50,16 @@ _LIST_ITEM_RE = re.compile(
) )
_TABS_RE = re.compile(r"var\s+TABS\s*=\s*(\[.*?\])\s*;", re.DOTALL) _TABS_RE = re.compile(r"var\s+TABS\s*=\s*(\[.*?\])\s*;", re.DOTALL)
_ARTICLE_SECTION_RE = re.compile(r'"articleSection"\s*:\s*"([^"]+)"') _ARTICLE_SECTION_RE = re.compile(r'"articleSection"\s*:\s*"([^"]+)"')
# Poster: <img class="... rmbd ..." src="..."> (fastpic.org itp.). Bez tego film-orphan
# (tytuł bez matchu do canonical) miał poster_url=None → pusty kafelek (report 8f8c10c0).
_POSTER_RE = re.compile(
r'<img[^>]+class="[^"]*\brmbd\b[^"]*"[^>]+src="(https?://[^"]+)"',
re.IGNORECASE,
)
# Hostery pomijane (martwe/flaky). # Hostery pomijane (martwe/flaky). player4me = JS-SPA bez znanego resolvera (28 martwych
_SKIP_HOSTS = ("streamtape", "mxdrop", "mixdrop", "streamsb") # źródeł freeomovie:video, report 8f8c10c0).
_SKIP_HOSTS = ("streamtape", "mxdrop", "mixdrop", "streamsb", "player4me")
def _host_label(embed_url: str) -> str | None: def _host_label(embed_url: str) -> str | None:
@ -60,7 +67,9 @@ def _host_label(embed_url: str) -> str | None:
if not host: if not host:
return None return None
parts = host.replace("www.", "").split(".") parts = host.replace("www.", "").split(".")
return parts[0] if parts else None # Rejestrowalna domena (SLD), nie subdomena: video.player4me.xyz → 'player4me' (nie
# 'video'), myvidplay.com → 'myvidplay'. Inaczej origin był śmieciowy (freeomovie:video).
return parts[-2] if len(parts) >= 2 else parts[0]
class FreeoMovieConnector(BaseMovieConnector): class FreeoMovieConnector(BaseMovieConnector):
@ -177,11 +186,15 @@ class FreeoMovieConnector(BaseMovieConnector):
if not playback: if not playback:
return None # bez playbacku film jest bezużyteczny (nie tworzymy orphana) return None # bez playbacku film jest bezużyteczny (nie tworzymy orphana)
pm = _POSTER_RE.search(detail)
poster_url = html.unescape(pm.group(1)) if pm else None
slug = urlparse(url).path.strip("/").split("/")[-1] slug = urlparse(url).path.strip("/").split("/")[-1]
return RawMovie( return RawMovie(
external_id=slug, external_id=slug,
title=title, title=title,
url=url, url=url,
poster_url=poster_url,
tags=tags, tags=tags,
playback_sources=playback, playback_sources=playback,
raw={"source": "freeomovie", "url": url}, raw={"source": "freeomovie", "url": url},

View file

@ -283,6 +283,15 @@ def _extract_direct_stream_urls(page_html: str, page_url: str) -> list[StreamSou
# i embed pages z .mp4 suffix (playmogo/dood /d/ — to HTML, nie video). # i embed pages z .mp4 suffix (playmogo/dood /d/ — to HTML, nie video).
if _NOT_DIRECT_STREAM_RE.search(url): if _NOT_DIRECT_STREAM_RE.search(url):
continue continue
# KVS false-positives (report c156e4b7/8db71220, watchporn przez _embed_iframe):
# preview_/videos_screenshots to krótkie trailery (404), a surowy get_file/ bez
# podpisanego query-tokena daje 403 — kt_player dokłada token z license_code (patrz
# natywny watchporn.extract). Żadne z nich to pełne wideo → pomijamy.
low = url.lower()
if "videos_screenshots/" in low or "/preview_" in low:
continue
if "/get_file/" in low and "?" not in url:
continue
# Quality wykrycie z nazwy pliku # Quality wykrycie z nazwy pliku
q_int = 0 q_int = 0
q_label = "mp4" q_label = "mp4"

View file

@ -16,6 +16,13 @@ export type ChangelogEntry = {
}; };
export const CHANGELOG: ChangelogEntry[] = [ export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-07-03',
date: 'July 2026',
items: [
'Movie posters show up again (freeomovie), and more of its sources actually play (myvidplay).',
],
},
{ {
id: '2026-07-02b', id: '2026-07-02b',
date: 'July 2026', date: 'July 2026',

View file

@ -38,6 +38,9 @@ const DOOD_HOSTS = new Set([
'ds2play.com', 'ds2video.com', 'ds2play.com', 'ds2video.com',
'd000d.com', 'd0o0d.com', 'do0od.com', 'do7go.com', 'd000d.com', 'd0o0d.com', 'do0od.com', 'do7go.com',
'dooood.com', 'd0000d.com', 'dooood.com', 'd0000d.com',
// myvidplay.com: 301 -> playmogo.com (doodcdn clone). Najczęstszy hoster freeomovie
// (63 filmy); bez tego isDoodStream() false -> WebView -> nie gra (report 8f8c10c0).
'myvidplay.com',
]); ]);
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';

View file

@ -33,12 +33,12 @@ export function DonateScreen() {
<Text style={styles.footnoteTitle}>Why crypto?</Text> <Text style={styles.footnoteTitle}>Why crypto?</Text>
<Text style={styles.footnoteBody}> <Text style={styles.footnoteBody}>
Mainstream payment processors (Stripe, PayPal, Patreon) refuse adult Mainstream payment processors (Stripe, PayPal, Patreon) refuse adult
projects even open-source tooling like this one. Crypto is the only projects, even open-source tooling like this one. Crypto is the only
channel that does not require us to identify our donors and does not channel that does not require us to identify our donors and does not
let third parties freeze the project mid-month. let third parties freeze the project mid-month.
</Text> </Text>
<Text style={[styles.footnoteBody, { marginTop: 10 }]}> <Text style={[styles.footnoteBody, { marginTop: 10 }]}>
Monero is privacy-preserving by design if you care about not being Monero is privacy-preserving by design; if you care about not being
linked to this app on-chain, send XMR. Bitcoin and USDT are public linked to this app on-chain, send XMR. Bitcoin and USDT are public
ledgers; use a fresh wallet if anonymity matters to you. ledgers; use a fresh wallet if anonymity matters to you.
</Text> </Text>
@ -83,7 +83,7 @@ function CoinCard({ coin }: { coin: DonateCoin }) {
{placeholder ? ( {placeholder ? (
<View style={styles.placeholderBox}> <View style={styles.placeholderBox}>
<Text style={styles.placeholderText}> <Text style={styles.placeholderText}>
Address not configured yet see{' '} Address not configured yet, see{' '}
<Text style={{ color: theme.muted, fontWeight: '700' }}>mobile/src/lib/donate.ts</Text> <Text style={{ color: theme.muted, fontWeight: '700' }}>mobile/src/lib/donate.ts</Text>
</Text> </Text>
</View> </View>