From bf8f8f8a0ff5bfd10afd90018b402d0ff2250c2c Mon Sep 17 00:00:00 2001 From: Quinten0508 <55107945+Quinten0508@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:26:17 +0100 Subject: [PATCH] hi * fixed script (changed token entry from 'token' to 'jwt') * added subtitles * removed bunch of unused files * idk misc fixes prob check diff --- .gitignore | 5 +-- cdrm.py | 35 ------------------ cdrm_api.py | 56 ----------------------------- cdrm_cache.py | 35 ------------------ extractwvd.py | 92 ----------------------------------------------- gettoken.py | 83 ------------------------------------------ init_pssh.py | 66 ---------------------------------- ism.py | 36 ------------------- main.py | 43 ---------------------- main_dsnp.py | 58 ------------------------------ main_m3u8.py | 42 ---------------------- npo all-in-one.py | 43 ++++++++++++++-------- 12 files changed, 30 insertions(+), 564 deletions(-) delete mode 100644 cdrm.py delete mode 100644 cdrm_api.py delete mode 100644 cdrm_cache.py delete mode 100644 extractwvd.py delete mode 100644 gettoken.py delete mode 100644 init_pssh.py delete mode 100644 ism.py delete mode 100644 main.py delete mode 100644 main_dsnp.py delete mode 100644 main_m3u8.py diff --git a/.gitignore b/.gitignore index 4c2ef5c..c9a97f0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,4 @@ env/ urls.txt urls/ cdm/ -*.mp4 - -# misc -test.py +*.mp4 \ No newline at end of file diff --git a/cdrm.py b/cdrm.py deleted file mode 100644 index cae5878..0000000 --- a/cdrm.py +++ /dev/null @@ -1,35 +0,0 @@ -import argparse -import requests -from cdm.wks import PsshExtractor, get_keys_license_cdrm_project, print_keys_cdrm_project - -token = "" - -def main(): - parser = argparse.ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") - parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") - parser.add_argument("-lic", required=True, help="URL of the license server") - args = parser.parse_args() - - mpd_url = args.mpd - license_url = args.lic - - headers_mpd = { - 'origin': 'https://play.hbomax.com', - 'referer': 'https://play.hbomax.com/', - } - - response = requests.get(mpd_url, headers=headers_mpd) - pssh_extractor = PsshExtractor(response.text) - pssh_value = pssh_extractor.extract_pssh() - - print("PSSH value:", pssh_value) - - headers_license = { - 'authorization': f'Bearer {token}', - } - - response = get_keys_license_cdrm_project(license_url, headers_license, pssh_value) - print_keys_cdrm_project(response) - -if __name__ == "__main__": - main() diff --git a/cdrm_api.py b/cdrm_api.py deleted file mode 100644 index 415981d..0000000 --- a/cdrm_api.py +++ /dev/null @@ -1,56 +0,0 @@ -import requests -from cdm.wks import PsshExtractor, get_keys_cdrm_api -# HBOMAX Test -def parse_command_line_arguments(): - """Parse command line arguments.""" - parser = __import__('argparse').ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") - parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") - parser.add_argument("-lic", required=True, help="URL of the license server") - return parser.parse_args() - -def get_mpd_response(mpd_url): - """Get MPD manifest response.""" - mpd_headers = { - 'origin': 'https://play.hbomax.com', - 'referer': 'https://play.hbomax.com/', - } - return requests.get(mpd_url, headers=mpd_headers) - -def get_license_headers(token): - """Get headers for the license server request.""" - return { - 'accept': "*/*", # no delet - 'content-length': "316", # no delet - 'Connection': 'keep-alive', # no delet - 'authorization': f'Bearer {token}', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Ktesttemp, like Gecko) Chrome/90.0.4430.85 Safari/537.36' - } - -def main(): - # Parse command line arguments - args = parse_command_line_arguments() - mpd_url, license_url = args.mpd, args.lic - - # Get MPD response - mpd_response = get_mpd_response(mpd_url) - - # Extract PSSH value from MPD response - pssh_extractor = PsshExtractor(mpd_response.text) - pssh_value = pssh_extractor.extract_pssh() - - print("PSSH value:", pssh_value) - - # Get headers for the license server request - token = "" - # Update with your actual token - license_headers = get_license_headers(token) - - # Call the function in keys.py to get the keys - keys = get_keys_cdrm_api(license_headers, license_url, pssh_value) - - # Process each key - for key in keys: - print(f'KEY: {key}') - -if __name__ == "__main__": - main() diff --git a/cdrm_cache.py b/cdrm_cache.py deleted file mode 100644 index 98fb5ad..0000000 --- a/cdrm_cache.py +++ /dev/null @@ -1,35 +0,0 @@ -import argparse -import requests -from cdm.wks import PsshExtractor, get_keys_cache_cdrm_project - -def extract_pssh_value(mpd_url): - headers_mpd = { - 'origin': 'https://play.hbomax.com', - 'referer': 'https://play.hbomax.com/', - } - - response = requests.get(mpd_url, headers=headers_mpd) - - if response.status_code == 200: - pssh_extractor = PsshExtractor(response.text) - pssh_value = pssh_extractor.extract_pssh() - return pssh_value - else: - raise ValueError(f"Error: Unable to fetch MPD manifest, Status Code: {response.status_code}") - -def main(): - parser = argparse.ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") - parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") - args = parser.parse_args() - - mpd_url = args.mpd - - try: - pssh_value = extract_pssh_value(mpd_url) - print("PSSH value:", pssh_value) - get_keys_cache_cdrm_project(pssh_value) - except Exception as e: - print(f"An error occurred: {e}") - -if __name__ == "__main__": - main() diff --git a/extractwvd.py b/extractwvd.py deleted file mode 100644 index 908a5b5..0000000 --- a/extractwvd.py +++ /dev/null @@ -1,92 +0,0 @@ -import argparse -import json -from enum import Enum -from pathlib import Path -from construct import BitStruct, Bytes, Const -from construct import Enum as CEnum -from construct import Flag, If, Int8ub, Int16ub, Optional, Padded, Padding, Struct, this -from Cryptodome.PublicKey import RSA -from cdm.wks import ClientIdentification - -class DeviceTypes(Enum): - CHROME = 1 - ANDROID = 2 - -WidevineDeviceStruct = Struct( - 'signature' / Const(b'WVD'), - 'version' / Int8ub, - 'type' / CEnum( - Int8ub, - **{t.name: t.value for t in DeviceTypes} - ), - 'security_level' / Int8ub, - 'flags' / Padded(1, Optional(BitStruct( - Padding(7), - 'send_key_control_nonce' / Flag - ))), - 'private_key_len' / Int16ub, - 'private_key' / Bytes(this.private_key_len), - 'client_id_len' / Int16ub, - 'client_id' / Bytes(this.client_id_len), - 'vmp_len' / Optional(Int16ub), - 'vmp' / If(this.vmp_len, Optional(Bytes(this.vmp_len))) -) - -WidevineDeviceStructVersion = 1 - -def parse_args(): - parser = argparse.ArgumentParser(description='Widevine Device Information Parser') - parser.add_argument('file', type=Path, help='Path to WVD file') - return parser.parse_args() - -def write_key_and_blob_files(out_dir, device): - private_key_file = out_dir / 'device_private_key' - print(f'\n[INFO] Writing private key to: {private_key_file}') - private_key = RSA.import_key(device.private_key) - private_key_file.write_text(private_key.export_key('PEM').decode()) - - client_id_blob_file = out_dir / 'device_client_id_blob' - print(f'[INFO] Writing client ID blob to: {client_id_blob_file}') - client_id_blob_file.write_bytes(device.client_id) - - if device.vmp: - vmp_blob_file = out_dir / 'device_vmp_blob' - print(f'[INFO] Writing VMP blob to: {vmp_blob_file}') - vmp_blob_file.write_bytes(device.vmp) - -def write_json_file(out_dir, name, client_id, device): - wv_json_file = out_dir / 'wv.json' - description = f'{name} ({client_id.Token._DeviceCertificate.SystemId})' - print(f'[INFO] Writing JSON file to: {wv_json_file}') - wv_json_file.write_text(json.dumps({ - 'name': name, - 'description': description, - 'security_level': device.security_level, - 'session_id_type': device.type.lower(), - 'private_key_available': True, - 'vmp': bool(device.vmp), - 'send_key_control_nonce': device.type == DeviceTypes.ANDROID - }, indent=2)) - -def main(): - args = parse_args() - - name = args.file.with_suffix('').name - out_dir = Path.cwd() / 'cdm' / 'devices' / 'android_generic' - out_dir.mkdir(parents=True, exist_ok=True) - - with args.file.open('rb') as fd: - device = WidevineDeviceStruct.parse_stream(fd) - - print(f'\n[INFO] Starting Widevine Device Information Parsing') - write_key_and_blob_files(out_dir, device) - - client_id = ClientIdentification() - client_id.ParseFromString(device.client_id) - - write_json_file(out_dir, name, client_id, device) - - print('[INFO] Done') - -if __name__ == '__main__': - main() diff --git a/gettoken.py b/gettoken.py deleted file mode 100644 index f636df7..0000000 --- a/gettoken.py +++ /dev/null @@ -1,83 +0,0 @@ -import requests - -headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', - 'Accept': '*/*', - 'Accept-Language': 'en-US,en;q=0.5', - # 'Accept-Encoding': 'gzip, deflate, br', - 'Referer': 'https://npo.nl/start/serie/nos-journaal/seizoen-328/nos-journaal_91491/afspelen', - 'Content-Type': 'application/json', -# 'sentry-trace': 'adf1091e221d423fa031623483d5a75f-b2cd5555b5236103-0', - 'baggage': 'sentry-environment=prod,sentry-release=7-1xQyxcfFi-KAQG1GEsk,sentry-public_key=ff5dbc8fc4e94b9390c3581c962b975a,sentry-trace_id=adf1091e221d423fa031623483d5a75f,sentry-transaction=%2Fserie%2F%5BseriesSlug%5D%2F%5B%5B...seriesParams%5D%5D,sentry-sampled=false', - 'DNT': '1', - 'Connection': 'keep-alive', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache', - # Requests doesn't support trailers - # 'TE': 'trailers', -} -session = requests.Session() -response = session.get('https://npo.nl/start/api/auth/session', headers=headers) - -response_cookies = session.cookies.get_dict() -# dict: -# { -# '__Host-next-auth.csrf-token' : '73bcedab2973fc2547d6a8f4405da66ac1dbeb432e21ba209ed93dac14dfbe88%7Cf4ed5605a6f0b044e80da012b9705c9d915a6603e2109d38ebff47823d58acc3', -# '__Secure-next-auth.callback-url' : 'https%3A%2F%2Fnpo.nl' -# } - -csrf = response_cookies["__Host-next-auth.csrf-token"] -print(f'csrf: {csrf}') - - - - -cookies2 = { - '__Host-next-auth.csrf-token': csrf, - '__Secure-next-auth.callback-url': 'https%3A%2F%2Fnpo.nl', -# 'CCM_Wrapper_Cache': 'eyJ2ZXIiOiJ2My4yLjgiLCJqc2giOiIiLCJjaWQiOiJWSHNva3FFUkk2TVozbTI1IiwiY29uaWQiOiJXR0ptTCJ9', - 'pa_privacy': '%22optin%22', - '_pcid': '%7B%22browserId%22%3A%22lr5dxgvg8okqb7ru%22%2C%22_t%22%3A%22m6tsuy7i%7Clr5dxgvi%22%7D', - '_pctx': '%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbAGz4IYAJ4B2AFYAfVDACsrAB4BzAG5SQAXyA', - 'Cookie_Consent': 'false', - 'CCM_ID': 'VHsokqERI6MZ3m25', - 'Cookie_Category_Necessary': 'true', - 'Cookie_Category_Analytics': 'true', -# 'Cookie_Category_Social': '', -# 'bitmovin_analytics_uuid': 'f6cdf35e-618f-4b82-8bec-2a543be32390', -} - -headers2 = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en-US,en;q=0.5', - # 'Accept-Encoding': 'gzip, deflate, br', - 'Content-Type': 'application/json', -# 'sentry-trace': 'da0c64681077477196b6c39ce00fe616-b275f8f0de3ad282-0', -# 'baggage': 'sentry-environment=prod,sentry-release=7-1xQyxcfFi-KAQG1GEsk,sentry-public_key=ff5dbc8fc4e94b9390c3581c962b975a,sentry-trace_id=da0c64681077477196b6c39ce00fe616', - 'Origin': 'https://npo.nl', - 'DNT': '1', - 'Connection': 'keep-alive', - 'Referer': 'https://npo.nl/start/serie/nos-journaal/seizoen-328/nos-journaal_91491/afspelen', - # 'Cookie': '__Host-next-auth.csrf-token=e47c6a34c0fe5a4d68821408a2b1271ca16d7ae435f8ca6ddb1f088f00f4ec84%7C8b09f39a2efa3e8229eef01fffb4a41ee08e19996f0e6ca860260e68aac1d682; __Secure-next-auth.callback-url=https%3A%2F%2Fnpo.nl; CCM_Wrapper_Cache=eyJ2ZXIiOiJ2My4yLjgiLCJqc2giOiIiLCJjaWQiOiJWSHNva3FFUkk2TVozbTI1IiwiY29uaWQiOiJXR0ptTCJ9; pa_privacy=%22optin%22; _pcid=%7B%22browserId%22%3A%22lr5dxgvg8okqb7ru%22%2C%22_t%22%3A%22m6tsuy7i%7Clr5dxgvi%22%7D; _pctx=%7Bu%7DN4IgrgzgpgThIC4B2YA2qA05owMoBcBDfSREQpAeyRCwgEt8oBJAE0RXSwH18yBbAGz4IYAJ4B2AFYAfVDACsrAB4BzAG5SQAXyA; Cookie_Consent=false; CCM_ID=VHsokqERI6MZ3m25; Cookie_Category_Necessary=true; Cookie_Category_Analytics=true; Cookie_Category_Social=; bitmovin_analytics_uuid=f6cdf35e-618f-4b82-8bec-2a543be32390', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache', - # Requests doesn't support trailers - # 'TE': 'trailers', -} - -#json_data = { -# 'productId': 'POW_05759713', -#} - -response2 = requests.post('https://npo.nl/start/api/domain/player-token', cookies=cookies2, headers=headers2) - -#videoplayer token -token = response2.json()["token"] -print(f'token: {token}') diff --git a/init_pssh.py b/init_pssh.py deleted file mode 100644 index d33b44e..0000000 --- a/init_pssh.py +++ /dev/null @@ -1,66 +0,0 @@ -import base64 -import sys -from pathlib import Path -from google.protobuf.message import DecodeError -from cdm.wks import WidevineCencHeader - -# File path to read the raw data -file_path = "your init" - -# Read the raw data from the file -raw = Path(file_path).read_bytes() - -# Find the offset of 'pssh' in the raw data -pssh_offset = raw.rfind(b'pssh') - -if pssh_offset == -1: - print("[ERROR] 'pssh' not found in the file.") - sys.exit(1) -else: - # Extract the PSSH data based on the offset and length information - _start = max(pssh_offset - 4, 0) - _end = min(pssh_offset - 4 + raw[pssh_offset - 1], len(raw)) - pssh = raw[_start:_end] - - # Display the PSSH data in base64 format - print('\n[INFO] PSSH:', base64.b64encode(pssh).decode('utf-8')) - pssh_b64 = base64.b64encode(pssh) - print("\n[SUCCESS] PSSH extracted successfully.") - -# Check if the PSSH data needs modification -if not pssh[12:28] == bytes([237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]): - print("[Modifying PSSH data...]") - # Create a new PSSH data with the required modifications - new_pssh = bytearray([0, 0, 0]) - new_pssh.append(32 + len(pssh)) - new_pssh[4:] = bytearray(b'pssh') - new_pssh[8:] = [0, 0, 0, 0] - new_pssh[13:] = [237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237] - new_pssh[29:] = [0, 0, 0, 0] - new_pssh[31] = len(pssh) - new_pssh[32:] = pssh - pssh_b64 = base64.b64encode(new_pssh) - print("[Modified PSSH data:", pssh_b64.decode(), "]") -else: - print("[PSSH data doesn't need modification.]") - -# Parse the modified or original PSSH data using WidevineCencHeader -parsed_init_data = WidevineCencHeader() - -try: - parsed_init_data.ParseFromString(base64.b64decode(pssh_b64)) - print("[PSSH data parsed successfully.]") -except (DecodeError, SystemError) as e: - print("[Error parsing PSSH data:", e, "]") - try: - # Attempt to parse PSSH data from byte offset 32 - id_bytes = parsed_init_data.ParseFromString(base64.b64decode(pssh_b64)[32:]) - print("[PSSH data parsed successfully from byte offset 32.]") - except DecodeError as de: - print("[Error parsing PSSH data from byte offset 32:", de, "]") - sys.exit(1) - -# Convert the parsed key_id to a hexadecimal string -key_id_str = ''.join(['{:02x}'.format(b) for b in parsed_init_data.key_id[0]]) -print("[Key ID in hexadecimal:", key_id_str, "]") -print("\n[SUCCESS] [Done]\n") \ No newline at end of file diff --git a/ism.py b/ism.py deleted file mode 100644 index cb83d5b..0000000 --- a/ism.py +++ /dev/null @@ -1,36 +0,0 @@ -import argparse -from cdm.wks import parse_manifest_ism - -def main(): - # Create an ArgumentParser object and add the 'urls' argument - parser = argparse.ArgumentParser(description='Script for parsing Smooth Streaming manifest URLs.') - parser.add_argument('urls', - help='The URLs to parse. You may need to wrap the URLs in double quotes if you have issues.', - nargs='+') - - # Parse the arguments - args = parser.parse_args() - - # Iterate over the provided URLs - for manifest_link in args.urls: - kid, stream_info_list, encoded_string = parse_manifest_ism(manifest_link) - - # Print information for each stream - for stream_info in stream_info_list: - type_info = stream_info['type'] - codec = stream_info['codec'] - bitrate = stream_info['bitrate'] - resolution = stream_info['resolution'] - - if type_info == 'video': - print(f'[INFO] VIDEO - Codec: {codec}, Resolution: {resolution}, Bitrate: {bitrate}') - elif type_info == 'audio': - language = stream_info['language'] - track_id = stream_info['track_id'] - print(f'[INFO] AUDIO - Codec: {codec}, Bitrate: {bitrate}, Language: {language}, Track ID: {track_id}') - - # Print PSSH information - print('\n[INFO] PSSH:', encoded_string) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index de61670..0000000 --- a/main.py +++ /dev/null @@ -1,43 +0,0 @@ -import argparse -from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor -import requests - -def get_keys_license(mpd_url, license_url): - response = requests.get(mpd_url) - pssh_extractor = PsshExtractor(response.text) - pssh_value = pssh_extractor.extract_pssh() - - print("PSSH value:", pssh_value) - - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', - } - - cert_b64 = None - key_extractor = KeyExtractor(pssh_value, cert_b64, license_url, headers) - keys = key_extractor.get_keys() - wvdecrypt = WvDecrypt(init_data_b64=pssh_value, cert_data_b64=cert_b64, device=device_android_generic) - raw_challenge = wvdecrypt.get_challenge() - data = raw_challenge - - return keys - -def main(): - parser = argparse.ArgumentParser(description="Decrypt Widevine content using MPD URL and License URL") - parser.add_argument("-mpd", required=True, help="URL of the MPD manifest") - parser.add_argument("-lic", required=True, help="URL of the license server") - args = parser.parse_args() - - mpd_url = args.mpd - license_url = args.lic - - keys = get_keys_license(mpd_url, license_url) - - for key in keys: - if isinstance(key, list): - if key: - for key_str in key: - print(f"KEY: {key_str}") - -if __name__ == "__main__": - main() diff --git a/main_dsnp.py b/main_dsnp.py deleted file mode 100644 index 1347a36..0000000 --- a/main_dsnp.py +++ /dev/null @@ -1,58 +0,0 @@ -import argparse -from cdm.wks import KeyExtractor, DataExtractor_DSNP -import requests -token = "" -def get_keys_license(m3u8_url): - response = requests.get(m3u8_url) - content = response.text if response.status_code == 200 else None - - data_extractor = DataExtractor_DSNP(content) - - if content: - characteristics_list = data_extractor.get_characteristics_list() - - if characteristics_list: - print("Choose CHARACTERISTICS Value:") - for i, (characteristics, _) in enumerate(characteristics_list): - print(f"{i + 1}. {characteristics}") - - choice = int(input("Enter the number of the CHARACTERISTICS you want: ")) - characteristics, base64_data = data_extractor.extract_base64_by_choice(choice) - - if characteristics and base64_data: - print("CHARACTERISTICS Value:", characteristics) - - print("PSSH value (Base64 Data):", base64_data) - - license_url = "https://disney.playback.edge.bamgrid.com/widevine/v1/obtain-license" - - headers = { - 'authorization': f'Bearer {token}', - 'origin': 'https://www.disneyplus.com', - 'referer': 'https://www.disneyplus.com/', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', - } - - cert_b64 = None - key_extractor = KeyExtractor(base64_data, cert_b64, license_url, headers) - keys = key_extractor.get_keys() - - for key in keys: - if isinstance(key, list): - if key: - for key_str in key: - print(f"KEY: {key_str}") - - return base64_data - -def main(): - parser = argparse.ArgumentParser(description="Decrypt Widevine content using M3U8 URL") - parser.add_argument("-m3u8", required=True, help="URL of the M3U8 manifest") - args = parser.parse_args() - - m3u8_url = args.m3u8 - - pssh_value = get_keys_license(m3u8_url) - -if __name__ == "__main__": - main() diff --git a/main_m3u8.py b/main_m3u8.py deleted file mode 100644 index ee14484..0000000 --- a/main_m3u8.py +++ /dev/null @@ -1,42 +0,0 @@ -from cdm.wks import WvDecrypt, device_android_generic, extract_pssh_m3u8, KeyExtractor -import argparse -import requests - -def get_keys_license(m3u8_url, license_url): - response = requests.get(m3u8_url) - pssh_value = extract_pssh_m3u8(response.text) - - print("PSSH value:", pssh_value) - - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', - } - - cert_b64 = None - key_extractor = KeyExtractor(pssh_value, cert_b64, license_url, headers) - keys = key_extractor.get_keys() - wvdecrypt = WvDecrypt(init_data_b64=pssh_value, cert_data_b64=cert_b64, device=device_android_generic) - raw_challenge = wvdecrypt.get_challenge() - data = raw_challenge - - return keys - -def main(): - parser = argparse.ArgumentParser(description="Decrypt Widevine content using M3U8 URL and License URL") - parser.add_argument("-m3u8", required=True, help="URL of the M3U8 manifest") - parser.add_argument("-lic", required=True, help="URL of the license server") - args = parser.parse_args() - - m3u8_url = args.m3u8 - license_url = args.lic - - keys = get_keys_license(m3u8_url, license_url) - - for key in keys: - if isinstance(key, list): - if key: - for key_str in key: - print(f"KEY: {key_str}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/npo all-in-one.py b/npo all-in-one.py index 841d792..7674b0e 100644 --- a/npo all-in-one.py +++ b/npo all-in-one.py @@ -17,9 +17,10 @@ from fake_useragent import UserAgent # sets useragent import concurrent.futures # concurrent downloads when using a -file from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor - +# dont need any of these headers but makes it look like normal clients at least +# for extra "normal behavior": save the UA chosen here in some temp file so we can use the same one every time this utility is run headers = { - 'User-Agent': UserAgent(platforms='pc', min_version=120.0).random, + 'User-Agent': UserAgent(platforms='pc', min_version=122.0).random, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Cache-Control': 'no-cache', @@ -52,7 +53,7 @@ elif args.file: elif args.url: urls = [args.url] else: - print("ERR: Please input your URL.") + print("ERR: Please input your URL(s).") print("-url: input NPO video URL") print("-file: input a file with NPO video URLS, one per line") exit() @@ -62,6 +63,7 @@ def find_cookies(): print("NPO Plus subscribers are able to download in 1080p instead of 540p.") print("Are you an NPO Plus subscriber and logged in on your browser? (y/N)") userinput = input().lower() + print("\033[F\033[K\033[F\033[K\033[F\033[K") if not userinput or userinput.lower() != 'y': return @@ -123,8 +125,9 @@ def find_CSRF(targetId, plus_cookie): 'productId': targetId, } - response_token = requests.get('https://npo.nl/start/api/domain/player-token', cookies=response_cookies, headers=headers, params=json_productId) - token = response_token.json()["token"] + url = f'https://npo.nl/start/api/domain/player-token' + response_token = requests.get(url, cookies=response_cookies, headers=headers, params=json_productId) + token = response_token.json()["jwt"] return token @@ -137,7 +140,6 @@ def find_MPD(token, url, plus_cookie): 'drmType': 'widevine', 'referrerUrl': url, } - response = requests.post('https://prod.npoplayer.nl/stream-link', headers=headers, json=json_auth, cookies=plus_cookie) response_data = response.json() stream_data = response_data.get('stream', {}) @@ -208,9 +210,13 @@ def create_filename(url, programKey): filename = filename_enc.replace("_encrypted", "") return filename_enc, filename +def download(mpd_url, filename_enc, productId, filename): +# output: filename.m4a (audio), filename.mp4 (video), filename.vtt (subtitles) -def download(mpd_url, filename_enc): -# output: filename.m4a (audio) and filename.mp4 (video) + subtitle_url = f'https://cdn.npoplayer.nl/subtitles/nl/{productId}.vtt' + response = requests.get(subtitle_url) + with open(f"{filename}.vtt", 'wb') as subtitle_file: + subtitle_file.write(response.content) if windows_flag == True: subprocess.run(['N_m3u8DL-RE.exe', '--auto-select', '--no-log', '--save-name', filename_enc, mpd_url], stdout=subprocess.DEVNULL) else: @@ -228,13 +234,18 @@ def decrypt(key, filename_enc, filename): def merge(filename): ffmpeg_command = [ - 'ffmpeg', '-v', 'quiet', # '-stats', + 'ffmpeg', '-v', 'quiet', # '-v stats', '-i', filename + "_video.mp4", '-i', filename + "_audio.m4a", - '-c:v', 'copy', - '-c:a', 'copy', + '-i', filename + ".vtt", # Subtitle file + '-c:v', 'copy', # Copy video codec + '-c:a', 'copy', # Copy audio codec + '-c:s', 'mov_text', # Subtitle codec for MP4 + '-map', '0:v:0', # Map video stream + '-map', '1:a:0', # Map audio stream + '-map', '2:s:0', # Map subtitle stream '-strict', 'experimental', - filename + ".mp4" + filename + ".mp4" ] subprocess.run(ffmpeg_command) @@ -244,7 +255,8 @@ def clean(filename_enc, filename): os.remove(filename_enc + ".mp4") os.remove(filename_enc + ".m4a") os.remove(filename + "_audio.m4a") - os.remove(filename + "_video.mp4") + os.remove(filename + "_video.mp4") + os.remove(filename + ".vtt") def check_file(filename): @@ -263,7 +275,7 @@ def execute(url, plus_cookie, process_no): key = find_key(mpd, pssh) check_prereq() filename_enc, filename = create_filename(url, programKey) - download(mpd_url, filename_enc) + download(mpd_url, filename_enc, productId, filename) decrypt(key, filename_enc, filename) merge(filename) clean(filename_enc, filename) @@ -279,9 +291,11 @@ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [executor.submit(execute, url, plus_cookie, i + 1) for i, url in enumerate(urls)] completed_videos = 0 + print(f"0/{len(urls)} videos completed") for future in concurrent.futures.as_completed(futures): result = future.result() completed_videos += 1 + print("\033[F\033[K\033[F\033[K") print(f"{completed_videos}/{len(urls)} video{'s'[:len(urls) != 1]} completed") @@ -290,6 +304,7 @@ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: ######### # The downloader *should* work across every platform, linux/mac/win. # It has not been extensively tested on anything but windows. DM me if you need help :D +# Discord: quinten._. (That includes the ._.) # Supported browsers for NPO Plus cookies: # (https://github.com/borisbabic/browser_cookie3#testing-dates--ddmmyy)