In the process of re-writing and updating Temple of Doom I have two versions of l3 py for use with pywidevine; one when everything is working, and another to include full debugging information so you may see where the fail point lies. For those just starting out it should help.
Both use a headers.py file, in the same folder, to contain the headers needed for permissive access to the license server. (See curlconverter.com)
You will also, of course, have a wvd file created from your CDM using pywidevine, the path to which needs editing in both files.
Normal version l3.py
Debug Version l3_debug.pyCode:from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH import httpx #################### #BE SURE TO EDIT THIS !!!! #################### WVD_PATH = "<your path to>/device.wvd" from headers import headers def get_key(pssh, license_url): device = Device.load(WVD_PATH) cdm = Cdm.from_device(device) session_id = cdm.open() challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) response = httpx.post(license_url, data=challenge, headers=headers) cdm.parse_license(session_id, response.content) keys = [] for key in cdm.get_keys(session_id): if key.type == 'CONTENT': keys.append(f"--key {key.kid.hex}:{key.key.hex()}") cdm.close(session_id) return "\n".join(keys) if __name__ == "__main__": pssh_str = input("PSSH? ") lic_url = input("License URL? ") result = get_key(pssh_str, lic_url) print(result)
Each site needs its own new headers file - headers.py placed in the same folder as l3_debug and l3.pyCode:import logging from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH import httpx from headers import headers logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) #################### #BE SURE TO EDIT THIS!!!!! #################### WVD_PATH = "<path to your>/device.wvd" def get_key(pssh, license_url): logger.debug("Loading device...") device = Device.load(WVD_PATH) cdm = Cdm.from_device(device) session_id = cdm.open() logger.debug("Session opened...") challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) response = httpx.post(license_url, data=challenge, headers=headers) cdm.parse_license(session_id, response.content) keys = [] logger.debug("Retrieving keys...") for key in cdm.get_keys(session_id): logger.debug(f"Key found: {key.kid.hex}:{key.key.hex()}, Type: {key.type}") if key.type == 'CONTENT': keys.append(f"--key {key.kid.hex}:{key.key.hex()}") cdm.close(session_id) logger.debug("Session closed...") return "\n".join(keys) if __name__ == "__main__": pssh_str = input("PSSH? ") lic_url = input("License URL? ") result = get_key(pssh_str, lic_url) logger.debug("Result:") print(result)
A headers.py looks something like this:-
But every site needs its own version created by copying cURL of the license into curlconverter.comheaders={
'authority': 'www.itv.com',
'user-agent': 'Dalvik/2.9.8 (Linux; U; Android 9.9.2; ALE-L94 Build/NJHGGF)',
'accept': '*/*',
'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Referer': 'https://www.itv.com/',
}
Running l3_debug for tg4.ie produces this_
And l3.py tested at bitmovin.com produces this:-PSSH? AAAAVnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADYIARIQka QzESAPRdadcqc8IdLlBRoNd2lkZXZpbmVfdGVzdCIIMTIzNDU2 NzgyB2RlZmF1bHQ=<
License URL? https://manifest.prod.boltdns.net/license/v1/cenc/widevine/1555966122001/38dfe47f-f6c1...Y0MzY1NA%3D%3D
DEBUG:__main__:Loading device...
DEBUG:__main__:Session opened...
DEBUG:httpx:load_ssl_context verify=True cert=None trust_env=True http2=False
DEBUG:httpx:load_verify_locations cafile='/home/angela/.local/lib/python3.12/site-packages/certifi/cacert.pem'
DEBUG:httpcore.connection:connect_tcp.started host='manifest.prod.boltdns.net' port=443 local_address=None timeout=5.0 socket_options=None
DEBUG:httpcore.connection:connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7f6f850b02c0>
DEBUG:httpcore.connectiontart_tls.started ssl_context=<ssl.SSLContext object at 0x7f6f852d1e50> server_hostname='manifest.prod.boltdns.net' timeout=5.0
DEBUG:httpcore.connectiontart_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x7f6f86fe1e80>
DEBUG:httpcore.http11end_request_headers.started request=<Request [b'POST']>
DEBUG:httpcore.http11end_request_headers.complete
DEBUG:httpcore.http11end_request_body.started request=<Request [b'POST']>
DEBUG:httpcore.http11end_request_body.complete
DEBUG:httpcore.http11:receive_response_headers.sta rted request=<Request [b'POST']>
DEBUG:httpcore.http11:receive_response_headers.com plete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Connection', b'keep-alive'), (b'Content-Length', b'690'), (b'Access-Control-Allow-Origin', b'*'), (b'Access-Control-Expose-Headers', b'*'), (b'Bcov-Request-Id', b'74996227-6345-429a-ac26-5acd6637b1e2'), (b'Cache-Control', b'private, max-age=5988'), (b'Content-Type', b'application/octet-stream'), (b'X-Hosted-On', b'Gantry'), (b'X-Powered-By', b'BC'), (b'X-Powered-From', b'eu-west-1c'), (b'Accept-Ranges', b'bytes'), (b'Date', b'Wed, 05 Jun 2024 10:57:26 GMT'), (b'Via', b'1.1 varnish'), (b'X-Served-By', b'cache-lon420096-LON'), (b'X-Cache', b'MISS'), (b'X-Cache-Hits', b'0'), (b'X-Timer', b'S1717585046.217263,VS0,VE62'), (b'X-Device-Group', b'desktop-firefox')])
INFO:httpx:HTTP Request: POST https://manifest.prod.boltdns.net/license/v1/cenc/widevine/1555966122001/38dfe47f-f6c1...Y0MzY1NA%3D%3D "HTTP/1.1 200 OK"
DEBUG:httpcore.http11:receive_response_body.starte d request=<Request [b'POST']>
DEBUG:httpcore.http11:receive_response_body.comple te
DEBUG:httpcore.http11:response_closed.started
DEBUG:httpcore.http11:response_closed.complete
DEBUG:httpcore.connection:close.started
DEBUG:httpcore.connection:close.complete
DEBUG:__main__:Retrieving keys...
DEBUG:__main__:Key found: 00000000000000000000000000000000:4a5943fb90b54f114 c167e4423dc64c1d4ca4c7462e34fddc6528a2df6e8151eede f8382e0941e7d8c659e16fbc41b9db489c84529b1029b9c5a3 1025b5f99f5, Type: SIGNING
DEBUG:__main__:Key found: 91a43311200f45d69d72a73c21d2e505:c5ea6154a0aa8e563 59cf15ce5bd51bf, Type: CONTENT
DEBUG:__main__:Session closed...
DEBUG:__main__:Result:
--key 91a43311200f45d69d72a73c21d2e505:c5ea6154a0aa8e563 59cf15ce5bd51bf
All files are here https://files.videohelp.com/u/301890/L3_PY.zippy l3.py
PSSH? AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62 dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xq YVNkZmFsa3IzaioCSEQyAA==
License URL? https://cwip-shaka-proxy.appspot.com/no_auth
--key ccbf5fb4c2965be7aa130ffb3ba9fd73:9cc0c92044cb1d694 33f5f5839a159df
--key 9bf0e9cf0d7b55aeb4b289a63bab8610:90f52fd8ca48717b2 1d0c2fed7a12ae1
--key eb676abbcb345e96bbcf616630f1a3da:100b6c20940f779a4 589152b57d2dacb
--key 0294b9599d755de2bbf0fdca3fa5eab7:3bda2f40344c7def6 14227b9c0f03e26
--key 639da80cf23b55f3b8cab3f64cfa5df6:229f5f29b643e2030 04b30c4eaf348f4
Support our site by donate $5 directly to us Thanks!!!
Try StreamFab Downloader and download streaming video from Netflix, Amazon!
Try StreamFab Downloader and download streaming video from Netflix, Amazon!
+ Reply to Thread
Results 1 to 15 of 15
-
Last edited by A_n_g_e_l_a; 5th Jun 2024 at 07:10.
Noob Starter Pack. Just download every Widevine mpd! Not kidding!.
https://files.videohelp.com/u/301890/hellyes6.zip -
Thanks @A_n_g_e_l_a
Here are some extra tips for newbies.
1. Get ready to use CDM from this thread
2. To generate WVD from CDM - refer this thread
3. How to use curlconverter.com to generate required headers - refer this postLast edited by LittleSoldier; 5th Jun 2024 at 11:00. Reason: fixed broken link of curlconverter.com
-
A somewhat extended version of Angela's script that can handle more variants of how the challenge is posted to the license server and license data is returned back:
Code:from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH import base64 import httpx import re import urllib.parse #################### #BE SURE TO EDIT THIS !!!! #################### WVD_PATH = "<your path to>/device.wvd" from headers import headers try: from headers import data except: data = None def get_key(pssh, license_url): device = Device.load(WVD_PATH) cdm = Cdm.from_device(device) session_id = cdm.open() challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) if data: # rte.ie e.g. if match := re.search(r'"(CAES.*?)"', data): challenge = data.replace(match.group(1), base64.b64encode(challenge).decode()) elif match := re.search(r'=(CAES.*?)(&.*)?$', data): b64challenge = base64.b64encode(challenge).decode() quoted = urllib.parse.quote_plus(b64challenge) challenge = lic_ctx.data.replace(match.group(1), quoted) license = httpx.post(url=license_url, data=challenge, headers=headers) try: license.raise_for_status() except requests.HTTPError as e: raise e license_content = license.content try: # if content is returned as JSON object: match = re.search(r'"(CAIS.*?)"', license.content.decode('utf-8')) if match: license_content = match.group(1) except: pass cdm.parse_license(session_id, license_content) keys = [] for key in cdm.get_keys(session_id): if key.type == 'CONTENT': keys.append(f"--key {key.kid.hex}:{key.key.hex()}") cdm.close(session_id) return "\n".join(keys) if __name__ == "__main__": pssh_str = input("PSSH? ") lic_url = input("License URL? ") result = get_key(pssh_str, lic_url) print(result)
- if the license server expects the challenge as base64 encoded value either in a JSON object or as form data (can be seen, if the data contains "CAES..."), replace the original challenge data from the browser in the expected format by the challenge that our own CDM produced.
- if the response from the license server contains the license data in base64 format (can be seen, if it contains "CAIS..."), then only the relevant license data is extracted before parsing the license.
This is usually meant when talking about "you need a custom script".
rte.ie e.g. needs the challenge to be posted as JSON object and returns the license data as JSON object.Last edited by Obo; 5th Jun 2024 at 13:38.
-
And moving from Obo's complex solution to the simplest possible; I note that pywidevine itself will fetch keys for sites that need no headers and talk together in bytes.
Code:pywidevine license -t STREAMING <path to >/device.wvd pssh license_url
[Attachment 79715 - Click to enlarge]Noob Starter Pack. Just download every Widevine mpd! Not kidding!.
https://files.videohelp.com/u/301890/hellyes6.zip -
All well and good if it works as posted. There two errors
- except requests.HTTPError as e:
I happen to use httpx so requests has not been imported - lic_ctx
lic_ctx is an unknown variable. So wherever you pasted this from, you didn't catch the initialization.
This adaptation works as obo intended.
Code:""" Retrieves the keys from a license server using the provided PSSH and license URL. Args: pssh (str): The PSSH (Protection System Specific Header) of the content. license_url (str): The URL of the license server. Returns: str: A string containing the keys in the format "--key <kid>:<key>". Raises: httpx.HTTPStatusError: If the HTTP request to the license server fails. Be sure to populate headers.py with a cURL of the license URL converted at curlconverter.com """ from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH import base64 import httpx import re import urllib.parse #################### #BE SURE TO EDIT THIS !!!! #################### WVD_PATH = "<path to your>/device.wvd" global data from headers import headers try: from headers import data except: data = None def get_key(pssh, license_url): device = Device.load(WVD_PATH) cdm = Cdm.from_device(device) session_id = cdm.open() challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) if data: # rte.ie e.g. if match := re.search(r'"(CAES.*?)"', data): challenge = data.replace(match.group(1), base64.b64encode(challenge).decode()) elif match := re.search(r'=(CAES.*?)(&.*)?$', data): b64challenge = base64.b64encode(challenge).decode() quoted = urllib.parse.quote_plus(b64challenge) challenge = data.replace(match.group(1), quoted) # Prepare the final payload payload = challenge if data is None else challenge license_response = httpx.post(url=license_url, data=payload, headers=headers) try: license_response.raise_for_status() except httpx.HTTPStatusError as e: raise e license_content = license_response.content try: # if content is returned as JSON object: match = re.search(r'"(CAIS.*?)"', license_response.content.decode('utf-8')) if match: license_content = base64.b64decode(match.group(1)) except: pass # Ensure license_content is in the correct format if isinstance(license_content, str): license_content = base64.b64decode(license_content) cdm.parse_license(session_id, license_content) keys = [] for key in cdm.get_keys(session_id): if key.type == 'CONTENT': keys.append(f"--key {key.kid.hex}:{key.key.hex()}") cdm.close(session_id) return "\n".join(keys) if __name__ == "__main__": pssh_str = input("PSSH? ") lic_url = input("License URL? ") result = get_key(pssh_str, lic_url) print(result)
Last edited by A_n_g_e_l_a; 7th Jun 2024 at 10:19. Reason: addded reviced code from Obo
Noob Starter Pack. Just download every Widevine mpd! Not kidding!.
https://files.videohelp.com/u/301890/hellyes6.zip - except requests.HTTPError as e:
-
Yes thanks, I don't have httpx installed (pywidevine uses "requests" as well) and changed the import to an alias of requests, when I edited the script
The lic_ctx was a Dataclass that I now have removed as well, didn't test it after the change.
Locally I've refactored it further: the script now has two functions "prepare_challenge" and "process_license_response" to factor out the base64 stuff. Makes it more readable. Signature of the get_keys function now is (I pass the cdm from outside, because I've put this stuff into a package file):
Code:def get_keys( pssh: str | PSSH, cdm: Cdm, url: str, params: dict = None, headers: dict = None, data: str = None ) -> list:
-
I've added the grand-daddy of l3s to UK-FTA but standalone is newly posted here https://forum.videohelp.com/threads/407216-Decryption-The-Dungeon-of-Despair#post2669285 I call it hell3.py
Noob Starter Pack. Just download every Widevine mpd! Not kidding!.
https://files.videohelp.com/u/301890/hellyes6.zip -
-
Noob Starter Pack. Just download every Widevine mpd! Not kidding!.
https://files.videohelp.com/u/301890/hellyes6.zip -
Last edited by LittleSoldier; 14th Jun 2024 at 12:52.
-
I have this error
Traceback (most recent call last):
File "C:\Users\next\Desktop\init_to_pssh\HellYes2\hell3 .py", line 84, in <module>
result = get_key(pssh_str, lic_url)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\next\Desktop\init_to_pssh\HellYes2\hell3 .py", line 71, in get_key
cdm.parse_license(session_id, license_content)
File "C:\Users\next\AppData\Local\Programs\Python\Pytho n312\Lib\site-packages\pywidevine\cdm.py", line 396, in parse_license
raise InvalidLicenseMessage("Cannot parse an empty license_message")
pywidevine.exceptions.InvalidLicenseMessage: Cannot parse an empty license_messageLast edited by ORSA; 29th Jul 2024 at 14:58.
-
Thanks A_n_g_e_l_a for sharing!
I try to download the file ,which show at the 1# post, "All files are here https://files.videohelp.com/u/301890/L3_PY.zip", but it prompt "Error - File has been deleted.".
Could you help to upload the file again, or is it this file had merged to other file?
Thanks again!have a good weekend!
Similar Threads
-
My Pywidevine API Server
By ninjajiraiya in forum Video Streaming DownloadingReplies: 24Last Post: 27th Mar 2024, 03:53 -
Download tool which supports WKS-Keys/pywidevine or cloud
By pimboli in forum Video Streaming DownloadingReplies: 7Last Post: 27th Nov 2023, 10:17 -
Help needed with ModuleNotFoundError: No module named 'pywidevine.L3'
By LastResort in forum Video Streaming DownloadingReplies: 1Last Post: 24th Sep 2023, 02:07 -
help with pywidevine
By killua in forum Video Streaming DownloadingReplies: 1Last Post: 12th Jul 2023, 02:56 -
No Module Named pywidevine.l3
By myemailjobayer in forum Video Streaming DownloadingReplies: 3Last Post: 23rd Jun 2023, 17:09