diff --git a/mediaflow_proxy/extractors/dlhd.py b/mediaflow_proxy/extractors/dlhd.py index d290b7a..5f9bbab 100644 --- a/mediaflow_proxy/extractors/dlhd.py +++ b/mediaflow_proxy/extractors/dlhd.py @@ -30,46 +30,70 @@ class DLHDExtractor(BaseExtractor): try: # Channel URL is required and serves as the referer channel_url = url - player_origin = self._get_origin(channel_url) + channel_origin = self._get_origin(channel_url) # Channel page origin # Check for direct parameters - player_url = kwargs.get("player_url") - stream_url = kwargs.get("stream_url") - auth_url_base = kwargs.get("auth_url_base") + player_url_from_arg = kwargs.get("player_url") + stream_url_from_arg = kwargs.get("stream_url") + auth_url_base_from_arg = kwargs.get("auth_url_base") + + current_player_url_for_processing: str # If player URL not provided, extract it from channel page - if not player_url: + if not player_url_from_arg: # Get the channel page to extract the player iframe URL channel_headers = { - "referer": player_origin + "/", - "origin": player_origin, + "referer": channel_origin + "/", + "origin": channel_origin, "user-agent": self.base_headers["user-agent"], } channel_response = await self._make_request(channel_url, headers=channel_headers) - player_url = self._extract_player_url(channel_response.text) + extracted_iframe_url = self._extract_player_url(channel_response.text) - if not player_url: + if not extracted_iframe_url: raise ExtractorError("Could not extract player URL from channel page") + current_player_url_for_processing = extracted_iframe_url + else: + current_player_url_for_processing = player_url_from_arg - if not re.search(r"/stream/([a-zA-Z0-9-]+)", player_url): - iframe_player_url = await self._handle_playnow(player_url, player_origin) - player_origin = self._get_origin(player_url) - player_url = iframe_player_url - + # Attempt 1: _handle_vecloud with current_player_url_for_processing + # The referer for _handle_vecloud is the origin of the channel page (channel_origin) + # or the origin of the player itself if it is a /stream/ URL. try: - return await self._handle_vecloud(player_url, player_origin + "/") - except Exception as e: - pass + referer_for_vecloud = channel_origin + "/" + if re.search(r"/stream/([a-zA-Z0-9-]+)", current_player_url_for_processing): + referer_for_vecloud = self._get_origin(current_player_url_for_processing) + "/" + return await self._handle_vecloud(current_player_url_for_processing, referer_for_vecloud) + except Exception: + pass # Fail, Continue + + # Attempt 2: If _handle_vecloud fail and the URL is not /stream/, try _handle_playnow + # and then _handle_vecloud again with the URL resulting from playnow. + if not re.search(r"/stream/([a-zA-Z0-9-]+)", current_player_url_for_processing): + try: + playnow_derived_player_url = await self._handle_playnow(current_player_url_for_processing, channel_origin + "/") + if re.search(r"/stream/([a-zA-Z0-9-]+)", playnow_derived_player_url): + try: + referer_for_vecloud_after_playnow = self._get_origin(playnow_derived_player_url) + "/" + return await self._handle_vecloud(playnow_derived_player_url, referer_for_vecloud_after_playnow) + except Exception: + pass + except Exception: + pass + # If all previous attempts have failed, proceed with standard authentication. + player_url_for_auth = current_player_url_for_processing + player_origin_for_auth = self._get_origin(player_url_for_auth) + # Get player page to extract authentication information player_headers = { - "referer": player_origin + "/", - "origin": player_origin, + "referer": player_origin_for_auth + "/", + "origin": player_origin_for_auth, "user-agent": self.base_headers["user-agent"], } - player_response = await self._make_request(player_url, headers=player_headers) + player_response = await self._make_request(player_url_for_auth, headers=player_headers) player_content = player_response.text # Extract authentication details from script tag @@ -78,62 +102,63 @@ class DLHDExtractor(BaseExtractor): raise ExtractorError("Failed to extract authentication data from player") # Extract auth URL base if not provided - if not auth_url_base: - auth_url_base = self._extract_auth_url_base(player_content) + final_auth_url_base = auth_url_base_from_arg + if not final_auth_url_base: + final_auth_url_base = self._extract_auth_url_base(player_content) # If still no auth URL base, try to derive from stream URL or player URL - if not auth_url_base: - if stream_url: - auth_url_base = self._get_origin(stream_url) + if not final_auth_url_base: + if stream_url_from_arg: + final_auth_url_base = self._get_origin(stream_url_from_arg) else: # Try to extract from player URL structure - player_domain = self._get_origin(player_url) + player_domain_for_auth_derive = self._get_origin(player_url_for_auth) # Attempt to construct a standard auth domain - auth_url_base = self._derive_auth_url_base(player_domain) + final_auth_url_base = self._derive_auth_url_base(player_domain_for_auth_derive) - if not auth_url_base: + if not final_auth_url_base: raise ExtractorError("Could not determine auth URL base") # Construct auth URL auth_url = ( - f"{auth_url_base}/auth.php?channel_id={auth_data['channel_key']}" + f"{final_auth_url_base}/auth.php?channel_id={auth_data['channel_key']}" f"&ts={auth_data['auth_ts']}&rnd={auth_data['auth_rnd']}" f"&sig={quote(auth_data['auth_sig'])}" ) # Make auth request - player_origin = self._get_origin(player_url) - auth_headers = { - "referer": player_origin + "/", - "origin": player_origin, + auth_req_headers = { + "referer": player_origin_for_auth + "/", + "origin": player_origin_for_auth, "user-agent": self.base_headers["user-agent"], } - auth_response = await self._make_request(auth_url, headers=auth_headers) + auth_response = await self._make_request(auth_url, headers=auth_req_headers) # Check if authentication succeeded if auth_response.json().get("status") != "ok": raise ExtractorError("Authentication failed") # If no stream URL provided, look up the server and generate the stream URL - if not stream_url: - stream_url = await self._lookup_server( - lookup_url_base=player_origin, - auth_url_base=auth_url_base, + final_stream_url = stream_url_from_arg + if not final_stream_url: + final_stream_url = await self._lookup_server( + lookup_url_base=player_origin_for_auth, + auth_url_base=final_auth_url_base, auth_data=auth_data, - headers=auth_headers, + headers=auth_req_headers, ) # Set up the final stream headers stream_headers = { - "referer": player_url, - "origin": player_origin, + "referer": player_url_for_auth, + "origin": player_origin_for_auth, "user-agent": self.base_headers["user-agent"], } # Return the stream URL with headers return { - "destination_url": stream_url, + "destination_url": final_stream_url, "request_headers": stream_headers, "mediaflow_endpoint": self.mediaflow_endpoint, } diff --git a/mediaflow_proxy/extractors/vixcloud.py b/mediaflow_proxy/extractors/vixcloud.py index b480da5..1a230b6 100644 --- a/mediaflow_proxy/extractors/vixcloud.py +++ b/mediaflow_proxy/extractors/vixcloud.py @@ -56,8 +56,6 @@ class VixCloudExtractor(BaseExtractor): script = soup.find("body").find("script").text token = re.search(r"'token':\s*'(\w+)'", script).group(1) expires = re.search(r"'expires':\s*'(\d+)'", script).group(1) - canPlayFHD = re.search(r"window\.canPlayFHD\s*=\s*(\w+)", script).group(1) - print(script,"A") server_url = re.search(r"url:\s*'([^']+)'", script).group(1) if "?b=1" in server_url: final_url = f'{server_url}&token={token}&expires={expires}' diff --git a/mediaflow_proxy/handlers.py b/mediaflow_proxy/handlers.py index 97aeafe..193e1c3 100644 --- a/mediaflow_proxy/handlers.py +++ b/mediaflow_proxy/handlers.py @@ -85,13 +85,19 @@ async def handle_hls_stream_proxy( proxy_headers.request.update({"range": content_range}) try: + # If force_playlist_proxy is enabled, skip detection and directly process as m3u8 + if hls_params.force_playlist_proxy: + return await fetch_and_process_m3u8( + streamer, hls_params.destination, proxy_headers, request, hls_params.key_url, hls_params.force_playlist_proxy + ) + parsed_url = urlparse(hls_params.destination) # Check if the URL is a valid m3u8 playlist or m3u file if parsed_url.path.endswith((".m3u", ".m3u8", ".m3u_plus")) or parse_qs(parsed_url.query).get("type", [""])[ 0 ] in ["m3u", "m3u8", "m3u_plus"]: return await fetch_and_process_m3u8( - streamer, hls_params.destination, proxy_headers, request, hls_params.key_url + streamer, hls_params.destination, proxy_headers, request, hls_params.key_url, hls_params.force_playlist_proxy ) # Create initial streaming response to check content type @@ -100,7 +106,7 @@ async def handle_hls_stream_proxy( if "mpegurl" in response_headers.get("content-type", "").lower(): return await fetch_and_process_m3u8( - streamer, hls_params.destination, proxy_headers, request, hls_params.key_url + streamer, hls_params.destination, proxy_headers, request, hls_params.key_url, hls_params.force_playlist_proxy ) return EnhancedStreamingResponse( @@ -190,7 +196,7 @@ async def proxy_stream(method: str, destination: str, proxy_headers: ProxyReques async def fetch_and_process_m3u8( - streamer: Streamer, url: str, proxy_headers: ProxyRequestHeaders, request: Request, key_url: str = None + streamer: Streamer, url: str, proxy_headers: ProxyRequestHeaders, request: Request, key_url: str = None, force_playlist_proxy: bool = None ): """ Fetches and processes the m3u8 playlist on-the-fly, converting it to an HLS playlist. @@ -201,6 +207,7 @@ async def fetch_and_process_m3u8( proxy_headers (ProxyRequestHeaders): The headers to include in the request. request (Request): The incoming HTTP request. key_url (str, optional): The HLS Key URL to replace the original key URL. Defaults to None. + force_playlist_proxy (bool, optional): Force all playlist URLs to be proxied through MediaFlow. Defaults to None. Returns: Response: The HTTP response with the processed m3u8 playlist. @@ -211,7 +218,7 @@ async def fetch_and_process_m3u8( await streamer.create_streaming_response(url, proxy_headers.request) # Initialize processor and response headers - processor = M3U8Processor(request, key_url) + processor = M3U8Processor(request, key_url, force_playlist_proxy) response_headers = { "content-disposition": "inline", "accept-ranges": "none", diff --git a/mediaflow_proxy/mpd_processor.py b/mediaflow_proxy/mpd_processor.py index a6569f7..5540698 100644 --- a/mediaflow_proxy/mpd_processor.py +++ b/mediaflow_proxy/mpd_processor.py @@ -137,8 +137,10 @@ def build_hls(mpd_dict: dict, request: Request, key_id: str = None, key: str = N # Add video streams for profile, playlist_url in video_profiles.values(): + # Only add AUDIO attribute if there are audio profiles available + audio_attr = ',AUDIO="audio"' if audio_profiles else "" hls.append( - f'#EXT-X-STREAM-INF:BANDWIDTH={profile["bandwidth"]},RESOLUTION={profile["width"]}x{profile["height"]},CODECS="{profile["codecs"]}",FRAME-RATE={profile["frameRate"]},AUDIO="audio"' + f'#EXT-X-STREAM-INF:BANDWIDTH={profile["bandwidth"]},RESOLUTION={profile["width"]}x{profile["height"]},CODECS="{profile["codecs"]}",FRAME-RATE={profile["frameRate"]}{audio_attr}' ) hls.append(playlist_url) diff --git a/mediaflow_proxy/schemas.py b/mediaflow_proxy/schemas.py index d621754..e58ce80 100644 --- a/mediaflow_proxy/schemas.py +++ b/mediaflow_proxy/schemas.py @@ -64,6 +64,10 @@ class HLSManifestParams(GenericParams): None, description="The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)", ) + force_playlist_proxy: Optional[bool] = Field( + None, + description="Force all playlist URLs to be proxied through MediaFlow regardless of m3u8_content_routing setting. Useful for IPTV m3u/m3u_plus formats that don't have clear URL indicators.", + ) class MPDManifestParams(GenericParams): diff --git a/mediaflow_proxy/speedtest/providers/__pycache__/all_debrid.cpython-313.pyc b/mediaflow_proxy/speedtest/providers/__pycache__/all_debrid.cpython-313.pyc deleted file mode 100644 index 789cb1e..0000000 Binary files a/mediaflow_proxy/speedtest/providers/__pycache__/all_debrid.cpython-313.pyc and /dev/null differ diff --git a/mediaflow_proxy/speedtest/providers/__pycache__/base.cpython-313.pyc b/mediaflow_proxy/speedtest/providers/__pycache__/base.cpython-313.pyc deleted file mode 100644 index b8f9a0c..0000000 Binary files a/mediaflow_proxy/speedtest/providers/__pycache__/base.cpython-313.pyc and /dev/null differ diff --git a/mediaflow_proxy/speedtest/providers/__pycache__/real_debrid.cpython-313.pyc b/mediaflow_proxy/speedtest/providers/__pycache__/real_debrid.cpython-313.pyc deleted file mode 100644 index bc729af..0000000 Binary files a/mediaflow_proxy/speedtest/providers/__pycache__/real_debrid.cpython-313.pyc and /dev/null differ diff --git a/mediaflow_proxy/utils/m3u8_processor.py b/mediaflow_proxy/utils/m3u8_processor.py index b71fe51..a8c3f73 100644 --- a/mediaflow_proxy/utils/m3u8_processor.py +++ b/mediaflow_proxy/utils/m3u8_processor.py @@ -9,16 +9,18 @@ from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url, encode_ class M3U8Processor: - def __init__(self, request, key_url: str = None): + def __init__(self, request, key_url: str = None, force_playlist_proxy: bool = None): """ Initializes the M3U8Processor with the request and URL prefix. Args: request (Request): The incoming HTTP request. key_url (HttpUrl, optional): The URL of the key server. Defaults to None. + force_playlist_proxy (bool, optional): Force all playlist URLs to be proxied through MediaFlow. Defaults to None. """ self.request = request self.key_url = parse.urlparse(key_url) if key_url else None + self.force_playlist_proxy = force_playlist_proxy self.mediaflow_proxy_url = str( request.url_for("hls_manifest_proxy").replace(scheme=get_original_scheme(request)) ) @@ -146,8 +148,15 @@ class M3U8Processor: # Determine routing strategy based on configuration routing_strategy = settings.m3u8_content_routing + # Check if we should force MediaFlow proxy for all playlist URLs + if self.force_playlist_proxy: + return await self.proxy_url(full_url, base_url, use_full_url=True) + # For playlist URLs, always use MediaFlow proxy regardless of strategy - if ".m3u" in full_url: + # Check for actual playlist file extensions, not just substring matches + parsed_url = parse.urlparse(full_url) + if (parsed_url.path.endswith((".m3u", ".m3u8", ".m3u_plus")) or + parse.parse_qs(parsed_url.query).get("type", [""])[0] in ["m3u", "m3u8", "m3u_plus"]): return await self.proxy_url(full_url, base_url, use_full_url=True) # Route non-playlist content URLs based on strategy @@ -191,6 +200,8 @@ class M3U8Processor: has_encrypted = query_params.pop("has_encrypted", False) # Remove the response headers from the query params to avoid it being added to the consecutive requests [query_params.pop(key, None) for key in list(query_params.keys()) if key.startswith("r_")] + # Remove force_playlist_proxy to avoid it being added to subsequent requests + query_params.pop("force_playlist_proxy", None) return encode_mediaflow_proxy_url( self.mediaflow_proxy_url,