""" Proxy server that wraps widefrog services to extract and play m3u8/mpd manifests directly in the browser. Usage: python proxy_server.py [--port PORT] [--host HOST] Then open http://localhost:5000 in your browser. """ import base64 import builtins import json import os import re import sys import traceback from urllib.parse import urlencode, urlparse, urljoin, quote, unquote, parse_qs, urlunparse from flask import Flask, request, Response, render_template_string, jsonify, redirect import requests as http_requests from utils.constants.macros import CONFIG_FILE, DEFAULT_DEBUG_MODE from utils.structs import BaseElement from utils.tools.args import get_config from utils.tools.cdm import init_cdm, close_cdm from utils.tools.common import get_base_url from utils.tools.service import get_service, get_all_services # --------------------------------------------------------------------------- # Flask app # --------------------------------------------------------------------------- app = Flask(__name__) # Cache extracted data so we don't re-extract on every segment request _manifest_cache: dict = {} # url -> {manifest_url, manifest_type, keys, headers, additional} # Default headers used when proxying requests _PROXY_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.9", "Origin": None, "Referer": None, } # --------------------------------------------------------------------------- # Initialise widefrog config (once) # --------------------------------------------------------------------------- def _init_widefrog(): """Perform the same first-time setup as widefrog.py.""" if hasattr(builtins, "CONFIG"): return args = [] builtins.CONFIG = get_config(args) builtins.CONFIG["QUERY"] = { "MIN": {"COLLECTION": None, "ELEMENT": None}, "MAX": {"COLLECTION": None, "ELEMENT": None}, } builtins.CONFIG["DEBUG_MODE"] = DEFAULT_DEBUG_MODE builtins.SERVICES = get_all_services() # Override to skip downloads builtins.CONFIG["DOWNLOAD_COMMANDS"]["WAIT_BEFORE_DOWNLOADING"] = None # --------------------------------------------------------------------------- # Extraction helpers # --------------------------------------------------------------------------- def _extract_manifest(content_url: str) -> dict: """Use the widefrog service layer to get manifest + keys for *content_url*.""" if content_url in _manifest_cache: return _manifest_cache[content_url] _init_widefrog() service = get_service(content_url) if service is None: raise ValueError(f"No service found for URL: {content_url}") service_instance = service source_element = BaseElement(url=content_url) # --- get_video_data returns (manifest, pssh, additional) --- manifest, pssh, additional = service_instance.get_video_data(source_element) # Normalise manifest to list of (url, name) if not isinstance(manifest, list): manifest = [(manifest, None)] if len(manifest) == 0: manifest = [(None, None)] # Normalise pssh if not isinstance(pssh, list): pssh = [pssh] # Determine manifest type manifest_url = None for m_url, m_name in manifest: if m_url: manifest_url = m_url break if manifest_url is None: raise ValueError("No manifest URL could be extracted") manifest_type = "unknown" if ".m3u8" in manifest_url.split("?")[0].lower() or "m3u8" in manifest_url.lower(): manifest_type = "hls" elif ".mpd" in manifest_url.split("?")[0].lower() or "mpd" in manifest_url.lower(): manifest_type = "dash" elif ".ism" in manifest_url.split("?")[0].lower(): manifest_type = "smooth" else: # Try to sniff from content try: resp = http_requests.get(manifest_url, timeout=10) ct = resp.headers.get("Content-Type", "").lower() body = resp.text[:500].lower() if "#extm3u" in body: manifest_type = "hls" elif " str: """Build a proxy URL pointing to our server.""" return f"{route}?url={quote(target_url, safe='')}" def _resolve_url(base_url: str, relative: str) -> str: """Resolve a possibly-relative URL against a base.""" if relative.startswith("http://") or relative.startswith("https://"): return relative return urljoin(base_url, relative) def _rewrite_m3u8(content: str, base_url: str) -> str: """Rewrite all URLs in an HLS manifest so they go through our proxy.""" lines = content.split("\n") result = [] for line in lines: stripped = line.strip() if not stripped: result.append(line) continue # Rewrite URI="..." in tags like #EXT-X-KEY, #EXT-X-MAP, etc. if stripped.startswith("#"): def _rewrite_uri(m): uri = m.group(1) absolute = _resolve_url(base_url, uri) return f'URI="{_proxy_url(absolute)}"' rewritten = re.sub(r'URI="([^"]*)"', _rewrite_uri, stripped, flags=re.IGNORECASE) result.append(rewritten) else: # This is a URL line (segment or sub-playlist) absolute = _resolve_url(base_url, stripped) result.append(_proxy_url(absolute)) return "\n".join(result) def _make_base_proxy_url(original_base_url: str) -> str: """Encode a base URL into a path-based proxy prefix. Shaka-player resolves relative segment URLs against the manifest URL. By injecting a that uses a path-based route like /proxy/b//, relative URLs naturally resolve to /proxy/b//segment.dash which our handler proxies to original_base + segment.dash. """ b = base64.urlsafe_b64encode(original_base_url.encode()).decode().rstrip("=") return f"/proxy/b/{b}/" def _rewrite_mpd(content: str, base_url: str) -> str: """Rewrite URLs in a DASH MPD manifest to go through our proxy. Strategy: - Replace / inject so that *relative* segment URLs resolve through our path-based proxy (/proxy/b//). - Rewrite any *absolute* URLs in media/initialization attributes of SegmentTemplate. """ has_base_url = bool(re.search(r"]*>", content, re.IGNORECASE)) if has_base_url: # Rewrite existing elements def _rewrite_baseurl(m): url = m.group(1).strip() if url and (url.startswith("http://") or url.startswith("https://")): resolved = url if url.endswith("/") else url + "/" return f"{_make_base_proxy_url(resolved)}" elif url: absolute = _resolve_url(base_url, url) if not absolute.endswith("/"): absolute += "/" return f"{_make_base_proxy_url(absolute)}" return m.group(0) content = re.sub(r"(.*?)", _rewrite_baseurl, content, flags=re.DOTALL) else: # No BaseURL exists — inject one right after proxy_base = _make_base_proxy_url(base_url) content = re.sub( r'(]*>)', rf'\1\n {proxy_base}', content, count=1, ) # Rewrite absolute URLs in SegmentTemplate media/initialization attributes for attr in ["media", "initialization"]: def _rewrite_attr(m, attr_name=attr): url = m.group(1) if url.startswith("http://") or url.startswith("https://"): return f'{attr_name}="{_proxy_url(url)}"' return m.group(0) content = re.sub( rf'{attr}="(https?://[^"]*)"', _rewrite_attr, content, flags=re.IGNORECASE, ) return content # --------------------------------------------------------------------------- # API Routes # --------------------------------------------------------------------------- @app.route("/") def index(): """Home page with URL input form.""" return render_template_string(HOME_PAGE_HTML) @app.route("/api/extract", methods=["GET", "POST"]) def api_extract(): """Extract manifest info from a content URL (JSON API).""" if request.method == "POST": content_url = request.json.get("url", "").strip() else: content_url = request.args.get("url", "").strip() if not content_url: return jsonify({"error": "Missing 'url' parameter"}), 400 try: info = _extract_manifest(content_url) return jsonify({ "manifest_url": info["manifest_url"], "proxied_manifest_url": _proxy_url(info["manifest_url"], "/proxy/manifest"), "manifest_type": info["manifest_type"], "keys": info["keys"], "is_hls_aes": info["is_hls_aes"], "title": info["title"], }) except Exception as e: traceback.print_exc() return jsonify({"error": str(e)}), 500 @app.route("/play") def play(): """Player page — extracts manifest and renders an HTML5 video player.""" content_url = request.args.get("url", "").strip() if not content_url: return redirect("/") try: info = _extract_manifest(content_url) except Exception as e: traceback.print_exc() return render_template_string(ERROR_PAGE_HTML, error=str(e), url=content_url), 500 proxied_manifest = request.host_url.rstrip("/") + _proxy_url(info["manifest_url"], "/proxy/manifest") return render_template_string( PLAYER_PAGE_HTML, manifest_url=proxied_manifest, original_manifest_url=info["manifest_url"], manifest_type=info["manifest_type"], keys=info["keys"], title=info["title"], content_url=content_url, is_hls_aes=info["is_hls_aes"], ) @app.route("/proxy/manifest") def proxy_manifest(): """Fetch a manifest, rewrite its URLs, and return it.""" target_url = request.args.get("url", "") if not target_url: return "Missing url parameter", 400 target_url = unquote(target_url) headers = {k: v for k, v in _PROXY_HEADERS.items() if v is not None} headers["User-Agent"] = builtins.CONFIG.get("USER_AGENT", headers["User-Agent"]) if hasattr(builtins, "CONFIG") else headers["User-Agent"] try: resp = http_requests.get(target_url, headers=headers, timeout=30) except Exception as e: return f"Failed to fetch manifest: {e}", 502 content_type = resp.headers.get("Content-Type", "") body = resp.text # Determine base URL for relative resolution base_url = target_url.rsplit("/", 1)[0] + "/" # Detect type and rewrite if "#EXTM3U" in body or "m3u8" in target_url.lower() or "mpegurl" in content_type.lower(): body = _rewrite_m3u8(body, base_url) content_type = "application/vnd.apple.mpegurl" elif "/") def proxy_base_resource(base_b64, subpath): """Path-based proxy: resolves *subpath* relative to the decoded base URL. This is used by DASH manifests where is set to /proxy/b// so that relative segment URLs like "segment-video=400000.dash" naturally resolve here. """ # Decode the base URL padding = 4 - len(base_b64) % 4 if padding != 4: base_b64 += "=" * padding try: decoded_base = base64.urlsafe_b64decode(base_b64).decode() except Exception: return "Invalid base encoding", 400 target_url = decoded_base + subpath # Preserve query string from the original request if request.query_string: target_url += "?" + request.query_string.decode() headers = {k: v for k, v in _PROXY_HEADERS.items() if v is not None} if hasattr(builtins, "CONFIG"): headers["User-Agent"] = builtins.CONFIG.get("USER_AGENT", headers["User-Agent"]) if "Range" in request.headers: headers["Range"] = request.headers["Range"] try: resp = http_requests.get(target_url, headers=headers, timeout=60, stream=True) except Exception as e: return f"Failed to fetch: {e}", 502 content_type = resp.headers.get("Content-Type", "application/octet-stream") resp_body = resp.content # If the response is a sub-manifest (m3u8 or mpd), rewrite it try: if b"#EXTM3U" in resp_body[:20]: text = resp_body.decode("utf-8", errors="replace") text_base = target_url.rsplit("/", 1)[0] + "/" text = _rewrite_m3u8(text, text_base) resp_body = text.encode("utf-8") content_type = "application/vnd.apple.mpegurl" elif b" WideFrog Proxy Player

WideFrog Proxy

Paste a video URL from a supported service to play it in your browser.

Extracting manifest… this may take a moment.

You can also use the API directly:

GET /api/extract?url=CONTENT_URL

GET /proxy/manifest?url=MANIFEST_URL

""" PLAYER_PAGE_HTML = r""" {{ title }} — WideFrog Player
← WideFrog Proxy {{ title }}
Initializing player…
Type: {{ manifest_type | upper }}
Original manifest: {{ original_manifest_url[:120] }}{% if original_manifest_url|length > 120 %}…{% endif %}
Proxied manifest: {{ manifest_url[:120] }}{% if manifest_url|length > 120 %}…{% endif %}
{% if keys %}
Decryption keys: {% for k in keys %}
  {{ k }}
{% endfor %}
{% endif %}
""" ERROR_PAGE_HTML = r""" Error — WideFrog Proxy

Extraction Failed

{{ url }}

{{ error }}

← Back

""" # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="WideFrog Proxy Player") parser.add_argument("--host", default="127.0.0.1", help="Host to bind (default: 127.0.0.1)") parser.add_argument("--port", type=int, default=5000, help="Port to bind (default: 5000)") parser.add_argument("--debug", action="store_true", help="Enable Flask debug mode") cli_args = parser.parse_args() # Pre-init widefrog _init_widefrog() print(f"\n WideFrog Proxy Player") print(f" Open http://{cli_args.host}:{cli_args.port} in your browser\n") app.run(host=cli_args.host, port=cli_args.port, debug=cli_args.debug, threaded=True)