11# 25.07.25
22
33import base64
4- import logging
4+ from urllib . parse import urlencode
55
66
77# External libraries
1616console = Console ()
1717
1818
19- def get_widevine_keys (pssh , license_url , cdm_device_path , headers = None , payload = None ):
19+ def get_widevine_keys (pssh , license_url , cdm_device_path , headers = None , query_params = None ):
2020 """
2121 Extract Widevine CONTENT keys (KID/KEY) from a license using pywidevine.
2222
2323 Args:
2424 pssh (str): PSSH base64.
2525 license_url (str): Widevine license URL.
2626 cdm_device_path (str): Path to CDM file (device.wvd).
27- headers (dict): Optional HTTP headers.
27+ headers (dict): Optional HTTP headers for the license request (from fetch).
28+ query_params (dict): Optional query parameters to append to the URL.
2829
2930 Returns:
3031 list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error.
@@ -40,13 +41,26 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
4041
4142 try :
4243 challenge = cdm .get_license_challenge (session_id , PSSH (pssh ))
43- req_headers = headers or {}
44- req_headers ['Content-Type' ] = 'application/octet-stream'
45-
46- # Send license request using curl_cffi
44+
45+ # Build request URL with query params
46+ request_url = license_url
47+ if query_params :
48+ request_url = f"{ license_url } ?{ urlencode (query_params )} "
49+
50+ # Prepare headers (use original headers from fetch)
51+ req_headers = headers .copy () if headers else {}
52+ request_kwargs = {}
53+ request_kwargs ['data' ] = challenge
54+
55+ # Keep original Content-Type or default to octet-stream
56+ if 'Content-Type' not in req_headers :
57+ req_headers ['Content-Type' ] = 'application/octet-stream'
58+
59+ # Send license request
4760 try :
4861 # response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload)
49- response = requests .post (license_url , data = challenge , headers = req_headers , json = payload , impersonate = "chrome124" )
62+ response = requests .post (request_url , headers = req_headers , impersonate = "chrome124" , ** request_kwargs )
63+
5064 except Exception as e :
5165 console .print (f"[bold red]Request error:[/bold red] { e } " )
5266 return None
@@ -56,60 +70,51 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
5670 console .print ({
5771 "url" : license_url ,
5872 "headers" : req_headers ,
59- "content" : payload ,
6073 "session_id" : session_id .hex (),
6174 "pssh" : pssh
6275 })
63-
6476 return None
6577
66- # Handle (JSON) or classic (binary) license response
67- license_data = response .content
78+ # Parse license response
79+ license_bytes = response .content
6880 content_type = response .headers .get ("Content-Type" , "" )
69- logging .info (f"License data: { license_data } , Content-Type: { content_type } " )
70-
71- # Check if license_data is empty
72- if not license_data :
73- console .print ("[bold red]License response is empty.[/bold red]" )
74- return None
7581
82+ # Handle JSON response
7683 if "application/json" in content_type :
7784 try :
78-
79- # Try to decode as JSON only if plausible
80- data = None
81- try :
82- data = response .json ()
83- except Exception :
84- data = None
85-
86- if data and "license" in data :
87- license_data = base64 .b64decode (data ["license" ])
88-
89- elif data is not None :
90- console .print ("[bold red]'license' field not found in JSON response.[/bold red]" )
85+ data = response .json ()
86+ if "license" in data :
87+ license_bytes = base64 .b64decode (data ["license" ])
88+ else :
89+ console .print (f"[bold red]'license' field not found in JSON response: { data } .[/bold red]" )
9190 return None
92-
9391 except Exception as e :
9492 console .print (f"[bold red]Error parsing JSON license:[/bold red] { e } " )
93+ return None
9594
96- cdm .parse_license (session_id , license_data )
95+ if not license_bytes :
96+ console .print ("[bold red]License data is empty.[/bold red]" )
97+ return None
98+
99+ # Parse license
100+ try :
101+ cdm .parse_license (session_id , license_bytes )
102+ except Exception as e :
103+ console .print (f"[bold red]Error parsing license:[/bold red] { e } " )
104+ return None
97105
98- # Extract only CONTENT keys from the license
106+ # Extract CONTENT keys
99107 content_keys = []
100108 for key in cdm .get_keys (session_id ):
101109 if key .type == "CONTENT" :
102-
103110 kid = key .kid .hex () if isinstance (key .kid , bytes ) else str (key .kid )
104111 key_val = key .key .hex () if isinstance (key .key , bytes ) else str (key .key )
105112
106113 content_keys .append ({
107114 'kid' : kid .replace ('-' , '' ).strip (),
108115 'key' : key_val .replace ('-' , '' ).strip ()
109116 })
110- logging .info (f"Use kid: { kid } , key: { key_val } " )
111117
112- # Check if content_keys list is empty
113118 if not content_keys :
114119 console .print ("[bold yellow]⚠️ No CONTENT keys found in license.[/bold yellow]" )
115120 return None
0 commit comments