VideoHelp Forum


Try StreamFab Downloader and download from Netflix, Amazon, Youtube! Or Try DVDFab and copy Blu-rays!


Try StreamFab Downloader and download streaming video from Youtube, Netflix, Amazon! Download free trial.


+ Reply to Thread
Results 1 to 16 of 16
Thread
  1. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    l3.py without a CDM

    @sk8ordl3 made an interesting post recently and this has grown from it. I'm not sure how long it will be allowed to work.

    Below is code for a drop in replacement for l3.py.

    Code:
    '''
    cdrm-project (website of a CDM seller) put up a freely accessible L3 Content Decryption Module and then took down the API details so no-one could use it
    without first obtaining an API key.
    
    @sk8ordl3 posted some code in this forum that used the cdrm-project api, BUT without an api key; which got my attention.  Thanks sk8ordl3.
    
    The wayback machine has the old API.
    
    What follows is a drop in replacement for l3.py which does not need you to have a CDM
    '''
    
    import requests,json
    
    api_url = "https://cdrm-project.com/api"
    license_url = input("License url? ")
    pssh = input("PSSH? ")
    headers = {'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'}
    myjson={ "license":license_url, "pssh":pssh, "proxy":"",  "cache":False }
    r = requests.post(api_url,headers=headers ,json=myjson).text
    #print(r)
    mydict = json.loads(r)
    print("\n------------ Keys ------------\n")
    keys = mydict['keys']
    for key in keys:
      print (key['key'])
    It works for simple sites that do not need headers of any complexity.

    Test with License: https://cwip-shaka-proxy.appspot.com/no_auth
    PSSH: AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62 dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xq YVNkZmFsa3IzaioCSEQyAA==

    Newbie corner

    To use: save the code in the white box to a file called cl3.py
    Then, assuming you have python on your machine, open a command window in the directory where you saved cl3.py and type
    Code:
    python cl3.py
    paste responses in the command window to the questions asked.
    Last edited by A_n_g_e_l_a; 9th Jun 2023 at 09:16. Reason: Test added
    Quote Quote  
  2. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Here are a couple of downloader scripts that do not need you to own a CDM. They both use a replacement for the function WV_Function(pssh, lic_url, cert_b64=None): - common to much of widewine decrypting that uses WKS-KEYS.

    First is a fully worked up downloader script for sites that use boltdns.net to deliver their drm management. That means, in the UK, STV.tv UKtvPlay.co.uk tptvencore.co.uk - (not everything is drm).

    A generic boltdns.net downloader without needing your own CDM

    Code:
    #!/usr/bin/env python3
    #
    # v 3.0
    #
    # A_n_g_e_l_a 09:06:2023
    # 
    '''
    This program is a generic boltdns.net downloader. 
    It takes only a SINGLE mpd url,  master.m3u8 or vmap -  as input as a 'media_url' 
    The program downloads, decodes and merges.
    
    Written for Linux systems using WKS-KEYS with a working CDM.
    For encrypted media, this program needs to be in WKS-KEYS folder to use your local CDM
    it will save to ./output/ from the WKS_KEYS folder and you need create the folder 'output' if it doesn't exist.
    
    N_m3u8DL-RE, shaka-packager, mkvmerge and ffmpeg need to be in Path.
    (Windows users may need to tack .exe on the end of the called programs in the code below)
    
    'pip install vtt_to_srt3' probably needed first;  installs a subtitle conversion that works better than N_m3u8DL-RE's
    
    
    It is recommended 'The Stream Detector' browser plugin (Chrome + Firefox)
    Find here: Firefox version - https://addons.mozilla.org/en-US/firefox/addon/hls-stream-detector/
    Chrome version: https://github.com/rowrawer/stream-detector/releases  choose  hls_stream_detector-2.1X.XX.crx 
    and in chrome://extensions/  set Developer mode (top-right window) and drag and drop the crx file into the window to install
    If that does not work on Windows for you, try this:-
    
     https://forum.videohelp.com/threads/403491-Is-there-a-The-Stream-Detector-like-extension-for-Chrome#post2687785
    
    Once installed configure with an additional filter set for 'vmap'.
    Under Options:
    'Detect additional file extensions:' set checkbox ticked and enter 'vmap' in box.
    
    The program accepts calling from commandline, with media-url and videoname as parameters, or manual input from the clipboard.
    making this an ideal routine to be called by a larger command program
    
    working for :-
    STV.tv both encrypted and encryption free
    uktvplay.co.uk all encrypted.
    tptvencore.co.uk mainly unencrypted m3u8 but some encrypted vmap included too. Media_url may need to 
        be contained in single quotes if there are '&' characters present.
    tg4.ie  all encrypted
    and possibly other providers using boltdns.net for streaming
    
    '''
    
    import requests 
    #from pywidevine.L3.cdm import deviceconfig
    #from base64 import b64encode
    #from pywidevine.L3.decrypt.wvdecryptcustom import WvDecrypt
    import re
    import subprocess
    import pyfiglet as PF
    from termcolor import colored
    import pyperclip as PC
    import os
    import math
    
    
    ## globals
    headers = {
        'User-Agent': 'Dalvik/2.9.8 (Linux; U; Android 9.9.2; ALE-L94 Build/NJHGGF)',
        'Accept': '*/*',
        'Accept-Language': 'en-GB,en;q=0.9',
        'Connection': 'keep-alive',
    }
    
    # defs
    # regex search for pssh and license url
    def get_pssh_lic(mpd_url):
        mpd = requests.get(mpd_url, headers).text
        lines = mpd.split("\n")
        for line in lines:
            m = re.search('<cenc:pssh>(AAAA.+?)</cenc:pssh>', line)
            n = re.search('bc:licenseAcquisitionUrl=\"(http.+?)\" xmlns:bc=\"urn:brightcove:2015\"', line)
            if m:
                pssh = m.group(1)
            if n:
                lic_url = n.group(1)
        return pssh, lic_url
    
    ## pretty print screen divisions
    def divides(text):  
        #text = text
        l = len(text)
        try:
            count, lines = os.get_terminal_size()
            count = int(count)
        except:
            count = 78
        if count <= 78:
            count = int(count)
        else:
            count = int(math.ceil(count/2))
        count = count - l
        if text != 'null':   
            line = (chr(9601) * int(math.ceil(count/2)))
            #print("\n")
            print(colored(line, 'green'), " ",  text, " ",   colored(line, 'green'))  
        else:
            line = (chr(9601) * int(count + 10))
            print(colored(line, 'green'))
    
    ###############################
    # 
    # there is an experimental choice below  uncomment one or other to switch
    # and uncomment the 'import' statements above that are presently commented out
    # 
    ###############################       
    
    '''# WKS-KEYS key fetch using local CDM
    def WV_Function(pssh, lic_url, cert_b64=None):
        wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64=cert_b64, device=deviceconfig.device_android_generic)                   
        widevine_license = requests.post(url=lic_url, data=wvdecrypt.get_challenge(), headers=None)
        license_b64 = b64encode(widevine_license.content)
        wvdecrypt.update_license(license_b64)
        Correct, keyswvdecrypt = wvdecrypt.start_process()
        if Correct:
            mykeys = ''
            for key in keyswvdecrypt:
                mykeys += key + '--key'
        if mykeys.endswith('--key'):
            mykeys = mykeys[:-5]
        divides('Key(s)')
        print(mykeys)
        return mykeys 
    '''
    #CDRM key remote call to cdrm-project.com
    def WV_Function(pssh, lic_url, cert_b64=None):
        lic_url = lic_url
        headers_cdrm = {
        'Connection': 'keep-alive',
        'Content-Type': 'application/json',
        'Origin': 'https://cdrm-project.com',
        'Referer': 'https://cdrm-project.com/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36',
        }
        json_data = {
            'license': lic_url,
            'headers': 'Connection: keep-alive',
            'pssh': pssh,
            'buildInfo': '',
            'proxy': '',
            'cache': True,
        }
        resp = requests.post('https://cdrm-project.com/wv', headers=headers_cdrm, json=json_data).text
        keys = re.findall("<li style=\"font-family:'Courier'\">(.+?)<\/li>", resp)
        if keys:
            for match in keys:
                mykeys = ''
                mykeys += match + '--key'
            if mykeys.endswith('--key'):
                mykeys = mykeys[:-5]
            divides('Key(s)')
            print(mykeys)
            return mykeys 
      
        
    # find mpd/m3u8 in vmap
    def parse_vmap(content):
        lines = content.split("\n")
        for line in lines:
            m = re.search(r'contenturi="(https.+?)" contentlength=', line)
            if m:
                media_url = m.group(1)
                return(media_url)
        
        return None
    
    # add leading zero to series or episode
    def pad_number(match):
        number = int(match.group(1))
        return format(number, "02d")
    
    
    if __name__ == '__main__':
        #### main #### 
        divides("null")
        print()
        title = PF.figlet_format(' BoltDNS dot net ', font='smslant')
        print(colored(title, 'green'))
        divides('A Generic Boltdns.net single downloader')
        from sys import argv
        #print(argv)
        if argv and len(argv) > 1:
            media_url = argv[1]
            videoname = argv[2]
            videoname = input(f"{videoname} \nVideoname?? ")
        else:
            print('\n\n[info] URLs may be of the form:- mpd, master.m3u8 or vmap from boltdns.net.')
            print('\n\n[info] Use Stream Detector "copy as Table Entry"')
            input("Ready to read clipboard TSD Table Entry: ")
            line = PC.paste()
            if 'master.m3u8?behavior_id' in line: # keeps changing!!
                STV = True
            else:
                STV = False   
            linetuple = line.split('|')
            media_url = linetuple[0].replace('\n','').replace(' ', '')
            print(f"[info] downloading {media_url}")
            # collect videoname from table entry
            # each site has different naming conventions
            # this may need tweeking ...
            if STV:
                videoname1 = linetuple[1]
                videoname2 = linetuple[2].split(',')
                videoname2 = videoname2[0]
                #videoname = (videoname1 + videoname2).replace(' ','_').replace('__','-')  
                videoname = videoname2.replace(' ','_').replace('__','-').replace(':','.')
            else:
                videoname = linetuple[1].replace('\n','').replace(' ','_')\
                    .replace('_Watch_','').replace('_Online_','')
            if videoname.endswith('_'):
                videoname = videoname.rstrip(videoname[-1])
            if videoname.startswith('_'):
                videoname = videoname[1:]
            if not STV:
                videoname = videoname.replace('Series_','S').replace('_Episode_','E')\
                    .replace("'", '').replace('(','').replace(')','').replace(':','.').replace('&','and')
                videoname = re.sub(r"(\d+)", pad_number, videoname)
            # ... or reverting
            #videoname = input("Enter Video name to save: (without mkv) : ") 
    
        #### decide type dash (mpd),  m3u8 or vmap
        dash = "dash" #encrypted
        m3u8 = 'm3u8'
        vmap = 'vmap' #encrypted or not
    
        if vmap in media_url:
            content = requests.get(media_url, headers).text
            media_url = parse_vmap(content)  # either m3u8 or dash
        if dash in media_url:
            pssh, lic = get_pssh_lic(media_url)
            # need keys
            mykeys = '' 
            mykeys = WV_Function(pssh, lic)
          
            divides("getting encrypted streams")
            command = [
                "N_m3u8DL-RE",
                media_url,
                "--auto-select",
                "-sv",
                "best",
                "-sa",
                "id='audio-0'",
                "-ss",
                "id='subtitles-0':for=all", 
                "--save-name",
                videoname,
                "--save-dir",
                "./output",
                "--tmp-dir",
                "./",
                "-mt",
                #"--use-shaka-packager",  ## producing error intermittently so revert to ffmpeg
                "--key",
                mykeys,
                #"-M",
                #"format=mkv:muxer=mkvmerge",
                ]
            subprocess.run(command)
            
        elif m3u8 in media_url: 
            divides('Getting encryption free stream')
            command = [
            "N_m3u8DL-RE",
            media_url,
            "--binary-merge",
            "--auto-select",
            "-sv",
            "best",
            "-sa",
            "id='audio-0'",
            "-ss",
            "id='subtitles-0':for=all",
            "--save-name",
            videoname,
            "--save-dir",
            "./output/",
            "--tmp-dir",
            "./",
            "-mt",
            #"--use-shaka-packager",
            #"-M",
            #"format=mkv:muxer=mkvmerge",
            ]
            subprocess.run(command)
        if os.path.isfile(f"./output/{videoname}.en.srt"):
            os.system(f"perl -i -pe  's/<.*?>//gm' ./output/{videoname}.en.srt")
            # attempt a
            '''with open(f'./output/{videoname}.en.srt', 'r') as original: data = original.read()
            with open(f'./output/{videoname}.srt', 'w') as modified: modified.write("WEBVTT\n\n" + data)
            original.close()
            modified.close()'''
            os.system(f"mkvmerge -q --no-date -o ./output/'{videoname}'.mkv \
                  -S -B -M --language 0:en --default-duration 0:25000/1000p \
                  --fix-bitstream-timing-information 0:1 ./output/'{videoname}'.mp4 \
                  --language 0:en ./output/'{videoname}'.en.m4a --language 0:en \
                  --track-name 0:English --default-track 0:0 --forced-track 0:0 \
                   ./output/'{videoname}'.en.srt")
    
        elif os.path.isfile(f"./output/{videoname}.mp4"):
            os.system(f"mkvmerge -q --no-date -o ./output/'{videoname}'.mkv \
                    -S -B -M --language 0:en --default-duration 0:25000/1000p \
                    --fix-bitstream-timing-information 0:1 ./output/'{videoname}'.mp4 \
                    --language 0:en ./output/'{videoname}'.en.m4a")
        # tptvencore may have ts files
        elif os.path.isfile(f"./output/{videoname}.ts"):
            print(videoname)
            os.system(f"mkvmerge -q --no-date -o ./output/'{videoname}'.mkv \
                -S -B -M --language 0:en --default-duration 0:25000/1000p \
                --fix-bitstream-timing-information 0:1 ./output/'{videoname}'.ts \
                --language 0:en ./output/'{videoname}'.en.ts")
            
    
        os.system("rm -f ./output/*.m4a  ./output/*.srt ./output/*.mp4 ./output/*.ts") 
        #os.system("reset") # uncomment if  N_m3u8DL-RE leaves text unprinted in terminal
        exit(0)
    The above uses yet another API link from the CDM Seller's website which returns html. Enjoy it while you can.

    An ITVX batch downloader with no CDM required.

    Code:
    #!/usr/bin/env python3
    
    '''
    ITVXbatch.py - a batch downloader for ITVX
    coded by A_n_g_e_l_a
    v 2.0
    09:06:2023
    
    Requirements: N_m3u8DL-RE, shaka-packager and mkvmerge in $PATH and python modules
    pyperclip and termcolor may be needed, install with 'pip install pyperclip termcolor'
    
    Written for Linux systems using WKS-KEYS with a working CDM, place in the WKS-KEYS folder.
    Should work on Windows with .exe sprinkled where necessary 
    
    This program loads content from The Stream Detector to provide mpds and program name.
    
    The program reads the clipboard for 'Table Entries' saved from 'The Stream Detector'.
    
    Use 'The Stream Detector' browser plugin (Chrome + Firefox +  Brave + Kiwi-Browser (Android))
    Find here: Firefox version - https://addons.mozilla.org/en-US/firefox/addon/hls-stream-detector/
    Chrome version: https://github.com/rowrawer/stream-detector/releases  choose  hls_stream_detector-2.1X.XX.crx 
    and in chrome://extensions/  set Developer mode (top-right window) and drag and drop the crx file into the window to install
    Windows is  ot officially supported and you may need to unpack the crx file with winrar and the  drage the unpacked folder to
    the extensions window whilst switched to developer mode.
    
    First clear all old urls from The Stream Detector (TSD)
    And then set 'Copy stream URL as' to 'Table Entry' in The Stream Detector GUI
    
    Now just visit any number of video pages at ITV.com and the mpd urls will shown in TSD GUI. 
    Copy the Table Entry or Entries to the clipboard from  'The Stream Detector' window when finished selecting videos.
    Then run this program
    
    '''
    
    import os
    import base64
    import requests
    #from pywidevine.L3.cdm import deviceconfig
    #from base64 import b64encode
    #from pywidevine.L3.decrypt.wvdecryptcustom import WvDecrypt
    import subprocess
    import re
    from termcolor import colored
    import  pyperclip as PC
    import time
    import functools
    import math
    import pyfiglet as PF
    
    headers = {
        'User-Agent': 'Dalvik/2.9.8 (Linux; U; Android 9.9.2; ALE-L94 Build/NJHGGF)',
        'Accept': '*/*',
        'Accept-Language': 'en-GB,en;q=0.9',
        'Connection': 'keep-alive',
        'Referer': 'itv.com',
    }
    '''
    def WV_Function(pssh, lic_url, cert_b64=None):
        wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64=cert_b64, device=deviceconfig.device_android_generic)                   
        widevine_license = requests.post(url=lic_url, data=wvdecrypt.get_challenge(), headers=None)
        license_b64 = b64encode(widevine_license.content)
        wvdecrypt.update_license(license_b64)
        Correct, keyswvdecrypt = wvdecrypt.start_process()
        # chain any multiple keys with --key at the start of all but the first kid:key pair
        if Correct:
            mykeys = ''
            for key in keyswvdecrypt:
                mykeys += key + '--key'
        if mykeys.endswith('--key'):
            mykeys = mykeys[:-5]
        divides("key found")
        print(mykeys)
        return mykeys  
    '''
    
    def WV_Function(pssh, lic_url, cert_b64=None):
        lic_url = lic_url
        headers_cdrm = {
        'Connection': 'keep-alive',
        'Content-Type': 'application/json',
        'Origin': 'https://cdrm-project.com',
        'Referer': 'https://cdrm-project.com/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36',
        }
        json_data = {
            'license': lic_url,
            'headers': 'Connection: keep-alive',
            'pssh': pssh,
            'buildInfo': '',
            'proxy': '',
            'cache': True,
        }
        resp = requests.post('https://cdrm-project.com/wv', headers=headers_cdrm, json=json_data).text
        keys = re.findall("<li style=\"font-family:'Courier'\">(.+?)<\/li>", resp)
        if keys:
            for match in keys:
                mykeys = ''
                mykeys += match + '--key'
            if mykeys.endswith('--key'):
                mykeys = mykeys[:-5]
            divides('Key(s)')
            print(mykeys)
            return mykeys 
    
    
    ## pretty print screen divisions
    def divides(text):  
        #text = text
        l = len(text)
        count, lines = os.get_terminal_size()
        count = int(count)
        if count <= 78:
            count = int(count)
        else:
            count = int(math.ceil(count/2))
        count = count - l
        if text != 'null':   
            line = (chr(9601) * int(math.ceil(count/2)))
            #print("\n")
            print(colored(line, 'green'), " ",  text, " ",   colored(line, 'green'))  
        else:
            line = (chr(9601) * int(count + 10))
            print(colored(line, 'green'))
    
    def findlicense(mpd_url):
        bit = mpd_url.split('/',8)
        ContentID = bit[7].rsplit('_',2 )
        license = "https://itvpnp.live.ott.irdeto.com/Widevine/getlicense?CrmId=itvpnp&AccountId=itvpnp&ContentId=" + ContentID[0]
        divides('license')
        print(license)
        return license
    
    def generate_pssh(kid: str):
        str1 = '000000387073736800000000edef8ba979d64acea3c827dcd51d21ed000000181210'
        str3 = '48e3dc959b06'
        return base64.b64encode(bytes.fromhex(str1+kid+str3)).decode()
    
    # add leading zero to series or episode
    def pad_number(match):
        number = int(match.group(1))
        return format(number, "02d")
    
    def getnm3u8(mpd_url, pssh, videoname):
        # decrypt
        lic_url = findlicense(mpd_url)
        key = WV_Function(pssh, lic_url)
        # organize cookie
        cookie = mpd_url.split("&")
        cookie = cookie[1].split('exp%3D')
        cookie = 'Cookie: hdntl=exp=' + cookie[1].replace('%3D', '=').replace('%2A', '*').replace('nohubplus', 'hdntl,nohubplus')
        #print (cookie)
        divides('Downloading')                                                                                         
        command = [
            "N_m3u8DL-RE",
            mpd_url,
            '--append-url-params',
            '--header',
            cookie,
            '--header',
            "host': itvpnpdotcom.blue.content.itv.com",
            '--header',
            "User-Agent': Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 
            "--auto-select",
            "--save-name",
            videoname,
            "--save-dir",
            "./output",
            "--tmp-dir",
            "./",
            "-mt",
            "--use-shaka-packager",
            "--key",
            key,
            "-M",
            "format=mkv:muxer=mkvmerge",
            ]
        #print(f"\n\ncommand for N_m3u8DL-re is: {command}\n\n")
        subprocess.run(command)
    
    def timer(func):
        """Print the runtime of the decorated function"""
        @functools.wraps(func)
        def wrapper_timer(*args, **kwargs):
            start_time = time.perf_counter()    # 1
            value = func(*args, **kwargs)
            end_time = time.perf_counter()      # 2
            run_time = end_time - start_time    # 3
            #print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
            print(f"Program execution took {run_time:.2f} secs")
            return value
        return wrapper_timer
    
    @timer
    def main():
        fmpd = PC.paste().split('\n')
        mylen = len(fmpd)
        print(f"There are {mylen} videos to download")
        divides('ITVX')
        for line in fmpd:
            linetuple = line.split('|')
            mpd_url = linetuple[0].replace('\n','').replace(' ', '')
            print(f"[info] downloading {mpd_url}")  # A_Touch_of_Frost__S_1__E0_1
            videoname = linetuple[1].replace('\n','').replace('ITVX','').replace(':','_')\
                .replace('-','').replace('  Series ','_S').replace('  Episode ','E').replace(' ','_').lstrip('_').rstrip('_')
            # two digits for series and episode
            videoname = re.sub(r"(\d+)", pad_number, videoname)
            divides('downloading ' + videoname)
            # parse for KID to get PSSH
            mpd = requests.get(mpd_url,headers).text
            mylines = mpd.split("\n") 
            for myline in mylines:
            
                m = re.search('cenc:default_KID=\"(.+?)\">', myline)
                if m:
                    KID = m.group(1)
            KID = KID.replace('-', '')
            
            if KID != '':
                divides(f'Default_KID:')
                print(KID)
                pssh = generate_pssh(KID) 
                divides('pssh found')
                print(pssh)
            else:
                print('No KID was found; exiting!')
                exit(0)
      
            if not os.path.exists("./output/"):
                os.system("mkdir ./output/")
            # download    
            getnm3u8(mpd_url, pssh, videoname)
        divides("All Done.")
        print('[info] Your files are in ./output/\n')
    
    
    if __name__ == "__main__":
        divides("null")
        print()
        title = PF.figlet_format(' ITVX Downloader ', font='smslant')
        print(colored(title, 'green'))
        divides("An ITVX Batch Downloader")
        print("\nCopy  stream URL(s) as 'Table Entry' from The Stream Detector")
        print("When ready to copy from the clipboard....")
        input("Press Enter!")
        main()
        exit(0)
    Last edited by A_n_g_e_l_a; 9th Jun 2023 at 10:56. Reason: Updated
    Quote Quote  
  3. Originally Posted by A_n_g_e_l_a View Post
    l3.py without a CDM

    @sk8ordl3 made an interesting post recently
    thanks for showing the other examples, it's never easy to write an universal script for different sites

    i'll leave a version of this,
    which does not require python

    if the site for extra data in the headers, here use customdata,
    but you could also use x-dt-auth-token... etc

    Image
    [Attachment 71606 - Click to enlarge]


    windows batch: (.bat file)

    Code:
    @echo off
    setlocal enabledelayedexpansion
    2>nul del input.html
    2>nul del keys.txt
    
    set /p "lic_url=lic url: "
    set /p "pssh=pssh: "
    set /p "custom_data=custom data token: "
    
    curl.exe -s "https://cdrm-project.com/wv" ^
      -H "Connection: keep-alive" ^
      -H "Origin: https://cdrm-project.com" ^
      -H "Referer: https://cdrm-project.com/" ^
      -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" ^
      --data-raw "{\"license\":\"%lic_url%\",\"headers\":\"Connection: keep-alive\",\"customdata\":\"%custom_data%\",\"pssh\":\"%pssh%\",\"buildInfo\":\"\",\"proxy\":\"\",\"cache\":true}" > input.html
    
    set /p "filename=file name: "
    set /p "mpd=mpd url: "
    
    echo.
    for /f "tokens=3 delims=<>" %%a in ('findstr /r "<li.*>.*</li>" input.html') do (
        set line=--key %%a
    	echo|set /p=!line! >> keys.txt
    )
    set /p keys=<keys.txt
    echo.
    echo %keys%
    echo.
    
    N_m3u8DL-RE.exe %keys% --auto-select --save-name "%filename%" -M "format=mp4:muxer=ffmpeg" "%mpd%"
    2>nul del input.html
    2>nul del keys.txt
    Quote Quote  
  4. Nice work! I wrote a general downloader based on your findings. It should work on most basic sites where there's no payload. Things like JWT tokens and "?specConform=true" are handled automatically. If the api ever stops working(which I suspect it will), it's easy to swap for a local CDM.

    Uses the uncurl module to get necessary data.
    Prints keys by default and downloads with --download --file filename arguments. You can add --proxy to bypass geofences.

    cdl.py:
    Code:
    """
    Credit to @sk8ordi3 & @A_n_g_e_l_a for providing api url(s)
    
    Requirements:
    Python 3.7+
    N_m3u8DL-RE
    ffmpeg or mkvmerge
    mp4decrypt or shaka-packager
    
    Install necessary packages:
    pip install pyperclip uncurl beautifulsoup4
    
    Download:
    cdl.py --download --file filename 
    
    Proxy:
    cdl.py --proxy 123.456.789.10:8080
    
    Notes:
    Written on Linux but should work on all systems. 
    Downloads are found in /downloads folder. 
    N_m3u8DL-RE options can be adjusted in the download_stream function (line: 103)
    
    !Windows users! Do not use "copy as cURL(windows)" when curling the license.
    
    """
    import argparse
    import sys
    import requests
    import re
    import base64
    import pyperclip
    import uncurl
    import subprocess
    from pathlib import Path
    from bs4 import BeautifulSoup as bs
    
    def cURL() -> tuple:
        paste = pyperclip.paste().replace(" \\\n", "").replace("^", "")
        paste2 = re.sub(r"--data-raw.*", "", paste)
        context = uncurl.parse_context(paste2)
        headers = dict(context.headers)
        lic_url = context.url
        return headers, lic_url
    
    def get_kid(mpd_url: str) -> str:
        response = requests.get(mpd_url)
        if response.status_code != 200:
            raise ValueError(f"Failed to fetch manifest")
    
        match = re.search(r'default_KID="(.+?)"', response.content.decode('utf-8'))
        return match.group(1) if match else None
    
    def generate_pssh(kid: str) -> str:
        array_of_bytes = bytearray(b'\x00\x00\x002pssh\x00\x00\x00\x00')
        array_of_bytes.extend(bytes.fromhex("edef8ba979d64acea3c827dcd51d21ed"))
        array_of_bytes.extend(b'\x00\x00\x00\x12\x12\x10')
        array_of_bytes.extend(bytes.fromhex(kid.replace("-", "")))
        return base64.b64encode(bytes.fromhex(array_of_bytes.hex())).decode('utf-8')
    
    def get_pssh(mpd_url: str) -> str:
        try:
            kid = get_kid(mpd_url)
            pssh = generate_pssh(kid)
        except:
            print("\nPSSH cannot be generated from manifest. Please input manually")
            pssh = input("PSSH: ")
        return pssh
    
    def get_tokens(headers: dict) -> list:
        try:
            token_pairs = [
            (key, value) for key, value in headers.items()
            if isinstance(value, str) and (value.startswith("ey") 
            or value.startswith("Bearer"))
            ]
            tokens = [token for pair in token_pairs for token in pair]
            return tokens
        except:
            return None
    
    # Use this to make adjustments to license, headers, cookies, proxy etc.
    def addons(lic_url: str) -> str:
            if "drmtoday" in lic_url and lic_url.endswith("/cenc/"):
                lic_url = f"{lic_url}?specConform=true"
            return lic_url
    
    def parse_response(response: str) -> tuple:
        soup = bs(response, "html.parser")
        h2_tag = soup.find("h2")
        result = h2_tag.text if h2_tag else None
        error = h2_tag.text if result == "ERROR" else None
        p_tag = soup.find("p") if error else None
        error_message = p_tag.text.split("\n")[1] if error else None
        ol_tag = soup.find("ol")
        keys = ol_tag.text.strip() if ol_tag else None
        return result, keys, error_message
    
    def download_stream(mpd_url: str, file: str, keys: str):
        try:
            args = [
                'N_m3u8DL-RE',
                '--key',
                keys,
                mpd_url,
                '--auto-select',
                '-mt',
                '-M',
                'format=mp4:muxer=ffmpeg',
                '--save-name',
                file,
                '--save-dir',
                'downloads'
            ]
            subprocess.run(args, check=True)
        except:
            print('Failed downloading stream!')
            raise
    
    def create_argument_parser():
        parser = argparse.ArgumentParser(description="General downloader")
        parser.add_argument(
            "--download",
            help="Download the stream",
            action="store_true"
        )
        parser.add_argument(
            "--file",
            help="Filename of video"
        )
        parser.add_argument(
            "--proxy",
            help="Use a proxy"
        )
    
        args = parser.parse_args()
        return args
    
    def main():
        parser = create_argument_parser()
        download = parser.download
        file = parser.file
        proxy = parser.proxy if parser.proxy else ""
    
    
        api_url = "https://cdrm-project.com/wv"
        # pssh = input("\nPSSH: ")
        mpd_url = input("\nMPD_URL: ")
        curl = "cURL: Copy the license as cURL and press Enter to continue... "
        input(curl)
    
        headers, lic_url = cURL()
        lic_url = addons(lic_url)
        pssh = get_pssh(mpd_url)
        tokens = get_tokens(headers)
        if tokens:
            key, value = tokens
            headers = f'{key}: "{value}"'
        else:
            headers = "User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 \
                (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    
        api_headers = {
            'Connection': 'keep-alive',
            'Content-Type': 'application/json',
            'Origin': 'https://cdrm-project.com',
            'Referer': 'https://cdrm-project.com/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 \
                (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36',
        }
    
        myjson = {
            "license": lic_url,
            "headers": headers,
            "pssh": pssh,
            "buildInfo": "", 
            "proxy": proxy,  
            "cache": True 
            }
    
        r = requests.post(api_url, headers=api_headers, json=myjson).text
        result, keys, error_message = parse_response(r)
    
        print(f"\n{result}")
        if result == "ERROR":
            print(f"\n{error_message}")
            sys.exit(1)
        else:    
            print(f"\n--key {keys}\n")
        
        if download:
            Path("downloads").mkdir(parents=True, exist_ok=True)
            if file.endswith(".mkv") or file.endswith(".mp4"):
                file = file[:-4]
            download_stream(mpd_url, file, keys)
    
    if __name__ == "__main__":
        main()
    Quote Quote  
  5. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by stabbedbybrick View Post
    I wrote a general downloader..... It should work on most basic sites where there's no payload.
    Excellent.

    It's getting good here now isn't it?
    Quote Quote  
  6. cdrm-project.com
    Join Date
    Jun 2022
    Location
    USA
    Search PM
    I don't plan on taking down the API (https://cdrm-project.com/api) - it's just not very versatile which is why I launched a different api (https://api.cdrm-project.com) that uses rlapheonixs pywidevine serve.py to serve the new API, as it has more versatility.

    I might update the instructions again to include both - but fear not they will both stay up - everything public facing on my end now serves emulator CDMs, so I'm not too worried.

    Glad to see the site being circulated more around here.

    Happy decrypting all.

    - PS. Love you long time a_n_g_e_l_a
    Quote Quote  
  7. Appreciate the response, @TPD94. It's nice to know you plan on keeping them up. I can't imagine these scripts will result in any kind of abuse.
    Quote Quote  
  8. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by TPD94 View Post
    I don't plan on taking down ...[snip]

    - PS. Love you long time a_n_g_e_l_a
    In that case I understand you need to repair your Bot! The Bot database is dumped to https://big.thefileditch.ch/ but that site has been down for nearly two weeks. anonfiles.com allows up to 20Gb these days.
    Quote Quote  
  9. For Stabbedbybrick's script, when I download "N_m3u8DL-RE" from Github, my Antivirus says it's a file with a virus. I can't trust such files.

    Is there any other thing to use instead of "N_m3u8DL-RE"?
    Quote Quote  
  10. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by ridibunda View Post
    For Stabbedbybrick's script, when I download "N_m3u8DL-RE" from Github, my Antivirus says it's a file with a virus. I can't trust such files.

    Is there any other thing to use instead of "N_m3u8DL-RE"?
    Try the 1.6 release. Other windows users have reported the virus trigger issue, but not all, using this new 1.8 version.
    Quote Quote  
  11. Member
    Join Date
    Jun 2023
    Location
    Brasil
    Search Comp PM
    ./
    Last edited by fhdskjahl; 18th Jun 2023 at 13:01.
    Quote Quote  
  12. Member
    Join Date
    Jun 2023
    Location
    Brasil
    Search Comp PM
    ./
    Last edited by fhdskjahl; 18th Jun 2023 at 13:01.
    Quote Quote  
  13. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by fhdskjahl View Post
    Code:
    ..snipped
    Whoever is copy-pasting these horrendous scripts should take a moment to clean them up first before re-posting them
    People in glass houses shouldn't throw stones. Your script contains an unused import - 'import platform' ; people "should take a moment to clean them up first before re-posting them".

    And I'm always suspicious of using classes for encapsulation when encapsulation is not needed - as is the case here. All those 'self ' declarations cluttering defs would disappear .

    Secondly, count the key presses you need to download your three example files and get your program set-up to run by first creating, populating and saving a text file. Compare all that to the ONE key press to select a LIST of mpd from The Stream Detector and ONE key press to set the program running using https://forum.videohelp.com/threads/409937-No-CDM-No-Problem!#post2693032 (scroll to "An ITVX batch downloader with no CDM required."

    Interesting approach (thanks for posting) but much too hard work for me to use.

    Edit: I just added a function timer and ran both your script and mine to download a single program
    Yours took 18.60 seconds to complete
    Mine took 17.34 seconds.

    I think the difference is down to all the lookups for data parsing of the html that goes on in your script.
    /edit

    Horses for courses.

    times shown.
    Mine
    Image
    [Attachment 71785 - Click to enlarge]

    Yours
    Image
    [Attachment 71786 - Click to enlarge]
    Last edited by A_n_g_e_l_a; 18th Jun 2023 at 07:26.
    Quote Quote  
  14. Member
    Join Date
    Jun 2023
    Location
    Brasil
    Search Comp PM
    ./
    Last edited by fhdskjahl; 18th Jun 2023 at 13:00.
    Quote Quote  
  15. Originally Posted by A_n_g_e_l_a View Post
    l3.py without a CDM

    @sk8ordl3 made an interesting post recently and this has grown from it. I'm not sure how long it will be allowed to work.

    Below is code for a drop in replacement for l3.py.

    Code:
    '''
    cdrm-project (website of a CDM seller) put up a freely accessible L3 Content Decryption Module and then took down the API details so no-one could use it
    without first obtaining an API key.
    
    @sk8ordl3 posted some code in this forum that used the cdrm-project api, BUT without an api key; which got my attention.  Thanks sk8ordl3.
    
    The wayback machine has the old API.
    
    What follows is a drop in replacement for l3.py which does not need you to have a CDM
    '''
    
    import requests,json
    
    api_url = "https://cdrm-project.com/api"
    license_url = input("License url? ")
    pssh = input("PSSH? ")
    headers = {'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'}
    myjson={ "license":license_url, "pssh":pssh, "proxy":"",  "cache":False }
    r = requests.post(api_url,headers=headers ,json=myjson).text
    #print(r)
    mydict = json.loads(r)
    print("\n------------ Keys ------------\n")
    keys = mydict['keys']
    for key in keys:
      print (key['key'])
    It works for simple sites that do not need headers of any complexity.

    Test with License: https://cwip-shaka-proxy.appspot.com/no_auth
    PSSH: AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62 dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xq YVNkZmFsa3IzaioCSEQyAA==

    Newbie corner

    To use: save the code in the white box to a file called cl3.py
    Then, assuming you have python on your machine, open a command window in the directory where you saved cl3.py and type
    Code:
    python cl3.py
    paste responses in the command window to the questions asked.
    Not work for me

    Image
    [Attachment 71949 - Click to enlarge]
    Quote Quote  
  16. Member
    Join Date
    Feb 2022
    Location
    Search the forum first!
    Search PM
    Originally Posted by euphonic View Post

    Not work for me.
    Does it work on the test site? If so, cl3.py is not enough to get keys from the the site you want. cl3.py is only suitable for simple sites without the need for headers or json payloads sent. Keep reading the stickies and learn to walk before running.
    Quote Quote  



Similar Threads

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