Can we port Vinovo extractor #585

Closed
opened 2025-12-16 15:36:04 +00:00 by GLlgGL · 2 comments
GLlgGL commented 2025-12-16 15:36:04 +00:00 (Migrated from github.com)

Hi

I was trying to make the Vinovo extractor work(via mediaflow) and I am able to get the final url but is not playable...even on the same server where it was extracted...instead if I get the final link from browser and try it on same IP with curl it works lol

https://vinovo.to/e/r90rer94a7jy1

Addon
`import { NotFoundError } from '../error';
import { Context, Format, Meta, UrlResult } from '../types';
import { Extractor } from './Extractor';
import {
buildMediaFlowProxyExtractorStreamUrl,
supportsMediaFlowProxy,
} from '../utils';
import * as cheerio from 'cheerio';

const FIREFOX_UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
'AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/143.0.0.0 Safari/537.36';

export class Vinovo extends Extractor {
public readonly id = 'Vinovo';
public readonly label = 'Vinovo (MFP)';
public override readonly ttl = 10800000; // 3h
public override viaMediaFlowProxy = true;

private domains = ['vinovo.to', 'vinovo.si'];

public supports(ctx: Context, url: URL): boolean {
return (
this.domains.some(d => url.host.includes(d)) &&
supportsMediaFlowProxy(ctx)
);
}

// Normalize to embed URL
public override normalize(url: URL): URL {
// Accept /d/ or /e/, always normalize to /e/
return new URL(url.href.replace(//[de]//, '/e/'));
}

protected async extractInternal(
ctx: Context,
url: URL,
meta: Meta
): Promise<UrlResult[]> {
//const referer = meta.referer ?? url.href;

const headers: Record<string, string> = {

'User-Agent': FIREFOX_UA,
Referer: 'https://vinovo.to/',

};
;

// --- 1. Fetch embed page (existence check only) ---
let embedHtml: string;
try {
  embedHtml = await this.fetcher.text(ctx, url, headers);
} catch {
  // Fail fast so StreamResolver skips this source
  throw new NotFoundError('Vinovo: embed not responding');
}

if (!embedHtml) throw new NotFoundError();

// --- Detect removed / invalid video ---
if (
  embedHtml.includes('File Not Found') ||
  embedHtml.includes('Removed') ||
  embedHtml.includes('Not Found')
) {
  throw new NotFoundError('Vinovo: video removed');
}

// Optional: extract title (best-effort only)
let title = this.label;
try {
  const $ = cheerio.load(embedHtml);
  title =
    $('title')
      .text()
      .replace(/^Watch\s+/i, '')
      .trim() || this.label;
} catch {
  // ignore
}

// --- 2. MediaFlow proxy ---
const proxiedUrl = await buildMediaFlowProxyExtractorStreamUrl(
  ctx,
  this.fetcher,
  this.id,
  url,
  headers
);

return [
  {
    url: proxiedUrl,
    format: Format.mp4,
    label: this.label,
    sourceId: `${this.id}_${meta.countryCodes?.join('_') ?? 'all'}`,
    ttl: this.ttl,
    requestHeaders: headers,
    meta: {
      ...meta,
      title,
    },
  },
];

}
}`

Mediaflow
import re
import json
from typing import Dict, Any
from urllib.parse import urlparse

from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError
from mediaflow_proxy.utils.girc import girc

FIREFOX_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/143.0.0.0 Safari/537.36"
)

class VinovoExtractor(BaseExtractor):
"""
Vinovo extractor for MediaFlow Proxy

✔ MP4 stream (NOT HLS)
✔ Matches Kodi ResolveURL
✔ Correct reCAPTCHA handling
✔ Correct CDN headers (prevents 403)
"""

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.mediaflow_endpoint = "proxy_stream_endpoint"

async def extract(self, url: str) -> Dict[str, Any]:
    referer = self.base_headers.get("Referer")
    if not referer:
        parsed = urlparse(url)
        referer = f"{parsed.scheme}://{parsed.netloc}/"
    parsed = urlparse(url)

    if not parsed.hostname or "vinovo" not in parsed.hostname:
        raise ExtractorError("VINOVO: Invalid domain")

    # Normalize /d/ → /e/
    url = re.sub(r"/[dD]/", "/e/", url)

    # -------------------------------------------------
    # Incoming headers (from MediaFlow)
    # -------------------------------------------------
    incoming = self.base_headers or {}

    embed_referer = incoming.get("referer")
    if not embed_referer:
        raise ExtractorError("VINOVO: Missing embed referer")

    user_agent = incoming.get("user-agent", FIREFOX_UA)

    vinovo_origin = f"{parsed.scheme}://{parsed.hostname}/"

    # -------------------------------------------------
    # 1) Fetch embed page (USES EMBED REFERER)
    # -------------------------------------------------
    page_headers = {
        "User-Agent": user_agent,
        "Referer": embed_referer,
    }

    response = await self._make_request(url, headers=page_headers)
    html = response.text or ""

    if not html:
        raise ExtractorError("VINOVO: Empty embed page")

    cookies = response.cookies or {}

    # -------------------------------------------------
    # 2) Extract token + base URL
    # -------------------------------------------------
    token_match = re.search(r'name="token"\s*content="([^"]+)', html)
    base_match = re.search(r'<video[^>]+data-base="([^"]+)', html)

    if not token_match or not base_match:
        raise ExtractorError("VINOVO: Token or base missing")

    token = token_match.group(1)
    base = base_match.group(1)

    # -------------------------------------------------
    # 3) Solve reCAPTCHA (MUST USE VINOVO ORIGIN)
    # -------------------------------------------------
    recaptcha = await girc(
        self._make_request,
        html,
        vinovo_origin,   # 🔑 CRITICAL
    )

    if not recaptcha:
        raise ExtractorError("VINOVO: reCAPTCHA failed")

    # -------------------------------------------------
    # 4) Call Vinovo API
    # -------------------------------------------------
    media_id = self._extract_id(url)
    api_url = f"https://{parsed.hostname}/api/file/url/{media_id}"

    api_headers = {
        "User-Agent": user_agent,
        "Referer": vinovo_origin,
        "Origin": vinovo_origin.rstrip("/"),
        "X-Requested-With": "XMLHttpRequest",
    }

    if cookies:
        api_headers["Cookie"] = "; ".join(
            f"{k}={v}" for k, v in cookies.items()
        )

    api_resp = await self._make_request(
        api_url,
        method="POST",
        headers=api_headers,
        data={
            "token": token,
            "recaptcha": recaptcha,
        },
    )

    if not api_resp.text:
        raise ExtractorError("VINOVO: Empty API response")

    try:
        api_json = json.loads(api_resp.text)
    except Exception:
        raise ExtractorError("VINOVO: Invalid API JSON")

    if api_json.get("status") != "ok":
        raise ExtractorError(f"VINOVO: API rejected {api_json}")

    # -------------------------------------------------
    # 5) Final MP4 URL + CDN headers
    # -------------------------------------------------
    final_url = f"{base}/stream/{api_json['token']}"


    
    #self.base_headers["origin"] = "https://vinovo.to"
    #self.base_headers["range"] = "bytes=0-"

    self.base_headers.update({
        "origin": "https://vinovo.to",
        "range": "bytes=0-",
        "accept": "*/*",
        "accept-encoding": "identity",
        "host": urlparse(base).hostname,
        # 🔑 CRITICAL — request fingerprint
        "sec-fetch-site": "cross-site",
        "sec-fetch-mode": "cors",
        "sec-fetch-dest": "video",
    })
    return {
        "destination_url": final_url,
        "request_headers": self.base_headers,
        "mediaflow_endpoint": self.mediaflow_endpoint,
    }

def _extract_id(self, url: str) -> str:
    match = re.search(r"/(?:e|d)/([0-9a-zA-Z]+)", url)
    if not match:
        raise ExtractorError("VINOVO: Invalid URL format")
    return match.group(1)

What could it be...

Hi I was trying to make the Vinovo extractor work(via mediaflow) and I am able to get the final url but is not playable...even on the same server where it was extracted...instead if I get the final link from browser and try it on same IP with curl it works lol `https://vinovo.to/e/r90rer94a7jy1` Addon `import { NotFoundError } from '../error'; import { Context, Format, Meta, UrlResult } from '../types'; import { Extractor } from './Extractor'; import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy, } from '../utils'; import * as cheerio from 'cheerio'; const FIREFOX_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/143.0.0.0 Safari/537.36'; export class Vinovo extends Extractor { public readonly id = 'Vinovo'; public readonly label = 'Vinovo (MFP)'; public override readonly ttl = 10800000; // 3h public override viaMediaFlowProxy = true; private domains = ['vinovo.to', 'vinovo.si']; public supports(ctx: Context, url: URL): boolean { return ( this.domains.some(d => url.host.includes(d)) && supportsMediaFlowProxy(ctx) ); } // Normalize to embed URL public override normalize(url: URL): URL { // Accept /d/ or /e/, always normalize to /e/ return new URL(url.href.replace(/\/[de]\//, '/e/')); } protected async extractInternal( ctx: Context, url: URL, meta: Meta ): Promise<UrlResult[]> { //const referer = meta.referer ?? url.href; const headers: Record<string, string> = { 'User-Agent': FIREFOX_UA, Referer: 'https://vinovo.to/', }; ; // --- 1. Fetch embed page (existence check only) --- let embedHtml: string; try { embedHtml = await this.fetcher.text(ctx, url, headers); } catch { // Fail fast so StreamResolver skips this source throw new NotFoundError('Vinovo: embed not responding'); } if (!embedHtml) throw new NotFoundError(); // --- Detect removed / invalid video --- if ( embedHtml.includes('File Not Found') || embedHtml.includes('Removed') || embedHtml.includes('Not Found') ) { throw new NotFoundError('Vinovo: video removed'); } // Optional: extract title (best-effort only) let title = this.label; try { const $ = cheerio.load(embedHtml); title = $('title') .text() .replace(/^Watch\s+/i, '') .trim() || this.label; } catch { // ignore } // --- 2. MediaFlow proxy --- const proxiedUrl = await buildMediaFlowProxyExtractorStreamUrl( ctx, this.fetcher, this.id, url, headers ); return [ { url: proxiedUrl, format: Format.mp4, label: this.label, sourceId: `${this.id}_${meta.countryCodes?.join('_') ?? 'all'}`, ttl: this.ttl, requestHeaders: headers, meta: { ...meta, title, }, }, ]; } }` Mediaflow import re import json from typing import Dict, Any from urllib.parse import urlparse from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError from mediaflow_proxy.utils.girc import girc FIREFOX_UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/143.0.0.0 Safari/537.36" ) class VinovoExtractor(BaseExtractor): """ Vinovo extractor for MediaFlow Proxy ✔ MP4 stream (NOT HLS) ✔ Matches Kodi ResolveURL ✔ Correct reCAPTCHA handling ✔ Correct CDN headers (prevents 403) """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.mediaflow_endpoint = "proxy_stream_endpoint" async def extract(self, url: str) -> Dict[str, Any]: referer = self.base_headers.get("Referer") if not referer: parsed = urlparse(url) referer = f"{parsed.scheme}://{parsed.netloc}/" parsed = urlparse(url) if not parsed.hostname or "vinovo" not in parsed.hostname: raise ExtractorError("VINOVO: Invalid domain") # Normalize /d/ → /e/ url = re.sub(r"/[dD]/", "/e/", url) # ------------------------------------------------- # Incoming headers (from MediaFlow) # ------------------------------------------------- incoming = self.base_headers or {} embed_referer = incoming.get("referer") if not embed_referer: raise ExtractorError("VINOVO: Missing embed referer") user_agent = incoming.get("user-agent", FIREFOX_UA) vinovo_origin = f"{parsed.scheme}://{parsed.hostname}/" # ------------------------------------------------- # 1) Fetch embed page (USES EMBED REFERER) # ------------------------------------------------- page_headers = { "User-Agent": user_agent, "Referer": embed_referer, } response = await self._make_request(url, headers=page_headers) html = response.text or "" if not html: raise ExtractorError("VINOVO: Empty embed page") cookies = response.cookies or {} # ------------------------------------------------- # 2) Extract token + base URL # ------------------------------------------------- token_match = re.search(r'name="token"\s*content="([^"]+)', html) base_match = re.search(r'<video[^>]+data-base="([^"]+)', html) if not token_match or not base_match: raise ExtractorError("VINOVO: Token or base missing") token = token_match.group(1) base = base_match.group(1) # ------------------------------------------------- # 3) Solve reCAPTCHA (MUST USE VINOVO ORIGIN) # ------------------------------------------------- recaptcha = await girc( self._make_request, html, vinovo_origin, # 🔑 CRITICAL ) if not recaptcha: raise ExtractorError("VINOVO: reCAPTCHA failed") # ------------------------------------------------- # 4) Call Vinovo API # ------------------------------------------------- media_id = self._extract_id(url) api_url = f"https://{parsed.hostname}/api/file/url/{media_id}" api_headers = { "User-Agent": user_agent, "Referer": vinovo_origin, "Origin": vinovo_origin.rstrip("/"), "X-Requested-With": "XMLHttpRequest", } if cookies: api_headers["Cookie"] = "; ".join( f"{k}={v}" for k, v in cookies.items() ) api_resp = await self._make_request( api_url, method="POST", headers=api_headers, data={ "token": token, "recaptcha": recaptcha, }, ) if not api_resp.text: raise ExtractorError("VINOVO: Empty API response") try: api_json = json.loads(api_resp.text) except Exception: raise ExtractorError("VINOVO: Invalid API JSON") if api_json.get("status") != "ok": raise ExtractorError(f"VINOVO: API rejected {api_json}") # ------------------------------------------------- # 5) Final MP4 URL + CDN headers # ------------------------------------------------- final_url = f"{base}/stream/{api_json['token']}" #self.base_headers["origin"] = "https://vinovo.to" #self.base_headers["range"] = "bytes=0-" self.base_headers.update({ "origin": "https://vinovo.to", "range": "bytes=0-", "accept": "*/*", "accept-encoding": "identity", "host": urlparse(base).hostname, # 🔑 CRITICAL — request fingerprint "sec-fetch-site": "cross-site", "sec-fetch-mode": "cors", "sec-fetch-dest": "video", }) return { "destination_url": final_url, "request_headers": self.base_headers, "mediaflow_endpoint": self.mediaflow_endpoint, } def _extract_id(self, url: str) -> str: match = re.search(r"/(?:e|d)/([0-9a-zA-Z]+)", url) if not match: raise ExtractorError("VINOVO: Invalid URL format") return match.group(1) What could it be...
GLlgGL commented 2025-12-21 08:35:39 +00:00 (Migrated from github.com)

No link to test.The link i had expired or was removed

No link to test.The link i had expired or was removed
GLlgGL commented 2025-12-22 14:26:49 +00:00 (Migrated from github.com)

Link seems to be again up and working

Link seems to be again up and working
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Creepso/webstreamr-github#585
No description provided.