diff --git a/dl.py b/dl.py new file mode 100644 index 0000000..12018aa --- /dev/null +++ b/dl.py @@ -0,0 +1,47 @@ +import sys +import subprocess + +def main(): + args = sys.argv[1:] + + savename = None + if '--save-name' in args: + idx = args.index('--save-name') + if idx + 1 < len(args): + savename = args[idx + 1] + args = args[:idx] + args[idx+2:] + + tasks_result = subprocess.run([sys.executable, 'tasks.py'] + args, capture_output=True, text=True) + dash_link = None + for line in tasks_result.stdout.splitlines(): + if line.startswith("DASH Link:"): + dash_link = line[len("DASH Link:"):].strip() + break + + get_result = subprocess.run([sys.executable, 'keys.py'] + args, capture_output=True, text=True) + get_output = get_result.stdout.strip() + + string = f"DASH Link: {dash_link}\n{get_output}" if dash_link else get_output + + print(string) + if savename: + print(savename) + + command = [ + 'N_m3u8dl-re', + '-sv', 'best', + '-sa', 'best', + '--key', string, + ] + if savename: + command.extend(['--save-name', savename]) + if dash_link: + command.append(dash_link) + + try: + subprocess.run(command, check=True) + except subprocess.CalledProcessError as e: + print(f"Error running N_m3u8dl-re: {e}") + +if __name__ == "__main__": + main() diff --git a/get_lic.py b/get_lic.py new file mode 100644 index 0000000..88abf54 --- /dev/null +++ b/get_lic.py @@ -0,0 +1,43 @@ +import requests +import json + +# Define the API URL +url = "https://api.prd.video.talpa.network/graphql" + +# Prepare the headers +headers = { + "Accept": "*/*", + "Accept-Language": "nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7", + "Content-Type": "text/plain;charset=UTF-8", + "Priority": "u=1, i", + "Sec-CH-UA": "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"", + "Sec-CH-UA-Mobile": "?0", + "Sec-CH-UA-Platform": "\"Windows\"", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", +} + +# Prepare the body of the request +query = { + "query": """query DrmTokenQuery($provider: DrmProvider) { + drmToken(drmProvider: $provider) { + expiration + token + } + }""", + "variables": { + "provider": "JWP" + } +} + +# Send the POST request +response = requests.post(url, headers=headers, data=json.dumps(query)) + +# Print the output +if response.status_code == 200: + # Extract the token + token = response.json()['data']['drmToken']['token'] + print("Token:", token) +else: + print("Error:", response.status_code, response.text) diff --git a/get_vid.py b/get_vid.py new file mode 100644 index 0000000..45c5499 --- /dev/null +++ b/get_vid.py @@ -0,0 +1,73 @@ +import requests +import re +import sys + +def fetch_video_info(guid): + url = f"https://api.prd.video.talpa.network/graphql?query=query+GetVideoQuery%28%24guid%3A%5BString%5D%29%7Bprograms%28guid%3A%24guid%29%7Bitems%7Bguid+type+metadata+availableRegion+...Media+...Tracks+...Sources%7D%7D%7Dfragment+Media+on+Program%7Bmedia%7Btype+availableDate+availabilityState+airedDateTime+expirationDate%7D%7Dfragment+Tracks+on+Program%7Btracks%7Bfile+kind+label%7D%7Dfragment+Sources+on+Program%7Bsources%7Btype+file+drm%7D%7D&variables=%7B%22guid%22%3A%22{guid}%22%7D" + + headers = { + "accept": "*/*", + "accept-language": "nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7", + "content-type": "application/json", + "priority": "u=1, i", + "sec-ch-ua": "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + } + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + json_response = response.json() + + dash_link = None + widevine_license_server = None + + items = json_response.get('data', {}).get('programs', {}).get('items', []) + + for item in items: + for source in item.get('sources', []): + if source['type'] == 'dash' and 'drm' in source and 'widevine' in source['drm']: + dash_link = source['file'] + widevine_license_server = source['drm']['widevine']['url'] + break # Exit loop once we find the first match + + return { + "dash_link": dash_link, + "widevine_license_server": widevine_license_server + } + else: + return {"error": f"Request failed with status code {response.status_code}"} + +def extract_guid(url): + # Check if the URL starts with the required base URL + if not url.startswith("https://www.kijk.nl/programmas/"): + return None + + # Regex to extract GUID (the segment before the last slash) + match = re.match(r'https://www\.kijk\.nl/programmas/[^/]+/([^/?]+)', url) + if match: + return match.group(1) + return None + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python script.py {url}") + sys.exit(1) + + user_url = sys.argv[1] + guid = extract_guid(user_url) + + if guid: + video_info = fetch_video_info(guid) + + if 'error' not in video_info: + print(f"DASH Link: {video_info['dash_link']}") + print(f"Widevine License Server: {video_info['widevine_license_server']}") + else: + print(video_info['error']) + else: + print("Invalid URL. Please ensure it starts with 'https://www.kijk.nl/programmas/' and contains a GUID.") diff --git a/keys.py b/keys.py new file mode 100644 index 0000000..9a93b2e --- /dev/null +++ b/keys.py @@ -0,0 +1,145 @@ +import subprocess +import sys +import requests +import xml.etree.ElementTree as ET +import re +from pywidevine.cdm import Cdm +from pywidevine.device import Device +from pywidevine.pssh import PSSH + + +def call_tasks(subcommand, *args): + command = [sys.executable, 'tasks.py', subcommand] + list(args) + + try: + result = subprocess.run(command, capture_output=True, text=True, check=True) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error executing {subcommand}: {e}") + return None + + +def parse_output(output): + dash_link = None + widevine_license_server = None + token = None + + lines = output.splitlines() + + for line in lines: + if line.startswith("DASH Link:"): + dash_link = line.split("DASH Link: ", 1)[1].strip() + elif line.startswith("Widevine License Server:"): + widevine_license_server = line.split("Widevine License Server: ", 1)[1].strip() + elif line.startswith("Token:"): + token = line.split("Token: ", 1)[1].strip() + + return dash_link, widevine_license_server, token + + +def download_mpd(dash_link): + try: + response = requests.get(dash_link) + response.raise_for_status() + return response.text + except requests.RequestException as e: + print(f"Error downloading MPD file: {e}") + return None + + +def extract_pssh(mpd_content): + pssh_strings = set() + root = ET.fromstring(mpd_content) + + # More flexible approach to find elements regardless of namespaces + for elem in root.iter(): + if elem.tag.endswith('pssh') and elem.text: + pssh_strings.add(elem.text.strip()) + + return pssh_strings + + +KEY_REGEX = re.compile(r'\b([0-9a-fA-F]{32}):([0-9a-fA-F]{32})\b') + + +def extract_keys_from_cdm_output(text): + """ + Extract and return list of keys matching 32hex:32hex from CDM output text. + """ + matches = KEY_REGEX.findall(text) + return [f"{kid.lower()}:{key.lower()}" for kid, key in matches] + + +def fetch_keys_from_pssh(pssh_b64, license_url, token=None): + try: + pssh = PSSH(pssh_b64) + + # Load device (adjust path to your provisioned .wvd file) + device = Device.load("cdm.wvd") # <-- Replace with your actual path + cdm = Cdm.from_device(device) + + session_id = cdm.open() + + # Get license challenge + challenge = cdm.get_license_challenge(session_id, pssh) + + headers = {'x-vudrm-token': token} if token else {} + + # Send license request + response = requests.post(license_url, data=challenge, headers=headers) + response.raise_for_status() + + # Parse license + cdm.parse_license(session_id, response.content) + + # Extract keys from CDM object + keys_text = "" + for key in cdm.get_keys(session_id): + keys_text += f"[{key.type}] {key.kid.hex}:{key.key.hex()}\n" + + # Close session + cdm.close(session_id) + + # Extract and print only keys matching 32hex:32hex pattern + keys = extract_keys_from_cdm_output(keys_text) + if keys: + for k in keys: + print(k) + else: + print("No 32hex:32hex keys found in this session.") + + except Exception as e: + print(f"Error processing PSSH: {e}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python wrapper.py [args...]") + sys.exit(1) + + subcommand = sys.argv[1] + args = sys.argv[2:] + + # Step 1: Run tasks.py and get output + output = call_tasks(subcommand, *args) + + if output is not None: + # Step 2: Parse output + dash_link, widevine_license_server, token = parse_output(output) + + # Step 3: Download MPD file + mpd_content = download_mpd(dash_link) + + if mpd_content: + # Step 4: Extract PSSHs + pssh_strings = extract_pssh(mpd_content) + + + for pssh in pssh_strings: + print("") + + # Step 5: Fetch keys using each PSSH + print("") + for pssh in sorted(pssh_strings): + print() + fetch_keys_from_pssh(pssh, widevine_license_server, token) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4aa63c4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pywidevine==1.8.0 +Requests==2.32.5 diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..a860052 --- /dev/null +++ b/tasks.py @@ -0,0 +1,17 @@ +import subprocess +import sys + +def run_commands(): + # Run get_vid.py with the provided subcommands + if len(sys.argv) > 1: + vid_command = ['python', 'get_vid.py'] + sys.argv[1:] + vid_output = subprocess.run(vid_command, capture_output=True, text=True) + print(vid_output.stdout) + + # Run get_lic.py without any subcommands + lic_command = ['python', 'get_lic.py'] + lic_output = subprocess.run(lic_command, capture_output=True, text=True) + print(lic_output.stdout) + +if __name__ == '__main__': + run_commands()