From 7a1e96d8b115a77321a2636ba33891ff536dab5a Mon Sep 17 00:00:00 2001 From: UrloMythus Date: Sun, 15 Jun 2025 19:20:01 +0200 Subject: [PATCH] Update to newest version --- mediaflow_proxy/extractors/dlhd.py | 109 +++++++++++------- mediaflow_proxy/extractors/vixcloud.py | 2 - mediaflow_proxy/handlers.py | 15 ++- mediaflow_proxy/mpd_processor.py | 4 +- mediaflow_proxy/schemas.py | 4 + .../__pycache__/all_debrid.cpython-313.pyc | Bin 3388 -> 0 bytes .../__pycache__/base.cpython-313.pyc | Bin 1628 -> 0 bytes .../__pycache__/real_debrid.cpython-313.pyc | Bin 2552 -> 0 bytes mediaflow_proxy/utils/m3u8_processor.py | 15 ++- 9 files changed, 98 insertions(+), 51 deletions(-) delete mode 100644 mediaflow_proxy/speedtest/providers/__pycache__/all_debrid.cpython-313.pyc delete mode 100644 mediaflow_proxy/speedtest/providers/__pycache__/base.cpython-313.pyc delete mode 100644 mediaflow_proxy/speedtest/providers/__pycache__/real_debrid.cpython-313.pyc 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 789cb1e7989d0b736884632f4df5738d223ddb6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3388 zcmZ`+O>7&-6`uVex%?Al{aCd8NB`6{ZHts+S+-I`jvT77q*!&eq+mw`)+=%;twZiI zGfO)X5V%N-Kt@v37q?C?QGp(8pa=El-qaw6 zfU`4i-n@D5&3oS)Zid4F0OcsBg=SPu*H;~vusbgQZ8z?^s}<1n}%A9bGF1Z%hHV* z7U%7NtkF9f9We@~EoGQH0($2am1%NW)ADJJS=Xs~N6%~2?%G9q+cXOL3`lLG+6NWj zRqpE6tU@)5&gEQf-JYvYHalSOkYowvWI}nFqk_y+aYl%Xc1u(IFr_A4N5?+_V?4H` zD27_n6vYlGO3BPuiZ~7_$_Ev-SYL@KNEmUwo7@(uSN3FmSt;v58g9(`vjP0NGvM<&d_mv~!3@F(c7ko9sVd*GI!$7*#vk`TE@8Gra7?fgJn+IiYPiQnmoF;d}rHd)9pHuj2?Suru!;CjX z2n6)J)20v_oHl_sy`upPcF3LQ>vtgUjgX0bJ6K>UcgfKPkAb(*wwq^S|882tcVLMZ zY!T|VfF2b%v7En=pE$64R|}E%Wjrn9oB&ouU8@YA;Wfk?tdBoVBr?9qc-PVb*b5`U z5DjmETo4LeeE+yDyfd7hPt00YnZ21r;Hcw9&MYN$!_p{VHe^@wPTw}t%m)yudxmBp z1KottbJ~39Y@)a4T>rU?{R8R+z<-bGu46VqmfBtr133zb<(t)1Kyzk$%PLh%Z2fDn zZ2rhkRlTU?W0n~!XjX1E*37P0Jtu81!fA!UnU!2lV{HDwHqX%Yk(irSAVWj=w#B?^ zsWz{dZ9!*c+m|yd29$-kKof8vO=8!J-9hMV5n+5CLwp_YcwS7r(O0m!giYpy+qNei z_>M;?<^{!6vjY{-PjReuF(l}!spL$s67$3sZ!^=Nm}nGAc>O>xRgJt^vV9dpFPIbt zR4W(NoMwA;ODi#3fWi=$DB_nbQH_;N1O1LuzjLOJWz*|=#}2#V6r)m_2K&zWoo_O9Cs~$>a6s%6yb^!v3;vDQ&q@Nam@yWDlIdVTVnOVu0dv$5%wt{n8s zdmr8TyyZ`U+Q~~xlh8ejUaE~v*Sc~G;%6Z^J_{ig*uk24QiPi4&tI@$) zbnq!#zOoX1XCb_nzOkCVRZHJmO&4nE!nf(!ZzKA%^z2{XT~0j^zv%wF`%!3dY$^Qs zUUl!~XRViO>DdLb7SX?pv@hJNN{3#a1#i6i&v6pXbFVff(t4YF#V`mzPNdFqOI+%> z_~)ThF#K|VG<8D!^4w4hOupg{rcQgmI^@Igi6Hc^l?_BWr)*5Zz%3g9I(481QnNJx zW@ia-;H7N}ih|o@0C$-maKWS1feB%pVkLvj4LdQDuTZD(+Wuw;#d-QRuuyzrKz?D! z(ksx}oWf8vM4uaZ0gS7h^JudVHnaEjuydC&GubKVFy_{x2Ueq}YSB|G(KD-UXYTtJ z#OIL{OB2g|waC?~boHe`+IwEH5U_vT5$Ar({aF0no;XJlGshuYTj5Kw7*NDx3Iza4 zy`tpJ9Mo}vS(F||I+j(6`cm|(o3Ks0=m^rVMmQMYzMrsAP(WU{LpL$p@Co_5cws~2 zJ!cn%EdrknX`kotV&-w*lCUH%Nlyl!UR=If`^mNH>1(wgjc*ZH-RS2%BV6_9!1u6W zgmdMi=y)3)fRF99=I~wUD(I++TH0OBccS|wOt}7IE;_|_t~w7Yyto9sW3?i45eYHE zKKcK78*eX`n1+`*#hU3{w!KS!#nOw+sVIswbdb{a4}X}`oc{psd(?3mR{w1`+R`QW zWj#n`Sil%&7_}P$$8rB4;TPm&jhy^9IrD-Hy&$=NlFM6Oj`Kgbxkcc!B^~F$d;1dq JB{*=z{tstsMvDLd 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 b8f9a0c5fc2b3400dc5e6c6674fab3fea0b49bfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1628 zcma)6&2QXP5P$wyuXmG8fHZ2GLS^Mc)Pn3DNJJ4R5=|x24OwX2+$=1w?cEr8?Pcr* z(gUhM0&&ZwQjQ$SEhog0KOqqkDo;X1LOtv)lEZ-;Gtax*Mu5bV{PTM=HeS}1zi82pM^>6MyA9ox7;+?N&J8g`feJ)ow$>DMZf|_(OG^I z&ySeRK(ZLfmITTQG6K~u*_x%iidwsD>$bioQz1~MlFTYWrK_1`e%a~=fqy?pvkxKp zH1q@5OyW-19qMQ`q^j~-hFZrTfJ+JTiZ~7+N{?8}ta9CPVz(DK4zD^+FYyNv?WY~* zlYtwJG_#J=2_em*Fb?9xabSiVD`A{*RJn1SWWsrhDoT?pSSmj6I4Q|J$IUVb+XIY@ z1}~sNjt}D>HTIHT(7;@h#_hx#^ny4`8=DKlg{uWH%*OB0)`-O5PM_7zRooWvH`@NBtjN|Q^mf* zC!a=GVg_>QN^dG1$&?>#ycc9f6rz))Q#jkb_kIx|+Y2V14LA0UWYp!x0K3`ZU zTteVw$MF*n+n?9~wUw9@xJs`cMX zb7!(#s(rWn{o1i|Y#r-I8$aIsY3uauos-vgPFGqvLv5}w?UJxN)5p7$#kZd6a`I6` zqd+aQ#uc~i0sXK-~M-jd9w6ri~ku=`9AeJ{iXt7Jma)6q=3z 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 bc729afd9bee65b593992e82b196bdcd1c17c456..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2552 zcmbVO&2JM&6rcUDy>V=uU{HgFs7YGLQero%Qvy^-36V$~?9faSfpEIo*lTCQde@y@ zha?Ats8pplF0?&XRjZa;jycl*zy>7JX(cLEJ>-@sR8?+$v+ECNMO8BLzIpRz-kbN{ z%=?Y^!r>r-R+|`K-1Q^$I~z2cufgn11M>i3gtQh9g_bkh66mib+B0#RxgK}G_jsk@d|WR>$YsrRR2FlB`ibULiJ9j>x!JV!-o@ESHTo!;AT){2;BtEYtv$rYxi)Bhj3)5u?BZV&mZT&j zV_H$I8cu?!B)K-+)F%ro|JQ)0dA_M>SmxjXRTpoS4ytOGDsQNIq3JNFT5qV^3f5UY zu&SZ19C%k_`Jk%hYWbk5OShaiRK46x@oR6RVO0zNxQ~~atZ1l2b1#-0qQXc9Q(=1a zIu)-@We2G^K6wQwzH)7l`hZTsVCDv+S4XZh{^}gi%xnf|dU9rv2HQ3XE3Iu-yK7=% z@WUcNqfd9VvJJSPngy#&165NmTBIzCRJ1jtNX05KY$_R6-bD~9EdWR=Ovnm24vlKb zMXRhOL1rb#tbCQhjh)1KqE>XXlq_onT`d~c?EyfgmDPc=s+(=0;tF5NLKC{|B&%Gd zQMr{>8ZK#$!fMvtot|tXUkeixuPo^V2kpB+w$WYmTNH0aGh0Wd8o#3-w|P$(Q+AJq zyMpWD&I$Ahx4=D{FV*KuPv`Zgk)_A;di~0cM=STGuhZWx|8VEqI~(d|_mk5XH^*wH zMryIq$6cfKd3{~1N0zoC(fc#&Yc=WE{+@ukZgKl|7dUaRZzRTj-#>E1|5G>&>?;t= z-U1hb=Ya7bcozK^$WFk^xF{gB%smjYzK{p8hi`(!YZyQp^XCC5C6w)Q0aD0rv!@sy zy0DwYLX~(&+R;j?%#%J?LfF$H3*-dj-v&ZC#b&1Av`H@;2U()??lVYFyzU^pd1k~u z1B9KzR?qQgJ!k4YXP)$&eRlZl7lC#0*T|`j=|`!0WV|Mg?+Pe7uy2Q8`Mo1D_a%2) z{Hj~#9-^##h)RlLs%1@4Xi!ngR-tMzKBOq0R#l@h6H%0+PHe}}P0h4mI-n>8E3YVo zU01?_pn~lX!ZJ?a4+&~VOqiGSsG`{HwahE3;}CtJ3fTemvoN;@A952wSSDS