hi
* fixed script (changed token entry from 'token' to 'jwt') * added subtitles * removed bunch of unused files * idk misc fixes prob check diff
This commit is contained in:
parent
e823f88d2b
commit
bf8f8f8a0f
3
.gitignore
vendored
3
.gitignore
vendored
@ -20,6 +20,3 @@ urls.txt
|
|||||||
urls/
|
urls/
|
||||||
cdm/
|
cdm/
|
||||||
*.mp4
|
*.mp4
|
||||||
|
|
||||||
# misc
|
|
||||||
test.py
|
|
||||||
|
35
cdrm.py
35
cdrm.py
@ -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()
|
|
56
cdrm_api.py
56
cdrm_api.py
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
83
gettoken.py
83
gettoken.py
@ -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}')
|
|
66
init_pssh.py
66
init_pssh.py
@ -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")
|
|
36
ism.py
36
ism.py
@ -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()
|
|
43
main.py
43
main.py
@ -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()
|
|
58
main_dsnp.py
58
main_dsnp.py
@ -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()
|
|
42
main_m3u8.py
42
main_m3u8.py
@ -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()
|
|
@ -17,9 +17,10 @@ from fake_useragent import UserAgent # sets useragent
|
|||||||
import concurrent.futures # concurrent downloads when using a -file
|
import concurrent.futures # concurrent downloads when using a -file
|
||||||
from cdm.wks import WvDecrypt, device_android_generic, PsshExtractor, KeyExtractor
|
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 = {
|
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': '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',
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
@ -52,7 +53,7 @@ elif args.file:
|
|||||||
elif args.url:
|
elif args.url:
|
||||||
urls = [args.url]
|
urls = [args.url]
|
||||||
else:
|
else:
|
||||||
print("ERR: Please input your URL.")
|
print("ERR: Please input your URL(s).")
|
||||||
print("-url: input NPO video URL")
|
print("-url: input NPO video URL")
|
||||||
print("-file: input a file with NPO video URLS, one per line")
|
print("-file: input a file with NPO video URLS, one per line")
|
||||||
exit()
|
exit()
|
||||||
@ -62,6 +63,7 @@ def find_cookies():
|
|||||||
print("NPO Plus subscribers are able to download in 1080p instead of 540p.")
|
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)")
|
print("Are you an NPO Plus subscriber and logged in on your browser? (y/N)")
|
||||||
userinput = input().lower()
|
userinput = input().lower()
|
||||||
|
print("\033[F\033[K\033[F\033[K\033[F\033[K")
|
||||||
if not userinput or userinput.lower() != 'y':
|
if not userinput or userinput.lower() != 'y':
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -123,8 +125,9 @@ def find_CSRF(targetId, plus_cookie):
|
|||||||
'productId': targetId,
|
'productId': targetId,
|
||||||
}
|
}
|
||||||
|
|
||||||
response_token = requests.get('https://npo.nl/start/api/domain/player-token', cookies=response_cookies, headers=headers, params=json_productId)
|
url = f'https://npo.nl/start/api/domain/player-token'
|
||||||
token = response_token.json()["token"]
|
response_token = requests.get(url, cookies=response_cookies, headers=headers, params=json_productId)
|
||||||
|
token = response_token.json()["jwt"]
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
@ -137,7 +140,6 @@ def find_MPD(token, url, plus_cookie):
|
|||||||
'drmType': 'widevine',
|
'drmType': 'widevine',
|
||||||
'referrerUrl': url,
|
'referrerUrl': url,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post('https://prod.npoplayer.nl/stream-link', headers=headers, json=json_auth, cookies=plus_cookie)
|
response = requests.post('https://prod.npoplayer.nl/stream-link', headers=headers, json=json_auth, cookies=plus_cookie)
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
stream_data = response_data.get('stream', {})
|
stream_data = response_data.get('stream', {})
|
||||||
@ -208,9 +210,13 @@ def create_filename(url, programKey):
|
|||||||
filename = filename_enc.replace("_encrypted", "")
|
filename = filename_enc.replace("_encrypted", "")
|
||||||
return filename_enc, filename
|
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):
|
subtitle_url = f'https://cdn.npoplayer.nl/subtitles/nl/{productId}.vtt'
|
||||||
# output: filename.m4a (audio) and filename.mp4 (video)
|
response = requests.get(subtitle_url)
|
||||||
|
with open(f"{filename}.vtt", 'wb') as subtitle_file:
|
||||||
|
subtitle_file.write(response.content)
|
||||||
if windows_flag == True:
|
if windows_flag == True:
|
||||||
subprocess.run(['N_m3u8DL-RE.exe', '--auto-select', '--no-log', '--save-name', filename_enc, mpd_url], stdout=subprocess.DEVNULL)
|
subprocess.run(['N_m3u8DL-RE.exe', '--auto-select', '--no-log', '--save-name', filename_enc, mpd_url], stdout=subprocess.DEVNULL)
|
||||||
else:
|
else:
|
||||||
@ -228,11 +234,16 @@ def decrypt(key, filename_enc, filename):
|
|||||||
|
|
||||||
def merge(filename):
|
def merge(filename):
|
||||||
ffmpeg_command = [
|
ffmpeg_command = [
|
||||||
'ffmpeg', '-v', 'quiet', # '-stats',
|
'ffmpeg', '-v', 'quiet', # '-v stats',
|
||||||
'-i', filename + "_video.mp4",
|
'-i', filename + "_video.mp4",
|
||||||
'-i', filename + "_audio.m4a",
|
'-i', filename + "_audio.m4a",
|
||||||
'-c:v', 'copy',
|
'-i', filename + ".vtt", # Subtitle file
|
||||||
'-c:a', 'copy',
|
'-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',
|
'-strict', 'experimental',
|
||||||
filename + ".mp4"
|
filename + ".mp4"
|
||||||
]
|
]
|
||||||
@ -245,6 +256,7 @@ def clean(filename_enc, filename):
|
|||||||
os.remove(filename_enc + ".m4a")
|
os.remove(filename_enc + ".m4a")
|
||||||
os.remove(filename + "_audio.m4a")
|
os.remove(filename + "_audio.m4a")
|
||||||
os.remove(filename + "_video.mp4")
|
os.remove(filename + "_video.mp4")
|
||||||
|
os.remove(filename + ".vtt")
|
||||||
|
|
||||||
|
|
||||||
def check_file(filename):
|
def check_file(filename):
|
||||||
@ -263,7 +275,7 @@ def execute(url, plus_cookie, process_no):
|
|||||||
key = find_key(mpd, pssh)
|
key = find_key(mpd, pssh)
|
||||||
check_prereq()
|
check_prereq()
|
||||||
filename_enc, filename = create_filename(url, programKey)
|
filename_enc, filename = create_filename(url, programKey)
|
||||||
download(mpd_url, filename_enc)
|
download(mpd_url, filename_enc, productId, filename)
|
||||||
decrypt(key, filename_enc, filename)
|
decrypt(key, filename_enc, filename)
|
||||||
merge(filename)
|
merge(filename)
|
||||||
clean(filename_enc, 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)]
|
futures = [executor.submit(execute, url, plus_cookie, i + 1) for i, url in enumerate(urls)]
|
||||||
|
|
||||||
completed_videos = 0
|
completed_videos = 0
|
||||||
|
print(f"0/{len(urls)} videos completed")
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
result = future.result()
|
result = future.result()
|
||||||
completed_videos += 1
|
completed_videos += 1
|
||||||
|
print("\033[F\033[K\033[F\033[K")
|
||||||
print(f"{completed_videos}/{len(urls)} video{'s'[:len(urls) != 1]} completed")
|
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.
|
# 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
|
# 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:
|
# Supported browsers for NPO Plus cookies:
|
||||||
# (https://github.com/borisbabic/browser_cookie3#testing-dates--ddmmyy)
|
# (https://github.com/borisbabic/browser_cookie3#testing-dates--ddmmyy)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user