### Retrieve Production Config with Python Source: https://context7.com/diazole/c4-dl/llms.txt Fetches DRM configuration details from Channel 4's production bundle JavaScript file. It uses the 'requests', 'json', and 're' libraries. The function makes an HTTP GET request to a specific URL and parses the JavaScript content to extract JSON configurations, specifically focusing on DRM settings for Widevine and DrmToday. ```python import requests import json import re # Assuming Video and DrmToday and VodConfig classes are defined elsewhere # class Video: # def __init__(self, type, url): # self.type = type # self.url = url # # class DrmToday: # def __init__(self, url, token, video, message): # self.url = url # self.token = token # self.video = video # self.message = message # # class VodConfig: # def __init__(self, vodbs_url, drm_today, token): # self.vodbs_url = vodbs_url # self.drm_today = drm_today # self.token = token def get_config(): try: req = requests.get('https://static.c4assets.com/all4-player/latest/bundle.app.js') req.raise_for_status # Extract JSON configs from bundle configs = re.findall( "JSON\.parse\('(.*?)'\)", ''.join( req.content.decode() .replace('\u200c', '') .replace('\"', '"') ) ) config = json.loads(configs[1]) # Extract DRM configuration video_type = config['protectionData']['com.widevine.alpha']['drmtoday']['video']['type'] message = config['protectionData']['com.widevine.alpha']['drmtoday']['message'] video = Video(video_type, '') drm_today = DrmToday('', '', video, message) # Returns VodConfig with: # - vodbs_url: base URL for VOD backend # - drm_today: DRM configuration object return VodConfig(config['vodbsUrl'], drm_today, '') except Exception as e: print('[!] Failed getting production config !!!') raise ``` -------------------------------- ### Extract KID from MPD Manifest with Python Source: https://context7.com/diazole/c4-dl/llms.txt Extracts the Key ID (KID) from a DASH MPD manifest file, which is crucial for PSSH generation. This function uses the 'requests' and 're' libraries. It sends an HTTP GET request with specific headers to the MPD URL and then uses a regular expression to find and return the KID value. ```python import requests import re MPD_HEADERS = { 'Content-type': 'application/dash+xml', 'Accept': '*/*', 'Referer': 'https://www.channel4.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } def get_kid(url): # Example: url = "https://vod-stream.channel4.com/manifest.mpd" try: req = requests.get(url, headers=MPD_HEADERS) req.raise_for_status # Extract KID from MPD XML: cenc:default_KID="12345678-1234-1234-1234-123456789abc" kid = re.search('cenc:default_KID="(.*)"', req.text).group(1) # Returns: "12345678-1234-1234-1234-123456789abc" return kid except Exception as e: print('[!] Failed getting KID !!!') raise ``` -------------------------------- ### Main Download Workflow Orchestration (Python) Source: https://context7.com/diazole/c4-dl/llms.txt The main function orchestrates the entire download process, from fetching configuration and extracting asset IDs to handling DRM licensing and initiating downloads. It requires pywidevine library and interacts with Channel 4's DRM services. Outputs decryption keys and optionally downloads and merges streams. ```python import sys from pywidevine.device import Device from pywidevine.cdm import Cdm from pywidevine.pssh import PSSH import base64 # Assume get_config, get_asset_id, get_vod_stream, decrypt_token, get_service_certificate, get_kid, generate_pssh, get_license_response, get_file_output_title, download_streams, decrypt_streams, merge_streams, create_argument_parser are defined elsewhere # Example usage from command line: # python c4-dl.py --download --wvd "device.wvd" --url "https://www.channel4.com/programmes/show/on-demand/12345-001" def main(): parser = create_argument_parser() wvd = parser.wvd url = parser.url download = parser.download # Get production config from Channel 4 config = get_config() # Extract asset ID from URL asset_id = get_asset_id(url) # Get encrypted VOD stream info encrypted_vod_stream = get_vod_stream(asset_id) # Decrypt the stream token using AES decrypted_vod_stream = decrypt_token(encrypted_vod_stream.token) # Setup DRM request config.drm_today.video.url = encrypted_vod_stream.uri config.drm_today.token = decrypted_vod_stream.token config.drm_today.request_id = asset_id # Get service certificate service_cert = get_service_certificate( decrypted_vod_stream.uri, config.drm_today).license_response # Initialize Widevine CDM device = Device.load(wvd) cdm = Cdm.from_device(device) session_id = cdm.open() cdm.set_service_certificate(session_id, service_cert) # Generate PSSH and get license kid = get_kid(config.drm_today.video.url) pssh = generate_pssh(kid) challenge = cdm.get_license_challenge(session_id, PSSH(pssh), privacy_mode=True) config.drm_today.message = base64.b64encode(challenge).decode('UTF-8') # Parse license response license_response = get_license_response(decrypted_vod_stream.uri, config.drm_today) cdm.parse_license(session_id, license_response.license_response) # Extract decryption keys decryption_key = '' for key in cdm.get_keys(session_id): if key.type == 'CONTENT': decryption_key = f'{key.kid.hex}:{key.key.hex()}' print(f'[{key.type}] {key.kid.hex}:{key.key.hex()}') cdm.close(session_id) # Download and process streams if requested if download: output_title = get_file_output_title( encrypted_vod_stream.brand_title, encrypted_vod_stream.episode_title) download_streams(config.drm_today.video.url, output_title) files = decrypt_streams(decryption_key, output_title) merge_streams(files, output_title) if __name__ == '__main__': main() ``` -------------------------------- ### Format Episode Metadata to Scene Release Naming Conventions (Python) Source: https://context7.com/diazole/c4-dl/llms.txt Formats brand and episode titles into a standardized filename string following common scene release naming conventions. It handles both standard 'Series X Episode Y' formats and non-standard formats, converting spaces to dots. Returns a formatted string suitable for use as an output title. ```python import re def get_file_output_title(brand_title, episode_title): # Example 1: brand_title = "The Big Bang Theory", episode_title = "Series 1 Episode 1" # Example 2: brand_title = "Documentary", episode_title = "Special Episode" try: title = re.search('^Series\s+(\d+)\s+Episode\s+(\d+)$', episode_title) if title is None: # Non-standard episode format output_title = f'{brand_title} {episode_title} WEB-DL' output_title = ' '.join(output_title.split()) # Returns: "Documentary.Special.Episode.WEB-DL" return output_title.replace(' ', '.') # Standard series/episode format series = title.group(1) episode = title.group(2) # Zero-pad single digits if len(series) == 1: series = '0' + series if len(episode) == 1: episode = '0' + episode output_title = f'{brand_title} S{series}E{episode} WEB-DL' output_title = ' '.join(output_title.split()) # Returns: "The.Big.Bang.Theory.S01E01.WEB-DL" return output_title.replace(' ', '.') except Exception as e: print('[!] Failed getting output title !!!') raise ``` -------------------------------- ### Merge Video and Audio Streams into MP4 using FFmpeg (Python) Source: https://context7.com/diazole/c4-dl/llms.txt Merges decrypted video and audio streams into a single MP4 file using FFmpeg. It detects video and audio codecs, and resolution from filenames to create a structured output directory and filename. Requires FFmpeg executable in the 'bin' directory and temporary files in './tmp'. Outputs the merged file to './downloads'. ```python import subprocess import os import shutil def merge_streams(files, output_title): # Example: files = ['./tmp/.../video.mp4', './tmp/.../audio.mp4'] # output_title = "The.Big.Bang.Theory.S01E01.WEB-DL" try: video_codec = 'unknown' audio_codec = 'unknown' resolution = 'unknown' # Detect codecs and resolution from filenames for file in files: a_codec = get_audio_codec(file) # Returns 'AAC', 'AC-3', or 'MP3' if a_codec: audio_codec = a_codec v_codec = get_video_codec(file) # Returns 'H.264' or 'H.265' if v_codec: video_codec = v_codec v_resolution = get_resolution(file) # Returns '1080p', '720p', or '420p' if v_resolution: resolution = v_resolution output_dir = f'./downloads/{output_title}.{resolution}.{video_codec}.{audio_codec}' output_file = f'{output_dir}/{output_title}.{resolution}.{video_codec}.{audio_codec}.mp4' os.mkdir(output_dir) args = [ './bin/ffmpeg.exe', '-hide_banner', '-loglevel', 'error', '-i', files[0], # Video stream '-i', files[1], # Audio stream '-c', 'copy', # Copy streams without re-encoding output_file ] subprocess.run(args, check=True) # Clean up temporary files shutil.rmtree(f'./tmp/{output_title}') # Final output: ./downloads/The.Big.Bang.Theory.S01E01.WEB-DL.1080p.H.264.AAC/ # The.Big.Bang.Theory.S01E01.WEB-DL.1080p.H.264.AAC.mp4 except Exception as e: print('[!] Failed merging streams !!!') raise ``` -------------------------------- ### Retrieve VOD Stream Information with Python Source: https://context7.com/diazole/c4-dl/llms.txt Fetches VOD stream details, including MPD manifest URL and encrypted token, from Channel 4's asset API. It requires the 'requests' and 'xml.etree.ElementTree' libraries. The function takes an asset ID as input and returns a VodStream object containing the token, URI, brand title, and episode title. ```python import requests import xml.etree.ElementTree as ET import sys # Assuming VodStream class is defined elsewhere # class VodStream: # def __init__(self, token, uri, brand_title, episode_title): # self.token = token # self.uri = uri # self.brand_title = brand_title # self.episode_title = episode_title def get_vod_stream(asset_id): # Example: asset_id = 44564001 try: url = f'https://ais.channel4.com/asset/{asset_id}?client=android-mod' req = requests.get(url) if req.status_code == 404: print('[!] Invalid URL !!!') sys.exit(1) req.raise_for_status root = ET.fromstring(req.content) asset_info_xpath = './assetInfo/' # Extract metadata brand_title = root.find(asset_info_xpath + 'brandTitle').text brand_title = brand_title.replace(':', ' ').replace('/', ' ') episode_title = root.find(asset_info_xpath + 'episodeTitle').text episode_title = episode_title.replace(':', ' ').replace('/', ' ') # Get Widevine stream info stream_xpath = f'{asset_info_xpath}videoProfiles/videoProfile[@name='widevine-stream-4']/stream/' uri = root.find(stream_xpath + 'uri').text token = root.find(stream_xpath + 'token').text # Returns VodStream object with: # - token: encrypted authentication token # - uri: MPD manifest URL # - brand_title: "The Big Bang Theory" # - episode_title: "Series 1 Episode 1" return VodStream(token, uri, brand_title, episode_title) except Exception as e: print('[!] Failed getting VOD stream !!!') raise ``` -------------------------------- ### Generate Widevine PSSH Header (Python) Source: https://context7.com/diazole/c4-dl/llms.txt Generates a Protection System Specific Header (PSSH) for Widevine DRM. It requires a Key ID (kid) as input and utilizes the 'widevine_pssh_data_pb2' library. The output is a base64 encoded PSSH box. ```python import base64 import struct import widevine_pssh_data_pb2 as widevine def generate_pssh(kid): # Example: kid = "12345678-1234-1234-1234-123456789abc" try: wide_vine = widevine.WidevinePsshData() # Add key ID (convert UUID to bytes) wide_vine.key_id.append(base64.b16decode(kid.replace('-', ''))) wide_vine.provider = 'rbmch4tv' wide_vine.content_id = bytes(kid, 'UTF-8') wide_vine.policy = '' wide_vine.algorithm = 1 # AESCTR pssh_data = wide_vine.SerializeToString() # Build PSSH box structure ret = b'pssh' + struct.pack('>i', 0 << 24) ret += base64.b16decode('EDEF8BA979D64ACEA3C827DCD51D21ED') # Widevine system ID ret += struct.pack('>i', len(pssh_data)) ret += pssh_data pssh = struct.pack('>i', len(ret) + 4) + ret # Returns: base64 encoded PSSH box (e.g., "AAAANHBzc2gAAAAA...") return base64.b64encode(pssh).decode() except Exception as e: print('[!] Failed generating PSSH !!!') raise ``` -------------------------------- ### Retrieve Widevine License Response (Python) Source: https://context7.com/diazole/c4-dl/llms.txt Requests and retrieves a Widevine license from a DRM server. It uses the 'requests' library and requires the license server URL and DRM data. The function returns a LicenseResponse object containing the base64 encoded license and a status object. ```python import requests import json DEFAULT_HEADERS = { 'Content-type': 'application/json', 'Accept': '*/*', 'Referer': 'https://www.channel4.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } def get_license_response(url, drm_today): # Example: url = "https://lic.drmtoday.com/license-proxy-widevine/cenc/" # drm_today contains: request_id, token, video URL, and license challenge message try: req = requests.post( url, data=json.dumps(drm_today.to_json(), cls=ComplexJsonEncoder), headers=DEFAULT_HEADERS ) req.raise_for_status resp = json.loads(req.content) license_response = resp['license'] # Base64 encoded license status = Status(resp['status']['success'], resp['status']['type']) if not status.success: raise ValueError("License request failed") # Returns LicenseResponse with: # - license_response: base64 encoded Widevine license # - status: success status object return LicenseResponse(license_response, status) except Exception as e: print('[!] Failed getting license challenge !!!') raise ``` -------------------------------- ### Download Encrypted Streams (Python) Source: https://context7.com/diazole/c4-dl/llms.txt Downloads encrypted video and audio streams using yt-dlp and aria2c. It takes the MPD (Media Presentation Description) URL and an output title as input. The streams are downloaded to a temporary directory, preserving their encrypted state. ```python import subprocess def download_streams(mpd, output_title): # Example: mpd = "https://vod-stream.channel4.com/manifest.mpd" # output_title = "The.Big.Bang.Theory.S01E01.WEB-DL" try: args = [ './bin/yt-dlp.exe', '--downloader', 'aria2c', # Use aria2c for faster downloads '--allow-unplayable-formats', # Download encrypted streams '-q', # Quiet mode '--no-warnings', '--progress', # Show progress bar '-f', 'bv,wa', # Best video + worst audio (no audio description) mpd, '-o', f'./tmp/{output_title}/encrypted_{output_title}.%(height)sp.%(vcodec)s%(acodec)s.%(ext)s' ] subprocess.run(args, check=True) # Downloads to: ./tmp/The.Big.Bang.Theory.S01E01.WEB-DL/ # Files: encrypted_The.Big.Bang.Theory.S01E01.WEB-DL.1080p.avc1.mp4 # encrypted_The.Big.Bang.Theory.S01E01.WEB-DL.mp4a.40.mp4 except Exception as e: print('[!] Failed downloading streams !!!') raise ``` -------------------------------- ### Decrypt VOD Stream Token with Python Source: https://context7.com/diazole/c4-dl/llms.txt Decrypts an AES-128-CBC encrypted stream token using a hardcoded key and Initialization Vector (IV). This function requires the 'pycryptodome' library and 'base64'. It takes a base64 encoded token as input and returns a VodStream object containing the decrypted license URL and MPD manifest URL. ```python from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 # Assuming VodStream class is defined elsewhere # class VodStream: # def __init__(self, token, uri, brand_title, episode_title): # self.token = token # self.uri = uri # self.brand_title = brand_title # self.episode_title = episode_title def decrypt_token(token): # Example: token = "U2FsdGVkX1+..." (base64 encoded encrypted string) try: cipher = AES.new( b"\x41\x59\x44\x49\x44\x38\x53\x44\x46\x42\x50\x34\x4d\x38\x44\x48", # AES key AES.MODE_CBC, b"\x31\x44\x43\x44\x30\x33\x38\x33\x44\x4b\x44\x46\x53\x4c\x38\x32" # IV ) decoded_token = base64.b64decode(token) decrypted_string = unpad(cipher.decrypt(decoded_token), 16, style='pkcs7').decode('UTF-8') # Token format: "mpd_url|license_url" license_info = decrypted_string.split('|') # Returns VodStream with: # - token: license_info[1] = "https://lic.drmtoday.com/..." # - uri: license_info[0] = "https://vod-stream.channel4.com/..." return VodStream(license_info[1], license_info[0], '', '') except Exception as e: print('[!] Failed decrypting VOD stream !!!') raise ``` -------------------------------- ### Decrypt Streams with Bento4 mp4decrypt (Python) Source: https://context7.com/diazole/c4-dl/llms.txt Decrypts downloaded encrypted streams using Bento4's mp4decrypt utility. It requires the decryption key in the format 'KID:KEY' (hex) and the output title. The function iterates through encrypted files, decrypts them, and saves the decrypted files in the same directory. ```python import subprocess import os def decrypt_streams(decryption_key, output_title): # Example: decryption_key = "1234567890abcdef:fedcba0987654321" # output_title = "The.Big.Bang.Theory.S01E01.WEB-DL" try: files = [] for file in os.listdir(f'./tmp/{output_title}'): if output_title in file: input_file = f'./tmp/{output_title}/{file}' file = file.replace('encrypted_', 'decrypted_') output_file = f'./tmp/{output_title}/{file}' files.append(output_file) args = [ './bin/mp4decrypt.exe', '--key', decryption_key, # Format: KID:KEY in hex input_file, output_file ] subprocess.run(args, check=True) # Returns list of decrypted files: # ['./tmp/.../decrypted_...1080p.avc1.mp4', './tmp/.../decrypted_...mp4a.40.mp4'] return files except Exception as e: print('[!] Failed decrypting streams !!!') raise ``` -------------------------------- ### Extract Asset ID from URL (Python) Source: https://context7.com/diazole/c4-dl/llms.txt This function extracts the unique asset identifier from a Channel 4 on-demand URL by parsing embedded JSON data within the HTML content. It uses the requests library to fetch the URL and regular expressions to find the JSON payload. Handles potential request errors and invalid asset IDs. ```python import requests import json import re def get_asset_id(url): # Example: url = "https://www.channel4.com/programmes/the-big-bang-theory/on-demand/44564-001" try: req = requests.get(url) req.raise_for_status # Extract JSON from embedded script tag init_data = re.search( '', ''.join( req.content.decode() .replace('\u200c', '') .replace('\r\n', '') .replace('undefined', 'null') ) ) init_data = json.loads(init_data.group(1)) asset_id = int(init_data['initialData']['selectedEpisode']['assetId']) if asset_id == 0: raise ValueError("Invalid asset ID") # Returns: 44564001 (integer) return asset_id except Exception as e: print('[!] Failed getting asset ID !!!') raise ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.