VideoHelp Forum





Try StreamFab Downloader and download streaming video from Netflix, Amazon!



+ Reply to Thread
Results 1 to 9 of 9
  1. Member
    Join Date
    Jul 2024
    Location
    Germany
    Search Comp PM
    Hi everyone,

    I hope this is worthy of a new thread.

    I’ve been working on a macOS app called UKTVDownloader, which I built with the help of some AI tools (mostly Claude and ChatGPT). It’s a Python-based GUI that lets users download content from UK streaming platforms like Channel 4, ITVX, and Channel 5. It supports MPD detection, subtitle downloads, and merging with ffmpeg and N_m3u8DL-RE.

    GitHub repo:
    https://github.com/Padraig74/UKTVDownloader

    Right now, the only manual step left is getting the DRM key (KID:KEY). I can extract the PSSH from the MPD, but the user still has to manually input the decryption key — usually using Widevine CDM tools or browser extensions.


    What I’m asking for:
    • Is there any way to automate the process of getting the DRM key from the PSSH, via existing tools?
    • Has anyone integrated something like this into a script or app before?
    • Any advice on how I could approach this for macOS users would be really appreciated.

    I’m not a professional coder — I’ve stitched this together mostly by learning as I go and tweaking with AI assistance — so if anyone has tips, examples, or even partial solutions, I’d be really grateful.

    Thanks in advance!
    Padraig
    Quote Quote  
  2. You maybe want to look at https://github.com/aarubui/yt-dlp-mp4decrypt, which should have support for Channel 4 and ITV among others.
    Quote Quote  
  3. Did an answer for this ever appear?! I use scripts but still have to input the KID:KEY manually.
    Quote Quote  
  4. Originally Posted by Oohjimaflip View Post
    Did an answer for this ever appear?! I use scripts but still have to input the KID:KEY manually.
    You can use a txt file called key.txt to avoid manually entering kid:key.

    Code:
    key_file = os.path.expanduser("~/Downloads/key.txt")
    
    def get_drm_key(self, pssh, url, service_name, key_file):
        if not pssh:
            print("❌ No PSSH available to extract key")
            return None
    
        # Check cache
        if pssh in self.widevine_data:
            print(f"✅ Found cached key for PSSH: {self.widevine_data[pssh]}")
            return self.widevine_data[pssh]
    
        # Initialize browser if needed
        if not self.driver:
            self.initialize_browser()
    
        # Decode PSSH to extract KID
        try:
            pssh_bytes = base64.b64decode(pssh)
            kid = pssh_bytes[32:48].hex()
            print(f"✅ Extracted KID from PSSH: {kid}")
        except Exception as e:
            print(f"❌ Error extracting KID from PSSH: {e}")
            kid = None
    
        print("\n�� DRM KEY EXTRACTION ��")
        print(f"1. Opening {service_name} video page...")
    
        self.driver.get(url)
        time.sleep(5)
    
        print("2. Waiting for Widevine license request...")
    
        try:
            play_button = WebDriverWait(self.driver, 10).until(
                EC.element_to_be_clickable(
                    (By.CSS_SELECTOR, "button.play-button, .play-icon, [aria-label='Play'], video")
                )
            )
            play_button.click()
            time.sleep(5)
        except Exception:
            print("   (Play button not found, continuing anyway)")
    
        print("\n3. Reading DRM key from key.txt")
        try:
            with open(key_file, "r", encoding="utf-8") as f:
                key_input = f.read().strip()
        except FileNotFoundError:
            print(f"❌ File '{key_file}' not found. Create it with KID:KEY inside.")
            return None
        except Exception as e:
            print(f"❌ Error reading '{key_file}': {e}")
            return None
    
        # Validate key format
        if ':' not in key_input:
            print("❌ Invalid format in key.txt (expected KID:KEY)")
            return None
    
        kid_from_file, key = key_input.split(':', 1)
        kid_from_file = kid_from_file.strip().lower()
        key = key.strip().lower()
    
        # Optional consistency check
        if kid and kid_from_file != kid:
            print("⚠️ Warning: KID in key.txt does not match extracted PSSH KID")
    
        final_key = f"{kid_from_file}:{key}"
    
        # Store key
        self.widevine_data[pssh] = final_key
        self.save_widevine_proxy_data()
    
        print("✅ DRM key successfully loaded from key.txt")
        return final_key
    This avoids manually entering the kid:key
    Example of key.txt
    e3c1b7d2a4f94c8d9e2f123456789abc:1a2b3c4d5e6f7890a bcdef1234567890
    Quote Quote  
  5. otherwise a multiple version key.text.
    KID1:KEY1
    KID2:KEY2
    KID3:KEY3
    Code:
    key_file = os.path.expanduser("~/Downloads/key.txt")
    
    def get_drm_key(self, pssh, url, service_name, key_file):
        if not pssh:
            print("❌ No PSSH available to extract key")
            return None
    
        # Check cache
        if pssh in self.widevine_data:
            print(f"✅ Found cached key for PSSH: {self.widevine_data[pssh]}")
            return self.widevine_data[pssh]
    
        # Initialize browser if needed
        if not self.driver:
            self.initialize_browser()
    
        # Decode PSSH → extract KID
        try:
            pssh_bytes = base64.b64decode(pssh)
            kid = pssh_bytes[32:48].hex().lower()
            print(f"✅ Extracted KID from PSSH: {kid}")
        except Exception as e:
            print(f"❌ Error extracting KID from PSSH: {e}")
            return None
    
        print("\n🔑 DRM KEY EXTRACTION 🔑")
        print(f"1. Opening {service_name} video page...")
    
        self.driver.get(url)
        time.sleep(5)
    
        print("2. Triggering Widevine license request...")
    
        try:
            play_button = WebDriverWait(self.driver, 10).until(
                EC.element_to_be_clickable(
                    (By.CSS_SELECTOR, "button.play-button, .play-icon, [aria-label='Play'], video")
                )
            )
            play_button.click()
            time.sleep(5)
        except Exception:
            print("   (Play button not found, continuing anyway)")
    
        print("\n3. Searching matching key in key.txt")
        found_key = None
    
        try:
            with open(key_file, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line or ':' not in line:
                        continue
    
                    file_kid, file_key = line.split(':', 1)
                    file_kid = file_kid.strip().lower()
                    file_key = file_key.strip().lower()
    
                    if file_kid == kid:
                        found_key = f"{file_kid}:{file_key}"
                        break
    
        except FileNotFoundError:
            print(f"❌ File '{key_file}' not found.")
            return None
        except Exception as e:
            print(f"❌ Error reading '{key_file}': {e}")
            return None
    
        if not found_key:
            print("❌ No matching KID found in key.txt")
            return None
    
        # Store key
        self.widevine_data[pssh] = found_key
        self.save_widevine_proxy_data()
    
        print("✅ DRM key found and loaded from key.txt")
        return found_key
    Quote Quote  
  6. Originally Posted by sesamap159 View Post
    Originally Posted by Oohjimaflip View Post
    Did an answer for this ever appear?! I use scripts but still have to input the KID:KEY manually.
    You can use a txt file called key.txt to avoid manually entering kid:key.

    Code:
    key_file = os.path.expanduser("~/Downloads/key.txt")
    
    def get_drm_key(self, pssh, url, service_name, key_file):
        if not pssh:
            print("❌ No PSSH available to extract key")
            return None
    
        # Check cache
        if pssh in self.widevine_data:
            print(f"✅ Found cached key for PSSH: {self.widevine_data[pssh]}")
            return self.widevine_data[pssh]
    
        # Initialize browser if needed
        if not self.driver:
            self.initialize_browser()
    
        # Decode PSSH to extract KID
        try:
            pssh_bytes = base64.b64decode(pssh)
            kid = pssh_bytes[32:48].hex()
            print(f"✅ Extracted KID from PSSH: {kid}")
        except Exception as e:
            print(f"❌ Error extracting KID from PSSH: {e}")
            kid = None
    
        print("\n�� DRM KEY EXTRACTION ��")
        print(f"1. Opening {service_name} video page...")
    
        self.driver.get(url)
        time.sleep(5)
    
        print("2. Waiting for Widevine license request...")
    
        try:
            play_button = WebDriverWait(self.driver, 10).until(
                EC.element_to_be_clickable(
                    (By.CSS_SELECTOR, "button.play-button, .play-icon, [aria-label='Play'], video")
                )
            )
            play_button.click()
            time.sleep(5)
        except Exception:
            print("   (Play button not found, continuing anyway)")
    
        print("\n3. Reading DRM key from key.txt")
        try:
            with open(key_file, "r", encoding="utf-8") as f:
                key_input = f.read().strip()
        except FileNotFoundError:
            print(f"❌ File '{key_file}' not found. Create it with KID:KEY inside.")
            return None
        except Exception as e:
            print(f"❌ Error reading '{key_file}': {e}")
            return None
    
        # Validate key format
        if ':' not in key_input:
            print("❌ Invalid format in key.txt (expected KID:KEY)")
            return None
    
        kid_from_file, key = key_input.split(':', 1)
        kid_from_file = kid_from_file.strip().lower()
        key = key.strip().lower()
    
        # Optional consistency check
        if kid and kid_from_file != kid:
            print("⚠️ Warning: KID in key.txt does not match extracted PSSH KID")
    
        final_key = f"{kid_from_file}:{key}"
    
        # Store key
        self.widevine_data[pssh] = final_key
        self.save_widevine_proxy_data()
    
        print("✅ DRM key successfully loaded from key.txt")
        return final_key
    This avoids manually entering the kid:key
    Example of key.txt
    e3c1b7d2a4f94c8d9e2f123456789abc:1a2b3c4d5e6f7890a bcdef1234567890
    But you still need to create 'key.txt'! I am looking for this to be automated direct.
    Quote Quote  
  7. Alternatively, if there is no token for the license_url of the 3 sites: Channel 4, ITVX, and Channel 5, you can obtain the keys yourself with pywindevine.

    You can simplify things by creating a menu for the 3 sites: Channel 4, ITVX, Channel 5. Here is a simplified version without a menu:
    Code:
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    from datetime import datetime
    import pyfiglet, requests, base64, binascii, os
    
    # Colors
    RED = '\x1b[38;5;160m'
    GREEN = '\x1b[38;5;46m'
    CYAN = '\x1b[38;5;14m'
    YELLOW = '\x1b[38;5;226m'
    END = '\x1b[0m'
    WVD_PATH = "./device.wvd"
    
    # Record the PSSH + keys in the text.
    def save_keys_to_file(keys, pssh, output_dir="keys"):
        if not keys:
            return
    
        os.makedirs(output_dir, exist_ok=True)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        short_pssh = pssh[:16]
        filename = f"widevine_keys_{short_pssh}_{timestamp}.txt"
        filepath = os.path.join(output_dir, filename)
        with open(filepath, "w", encoding="utf-8") as f:
            f.write("Widevine Decryption Keys\n")
            f.write("=" * 30 + "\n")
            f.write(f"PSSH: {pssh}\n")
            f.write(f"Keys found: {len(keys)}\n")
            for key in keys:
                f.write(f"--key {key}\n")
    
        print(f"{GREEN}✔ Keys saved to file: {YELLOW}{filepath}{END}")
    
    # Validate PSSH (Base64 + 'pssh' box + reasonable length)
    def is_valid_pssh(pssh_b64: str) -> bool:
        try:
            raw = base64.b64decode(pssh_b64, validate=True)
            if len(raw) < 32:
                return False
            if raw[4:8] != b'pssh':
                return False
            return True
        except (binascii.Error, ValueError):
            return False
    
    # Display ASCII banner
    def title():
        banner = pyfiglet.figlet_format('Widevine Decryptor Keys', font='slant', width=150)
        print(f"{CYAN}{banner}{END}")
    
    # Prompt user input
    def ask(prompt):
        return input(f"{RED}{prompt}{YELLOW}")
    
    # Ensure Widevine device exists
    def ensure_device():
        if os.path.isfile(WVD_PATH):
            print(f"{GREEN}✔ Device found: {YELLOW}{WVD_PATH}{END}")
            return Device.load(WVD_PATH)
        else:
            print(f"{RED}❌ Device file not found: {YELLOW}{WVD_PATH}{END}")
            input("\nPress Enter to exit...")
            exit(1)
    
    # Retrieve decryption keys
    def get_key(pssh, license_url, headers, device):
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
        challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
        try:
            response = requests.post(license_url, data=challenge, headers=headers, timeout=10)
            response.raise_for_status()
        except Exception as e:
            print(f"{RED}❌ License error: {e}{END}")
            return ""
    
        cdm.parse_license(session_id, response.content)
        keys = []
        for key in cdm.get_keys(session_id):
            if key.type == "CONTENT":
                keys.append(f"{key.kid.hex}:{key.key.hex()}")
    
        cdm.close(session_id)
        return keys
    
    # Main function
    def main():
        title()
        device = ensure_device()
        # Loop until valid PSSH is entered
        while True:
            pssh_str = ask("Enter the PSSH code: ")
            if not pssh_str:
                print(f"{RED}❌ Empty PSSH, please try again.{END}")
                continue
            if not is_valid_pssh(pssh_str):
                print(f"{RED}❌ Invalid PSSH (base64 / pssh format){END}")
                continue
            break
    
        # Loop until License URL is entered
        while True:
            lic_url = ask("Enter the License URL: ")
            if not lic_url:
                print(f"{RED}❌ License URL is empty, please try again.{END}")
                continue
            break
    
        # Loop until Token is entered
        #while True:
        #    token = ask("Enter the Token: ")
        #    if not token:
        #       print(f"{RED}❌ Token is empty, please try again.{END}")
        #        continue
        #    break
    
        #headers = {'x-dt-auth-token': token}
        # Optional alternative header
        headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0'}
        keys = get_key(pssh_str, lic_url, headers, device)
        if not keys:
            print(f"{RED}❌ No key found{END}")
        else:
            print(f"{GREEN}✔ {len(keys)} key(s) found:{END}")
            for k in keys:
                print(f"{YELLOW}--key {k}{END}")
            
            save_keys_to_file(keys, pssh_str)
        
        input("\nPress Enter to exit...")
    
    if __name__ == "__main__":
        main()
    Here is my menu on the 3 French sites:
    Image
    [Attachment 90836 - Click to enlarge]


    Once you get the keys from Widevine, it automatically downloads the video with N-m3u8dl-re. If you need help creating a menu, I could help you if you want.
    Quote Quote  
  8. nice thank you for tips
    Quote Quote  
  9. Here is a menu to choose the site you want to download from after obtaining the keys, but there are things to change below, it's up to you.

    Code:
    from pywidevine.cdm import Cdm
    from pywidevine.device import Device
    from pywidevine.pssh import PSSH
    from lxml import etree
    import base64, requests, sys, os, pyfiglet, time, subprocess
    
    # ===================== COLORS ===================== #
    RED = "\033[91m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    CYAN = "\033[96m"
    BLUE = "\033[94m"
    END = "\033[0m"
    
    # ===================== PATHS ===================== #
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    TOOLS_PATH = os.path.join(BASE_DIR, "Tools")
    FOLDER = os.path.join(BASE_DIR, "Downloader_Video")
    WVD_PATH = os.path.join(BASE_DIR, "device.wvd")
    
    # ===================== REQUIRED TOOLS ===================== #
    EXECUTABLES = ["ffmpeg.exe", "mp4decrypt.exe", "N_m3u8DL-RE.exe"]
    
    missing = [exe for exe in EXECUTABLES if not os.path.isfile(os.path.join(TOOLS_PATH, exe))]
    if missing:
        print(f"{RED}❌ Missing files in Tools folder:{END}")
        for f in missing:
            print(f"  - {YELLOW}{f}{END}")
        input("\nPress Enter to exit...")
        sys.exit(1)
    
    # ===================== DISPLAY ===================== #
    def print_title():
        title = pyfiglet.figlet_format("Widevine Decryptor Keys", font="slant", width=150)
        print(f"{CYAN}{title}{END}")
    
    def clear_screen():
        os.system("cls" if os.name == "nt" else "clear")
    
    # ===================== INPUT UTILS ===================== #
    def ask_input(prompt, error):
        while True:
            try:
                value = input(f"{YELLOW}{prompt}{END} ").strip()
            except (EOFError, KeyboardInterrupt):
                print(f"\n{RED}Input cancelled{END}")
                sys.exit(1)
            if value:
                return value
            print(f"{RED}{error}{END}")
    
    def ensure_device():
        if os.path.isfile(WVD_PATH):
            print(f"{GREEN}✔ Device found: {YELLOW}{WVD_PATH}{END}")
            return Device.load(WVD_PATH)
        else:
            print(f"{RED}❌ Device file not found: {YELLOW}{WVD_PATH}{END}")
            input("\nPress Enter to exit...")
            exit(1)
    
    def extract_pssh(mpd_url):
        if not mpd_url:
            print(f"{RED}❌ Failed to detect MPD URL{END}")
            return None
        try:
            r = requests.get(mpd_url, timeout=10)
            r.raise_for_status()
        except Exception as e:
            print(f"{RED}❌ MPD request error: {e}{END}")
            return None
        try:
            xml = etree.fromstring(r.content)
        except Exception as e:
            print(f"{RED}❌ Invalid MPD XML: {e}{END}")
            return None
        namespaces = {"mpd": "urn:mpeg:dash:schema:mpd:2011", "cenc": "urn:mpeg:cenc:2013"}
        pssh_list = xml.xpath("//cenc:pssh", namespaces=namespaces)
        if not pssh_list:
            print(f"{RED}❌ No PSSH found in the MPD{END}")
            return None
        # Cleanup + remove duplicates
        pssh_values = list(dict.fromkeys(pssh.text.strip() for pssh in pssh_list if pssh.text))
        print(f"{GREEN}✔ {len(pssh_values)} PSSH found{END}")
        for i, pssh in enumerate(pssh_values, 1):
            print(f"{CYAN}PSSH #{i}:{END} {pssh}")
        # Usually the first one is sufficient
        return pssh_values[0]
    
    def get_key(pssh, license_url, headers, device):
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
        challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
        try:
            response = requests.post(license_url, data=challenge, headers=headers, timeout=10)
            response.raise_for_status()
        except Exception as e:
            print(f"{RED}❌ License error: {e}{END}")
            return ""
    
        cdm.parse_license(session_id, response.content)
        keys = []
        for key in cdm.get_keys(session_id):
            if key.type == "CONTENT":
                keys.append(f"{key.kid.hex}:{key.key.hex()}")
    
        cdm.close(session_id)
        return keys
    
    def Download_Video_DRM(mpd, title, keys):
        print(f"\n🧩 {BLUE}KID:KEY found:{END} {GREEN}{len(keys)}{END}")
        print("".join(f"🔑 --key {RED}{k}{END}\n" for k in keys))
        os.makedirs(FOLDER, exist_ok=True)
        exe_path = os.path.join(TOOLS_PATH, "N_m3u8DL-RE.exe")
        cmd = ([exe_path, mpd, "-M", "format=mp4", "--no-log", "--log-level", "ERROR"]
            + [item for k in keys for item in ("--key", k)] 
            + ["-sv", "best", "-sa", "best", "--save-name", title, "--save-dir", FOLDER])
        print(f"\n{CYAN}📥 Download in progress...{END}\n")
        time.sleep(2)
        subprocess.run(cmd)
    
    def saisir_infos(site_name):
        clear_screen()
        print_title()
        print(f"\n--- {site_name} ---")
        device = ensure_device()
        name = ask_input("Video title:", "❌ Title is empty.")
        licence_mpd = ask_input("MPD URL:", "❌ MPD URL is empty.")
        licence_url = ask_input("License URL:", "❌ License URL is empty.")
        token = ask_input("dt-auth-token :", "❌ Token is empty.") #replace dt-auth-token with another token name
        headers = {"dt-auth-token": token} #replace dt-auth-token with another token name
        # If there is no token, replace it with this:
        #headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0"}
        pssh = extract_pssh(licence_mpd)
        keys = get_key(pssh, licence_url, headers, device)
        if not keys:
            print(f"{RED}❌ No key found{END}")
        else:
            Download_Video_DRM(licence_mpd, name, keys)
    
    def menu():
        clear_screen()
        while True:
            print(f"\n{CYAN}============== Choix Replay ================{END}")
            print(f"{YELLOW}1{END} - {GREEN}Channel 4{END}")
            print(f"{YELLOW}2{END} - {BLUE}ITV{END}")
            print(f"{YELLOW}3{END} - {CYAN}Channel 5{END}")
            print(f"{YELLOW}4{END} - {RED}Exit{END}")
            print(f"{CYAN}============================================{END}")
            choice = input(f"{YELLOW}Choose a site: {END}").strip()
            if choice == "1":
                saisir_infos("Channel 4")
            elif choice == "2":
                saisir_infos("ITV")
            elif choice == "3":
                saisir_infos("Channel 5")
            elif choice == "4":
                print(f"{YELLOW}Goodbye!{END}")
                sys.exit(0)
            else:
                print("Invalid choice, please try again.")
                time.sleep(2)
    
    if __name__ == "__main__":
        menu()
    I haven't tested it because you have to create an account for each site; you'll have to try it yourself.
    Quote Quote  



Similar Threads

Visit our sponsor! Try DVDFab and backup Blu-rays!