VideoHelp Forum




+ Reply to Thread
Page 2 of 2
FirstFirst 1 2
Results 31 to 35 of 35
  1. Feels Good Man 2nHxWW6GkN1l916N3ayz8HQoi's Avatar
    Join Date
    Jan 2024
    Location
    Pepe Island
    Search Comp PM
    Alright. So here's the script

    Code:
    import base64
    import json
    import os
    import re
    from os.path import join
    from urllib.parse import quote
    
    import isodate
    import requests
    import xmltodict
    from pssh_box import _parse_boxes, widevine_pssh_data_pb2, _generate_widevine_data
    from pywidevine import PSSH, Device, Cdm
    from slugify import slugify
    
    USE_MPD = True
    INPUT_FILE = "input.txt"
    WVD_FILE = "cdm.wvd"
    INTRO_DURATION = 5
    TEMP_MPD_FOLDER = "temp_mpd"
    
    DEFAULT_LICENSE_URL = 'https://content-aeui1.uplynk.com/wv'
    VIDEO_PLAYER_URL = 'https://cxm-api.fifa.com/fifaplusweb/api/videoPlayerData/{video_id}'
    PREPLAY_URL = 'https://content.uplynk.com/preplay/{content_id}/multiple.json'
    
    
    def get_keys(license_url, pssh):
        if license_url is None:
            license_url = DEFAULT_LICENSE_URL
    
        pssh = PSSH(pssh)
        device = Device.load(WVD_FILE)
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
        challenge = cdm.get_license_challenge(session_id, pssh)
    
        response = requests.post(license_url, data=challenge)
        response.raise_for_status()
    
        cdm.parse_license(session_id, response.content)
        keys = []
        for key in cdm.get_keys(session_id):
            if "CONTENT" not in key.type:
                continue
            keys.append(f'{key.kid.hex}:{key.key.hex()}')
        cdm.close(session_id)
    
        return keys
    
    
    def get_mpd_command(manifest, video_title):
        manifest = manifest.replace(".m3u8?", ".mpd?")
        response = requests.get(manifest).content.decode()
        response = xmltodict.parse(response)
    
        kids_psshs = []
        periods = response["MPD"].get("Period", [])
        if type(periods) is not list:
            periods = [periods]
    
        filtered_periods = []
        for period in periods:
            if period.get("@duration", None) is None:
                continue
            duration = isodate.parse_duration(period["@duration"])
            duration = duration.total_seconds()
            if duration < INTRO_DURATION:
                continue
    
            filtered_periods.append(period)
            adaptations = period.get("AdaptationSet", [])
            if type(adaptations) is not list:
                adaptations = [adaptations]
    
            for adaptation in adaptations:
                protections = adaptation.get("ContentProtection", [])
                if type(protections) is not list:
                    protections = [protections]
    
                current_kid, current_pssh, license_url = None, None, None
                for protection in protections:
                    if protection.get("@cenc:default_KID", None) is not None:
                        current_kid = protection["@cenc:default_KID"]
                    if PSSH.SystemId.Widevine.__str__() in protection.get("@schemeIdUri", ""):
                        current_pssh = protection["cenc:pssh"]["#text"]
                        license_url = protection.get("ms:laurl", {}).get("@licenseUrl", None)
    
                    if current_kid is not None and current_pssh is not None:
                        break
    
                kids_psshs.append((current_kid.replace("-", ""), current_pssh, license_url))
    
        content_id_dict = {}
        pssh_data_dict = {}
        for k, p, l in kids_psshs:
            pssh_box = _parse_boxes(base64.b64decode(p))[0]
            pssh_data = widevine_pssh_data_pb2.WidevinePsshData()
            pssh_data.ParseFromString(pssh_box.pssh_data)
    
            if not pssh_data.HasField('content_id'):
                continue
            content_id = str(base64.b16encode(pssh_data.content_id).decode())
    
            provider = None
            if pssh_data.HasField('provider'):
                provider = pssh_data.provider
            protection_scheme = None
            if pssh_data.HasField('protection_scheme'):
                protection_scheme = pssh_data.protection_scheme
    
            content_id_dict[content_id] = content_id_dict.get(content_id, []) + [(k, l)]
            if pssh_data_dict.get(content_id, None) is None:
                pssh_data_dict[content_id] = (provider, protection_scheme, pssh_data.content_id)
    
        merged_psshs = []
        for cid, kid_license in content_id_dict.items():
            kids = [kid.encode() for kid, _ in kid_license]
            provider, protection_scheme, raw_cid = pssh_data_dict[cid]
    
            pssh_data = _generate_widevine_data(
                kids,
                content_id=raw_cid,
                provider=provider,
                protection_scheme=protection_scheme
            )
            merged_pssh = PSSH.new(init_data=pssh_data, system_id=PSSH.SystemId.Widevine)
            merged_psshs.append((merged_pssh.__str__(), kid_license[0][1]))
    
        keys = []
        for pssh, license_url in merged_psshs:
            keys += get_keys(license_url, pssh)
    
        assert len(keys) > 0
        keys = ' '.join([f'--key {k}' for k in keys])
    
        response["MPD"]["Period"] = filtered_periods
        response = xmltodict.unparse(response, pretty=True)
    
        output_path = join(TEMP_MPD_FOLDER, f"{video_title}.mpd")
        with open(output_path, "w") as f:
            f.write(response)
        return (
            f'N_m3u8DL-RE "{output_path}" {keys} --save-dir "fifa_com" --save-name "{video_title}" -M format=mkv'
            f' & '
            f'del "{output_path}"'
        )
    
    
    def get_command(url):
        video_id = re.search(r"/watch/([^/?]*)", url).group(1)
        response = requests.get(
            VIDEO_PLAYER_URL.format(video_id=video_id),
            params={'locale': 'en'}
        )
    
        status_code = response.status_code
        if status_code == 404:
            raise Exception("Content not available")
    
        response = json.loads(response.content.decode())
        video_title = slugify(response.get('title', video_id))
    
        response = response["preplayParameters"]
        preplay_url = PREPLAY_URL.format(content_id=response["contentId"])
        if response.get("queryStr", None) is not None:
            preplay_url += "?" + response["queryStr"]
        if response.get("signature", None) is not None:
            if "?" not in preplay_url:
                preplay_url += "?"
            else:
                preplay_url += "&"
            preplay_url += "sig=" + quote(response["signature"])
    
        response = json.loads(requests.get(preplay_url).content.decode())["playURL"]
    
        if USE_MPD:
            return get_mpd_command(response, video_title)
        return f'N_m3u8DL-RE "{response}" --save-dir "fifa_com" --save-name "{video_title}"'
    
    
    if __name__ == '__main__':
        if not os.path.exists(TEMP_MPD_FOLDER):
            os.makedirs(TEMP_MPD_FOLDER)
    
        with open(INPUT_FILE, 'r') as file:
            urls = file.readlines()
        urls = [u.strip() for u in urls]
        urls = list(dict.fromkeys([u for u in urls if len(u) > 0 and "plus.fifa.com" not in u]))
    
        index = 0
        nr = len(urls)
        for u in urls:
            index += 1
            print(f'[{index}/{nr}]')
    
            try:
                print(get_command(u))
            except Exception as e:
                print(f"Failed to get: {u}. Reason: {str(e)}. Try again later...")
    Input testing:
    Code:
    https://www.fifa.com/en/watch/01RkBvmA7uVcsiLLLRomhC
    https://www.fifa.com/en/watch/6lwu7h1Z5CRt5aYmwCP8mn
    https://www.fifa.com/en/watch/1rRt2pAV6xGmYW9v5zvtoq
    https://www.fifa.com/en/watch/3zT4IjsvIKHCId5WCxxnMX
    https://www.fifa.com/en/watch/5yqsXahmn0hwVh1ugfH9Xl
    https://www.fifa.com/en/watch/F2WJFEcD7KAeRZMjeBzlR
    https://www.fifa.com/en/watch/UiIXW4oXlTvF6W5ISZRY3
    https://www.fifa.com/en/watch/Uh0FqJlqKEIQqUBvJ97d3
    https://www.fifa.com/en/watch/6pefecqFMjxVS0Z11Flkqn
    https://www.fifa.com/en/watch/Kgl6LzzfkI6Pq6m8YzPuF
    https://www.fifa.com/en/watch/2yGOtlqSQhTSXIKadN89KM
    https://www.fifa.com/en/watch/6kqE5WhQnnGqLQRBAAKqJV
    Output:
    Code:
    [1/12]
    N_m3u8DL-RE "temp_mpd\spain-v-korea-republic-group-matches-1994-fifa-world-cup-usatm-full-match-replay.mpd" --key 2adca0f024af4d2cb283ff32114648c9:7057dc254bd7bff8b72981c7971fcf97 --key 79bdfc6ab57649dc9a019621b8f45c7b:72e147ee390c9a1bd4880fe22718e44a --key 3875508c669e4e0194d8ea2bfdb0ac10:32f4aba878c53be157eee132a46eb18c --save-dir "fifa_com" --save-name "spain-v-korea-republic-group-matches-1994-fifa-world-cup-usatm-full-match-replay" -M format=mkv & del "temp_mpd\spain-v-korea-republic-group-matches-1994-fifa-world-cup-usatm-full-match-replay.mpd"
    [2/12]
    N_m3u8DL-RE "temp_mpd\usa-v-colombia-group-matches-1994-fifa-world-cup-usatm-full-match-replay.mpd" --key 876b1bc04d474d11875519b9aaef1764:3a1df271d371c670b8dd9cb0927cc75a --key fe60355108834240847419ac9d36f328:05671c2d27eb6dcf42ba5e0c7b86dd33 --key 58ec2d4383c545f8b67fea0a73769f1d:6290017343acbe5ddb60ee96e9b1fbcc --save-dir "fifa_com" --save-name "usa-v-colombia-group-matches-1994-fifa-world-cup-usatm-full-match-replay" -M format=mkv & del "temp_mpd\usa-v-colombia-group-matches-1994-fifa-world-cup-usatm-full-match-replay.mpd"
    [3/12]
    N_m3u8DL-RE "temp_mpd\italy-v-spain-quarter-finals-1994-fifa-world-cup-usatm-full-match-replay.mpd" --key eac3724141ab47c7896a83478fa4792f:744c4a70c2ba8330960f1c1dd2dda432 --key bd50cc2b0f3645f0890fc1a86cf38884:24d683592da7c5a649ce9132f4aff618 --key cd533b0cc88b4344b3e0daed0bdf3a2c:874472c40e358a10edd0364c09462f08 --save-dir "fifa_com" --save-name "italy-v-spain-quarter-finals-1994-fifa-world-cup-usatm-full-match-replay" -M format=mkv & del "temp_mpd\italy-v-spain-quarter-finals-1994-fifa-world-cup-usatm-full-match-replay.mpd"
    [4/12]
    N_m3u8DL-RE "temp_mpd\netherlands-v-brazil-quarter-finals-1994-fifa-world-cup-usatm-full-match-replay.mpd" --key 521f512eb2e140b388d512a85d18edc6:aba6f354d46fb59e9dfbc1557c3cbc65 --key 6a69fa75a9904a60a5b8ae9861153771:91590d6c7abfc4df2497f8743e2281d9 --key 7c5c0fe5a6464322a9162182893246e3:bfe9b7da55416dd43698796b2ff1d19d --save-dir "fifa_com" --save-name "netherlands-v-brazil-quarter-finals-1994-fifa-world-cup-usatm-full-match-replay" -M format=mkv & del "temp_mpd\netherlands-v-brazil-quarter-finals-1994-fifa-world-cup-usatm-full-match-replay.mpd"
    [5/12]
    N_m3u8DL-RE "temp_mpd\spain-v-nigeria-group-d-1998-fifa-world-cup-francetm-full-match-replay.mpd" --key 3c8e15a8698748fa8a7f552c802e4137:703e925a92943374847a51de51ec76a7 --key a706a584f2c348d382b69d88f62dd7c4:fcd6f0fc84bc381e2c6f2f2706417168 --key 061c01aeaf7b44c19994c549ed61bf67:e4f80b0dc3de247481e14d306d726f55 --save-dir "fifa_com" --save-name "spain-v-nigeria-group-d-1998-fifa-world-cup-francetm-full-match-replay" -M format=mkv & del "temp_mpd\spain-v-nigeria-group-d-1998-fifa-world-cup-francetm-full-match-replay.mpd"
    [6/12]
    N_m3u8DL-RE "temp_mpd\netherlands-v-argentina-quarter-finals-1998-fifa-world-cup-francetm-full-match-replay.mpd" --key e09a7aab2a5849e0bf2af57257bbcbac:d5ac718aaa3a0d0ecdd0b22f71261cf9 --key 35eacb92904f41e1be78bce00e85180b:3c3f537c3ed5a68e3adb6e0f2a607458 --key 4fa897e1f2fa4015aa8b475f85b39c54:d70ea4cef75d9dc98045921a1e7663d2 --save-dir "fifa_com" --save-name "netherlands-v-argentina-quarter-finals-1998-fifa-world-cup-francetm-full-match-replay" -M format=mkv & del "temp_mpd\netherlands-v-argentina-quarter-finals-1998-fifa-world-cup-francetm-full-match-replay.mpd"
    [7/12]
    N_m3u8DL-RE "temp_mpd\argentina-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay.mpd" --key e4bbfdd3dc284b919738e3549c548c7d:365eaed4bc3e525751be4374060585d5 --key 0e96bf66722c473b88737ef0984f1691:fe5162fa0b3a5675506f2470cde6383d --key 5a4bef46ce2f4913bb988fa90713b37f:bfc81cd2ce0e6f24bf3450334aa2a740 --save-dir "fifa_com" --save-name "argentina-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay" -M format=mkv & del "temp_mpd\argentina-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay.mpd"
    [8/12]
    N_m3u8DL-RE "temp_mpd\italy-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay.mpd" --key 2ba03cb1ddf44c36823d3dfcd933b9e1:47fe839c1956de88f27223f31c61b9df --key 0c6174e5c0424002a3d5cd5de8c8c466:ed5ad9a5ead29057d86ec7bea1dae0be --key 2d3f794baec74c9680e884c68fb356b1:2c2b8cc746ce1c54bd151e15a8ddb867 --save-dir "fifa_com" --save-name "italy-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay" -M format=mkv & del "temp_mpd\italy-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay.mpd"
    [9/12]
    N_m3u8DL-RE "temp_mpd\germany-fr-v-france-semi-finals-1982-fifa-world-cup-spaintm-full-match-replay.mpd" --key 54d7ab1237de4b06a1353f5f5700ba69:7fe9bfbd74ba71f3f8f9ed000c9b0b7f --key 42a1bed45ad14c629f6f0d6d39adc053:24f82ba564dc400bac67dc2274a48a07 --key a3ac8d8fb4b74e888df7f4e08b1ee691:0d2ac824bfed2bf4c3b90b3ceb1fc549 --save-dir "fifa_com" --save-name "germany-fr-v-france-semi-finals-1982-fifa-world-cup-spaintm-full-match-replay" -M format=mkv & del "temp_mpd\germany-fr-v-france-semi-finals-1982-fifa-world-cup-spaintm-full-match-replay.mpd"
    [10/12]
    N_m3u8DL-RE "temp_mpd\portugal-v-morocco-group-matches-1986-fifa-world-cup-mexicotm-full-match-replay.mpd" --key ba71f25f0a6f4674b253926e3b93cafb:c8bd30455298bc690b9a523366ed4d0f --key 5cdf865b3524473abf1f095842a71c4e:19f8beccc7b5099ab21ff0681a7c05e1 --key c85fa6b6d7d34850bf823fbd428c6400:8f1a3903794579e1d73795b789d0e1f4 --save-dir "fifa_com" --save-name "portugal-v-morocco-group-matches-1986-fifa-world-cup-mexicotm-full-match-replay" -M format=mkv & del "temp_mpd\portugal-v-morocco-group-matches-1986-fifa-world-cup-mexicotm-full-match-replay.mpd"
    [11/12]
    N_m3u8DL-RE "temp_mpd\england-v-cameroon-quarter-finals-1990-fifa-world-cup-italytm-full-match-replay.mpd" --key 88958a17e0224c5cbdde765432bdc166:4c8c1913dd90c5ff4d835856b7f360a0 --key e0b728636f6a48bbb08c094a75cb9bb2:05b234a5032b1484301ad8e6e8a63de4 --key a47096c71d794db09a185f5d71218afa:655a7bab3be9a81c4fa4c192e3c3d502 --save-dir "fifa_com" --save-name "england-v-cameroon-quarter-finals-1990-fifa-world-cup-italytm-full-match-replay" -M format=mkv & del "temp_mpd\england-v-cameroon-quarter-finals-1990-fifa-world-cup-italytm-full-match-replay.mpd"
    [12/12]
    N_m3u8DL-RE "temp_mpd\england-v-egypt-group-matches-1990-fifa-world-cup-italytm-full-match-replay.mpd" --key e4ae592f3add4444937d83398d58b9f7:c13004723b5bdf597c75240a6a7ddaa8 --key 4eb93e877fa84885bffa89f130406499:fdddc88a1c0bb62b07792c99e5a09386 --key 86bff977a4d440dcb644234d9248db1a:1280fd7065b1514b1c7ec6a2beff5172 --save-dir "fifa_com" --save-name "england-v-egypt-group-matches-1990-fifa-world-cup-italytm-full-match-replay" -M format=mkv & del "temp_mpd\england-v-egypt-group-matches-1990-fifa-world-cup-italytm-full-match-replay.mpd"
    Installation:
    - install the necessary packages
    Code:
    pip install isodate
    pip install requests
    pip install xmltodict
    pip install protobuf
    pip install pywidevine
    pip install python-slugify
    - create an empty folder
    - download https://github.com/shaka-project/shaka-packager/releases/download/v3.2.0/pssh-box.py.tar.gz ( from https://github.com/shaka-project/shaka-packager/releases ) and extract that archive in the empty folder
    - rename pssh-box.py to pssh_box.py, create an empty file script.py and place the script I posted in it
    - create an empty file input.txt where you're gonna place all your fifa.com/watch links, also get a cdm in wvd format (thanks to @karoolus you can download it from here https://forum.videohelp.com/threads/413719-Ready-to-use-CDMs-available-here! )
    - convert that cdm to wvd format by running
    Code:
    pywidevine create-device -k private_key.pem -c client_id.bin -t "ANDROID" -l 3 -o output_folder
    - rename the generated file to cdm.wvd, at this point the structure of that folder will contain 2 python files, 1 folder, 1 text file, 1 wvd file
    - run the script using
    Code:
    python script.py
    Now you can download the hidden version of fifa.com links that redirect. We're gonna compare the files. One example is this.
    https://www.fifa.com/en/watch/Uh0FqJlqKEIQqUBvJ97d3

    I'm just gonna use the key provided by @ddll
    Code:
    N_m3u8DL-RE "http://d33tkx2907c8gi.cloudfront.net/s/vod/main_100/FIM100692-M00_v05.263fa186_SDR_20240326-1340/FIM100692-M00_v05.263fa186_SDR.eng_mul.h264_SDHDP.cenc_subs.mpd" --key 0180974b56c1d51f911a32add5d0e827:766601f890cea819bd5a4860b5380bea -M format=mkv
    And the shortened code for the hidden version with the modified manifest.
    Code:
    N_m3u8DL-RE "temp_mpd\italy-v-brazil-second-round-1982-fifa-world-cup-spaintm-full-match-replay.mpd" --key 2ba03cb1ddf44c36823d3dfcd933b9e1:47fe839c1956de88f27223f31c61b9df --key 0c6174e5c0424002a3d5cd5de8c8c466:ed5ad9a5ead29057d86ec7bea1dae0be --key 2d3f794baec74c9680e884c68fb356b1:2c2b8cc746ce1c54bd151e15a8ddb867 -M format=mkv
    The videos shows some differences.
    Image
    [Attachment 81382 - Click to enlarge]


    So it's up to you which one you pick @OP. In this rare case even the audio differs. One has commentary added while the other only has the crowd cheering. You could take the audio from one and mux it into the other. You decide.

    By the way, @LZAA, I'm still curious how you merged the psshs. Maybe your way is better.
    --[----->+<]>.++++++++++++.---.--------.
    [*drm mass downloader: widefrog*]~~~~~~~~~~~[*how to make your own mass downloader: guide*]
    Quote Quote  
  2. This is what I came up with:
    Code:
        psshs = [
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgOWMxOWY5YWI4MjM5NDNmMGFjMzI2ZWVmN2NhMzhkNmQaBHZkbXMiRzlhNDk3YTZlZTY5NDQxYzY5MzcxNDk5MTA1ZWZmYmNjX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgMDQ1M2Y1YTAwOTE2NGRmODhkM2FiYTMzNTkyMzA5ZTMaBHZkbXMiRzlhNDk3YTZlZTY5NDQxYzY5MzcxNDk5MTA1ZWZmYmNjX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgZThjNGZmZmZlZTIxNDBjOGI1OGY4OTg2NGMzZDYxNGEaBHZkbXMiRzlhNDk3YTZlZTY5NDQxYzY5MzcxNDk5MTA1ZWZmYmNjX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgYzA3OGIyOWEyNGY1NDdjZWIyMTBkNTU1MTMxYTY5MTQaBHZkbXMiRzYzYTg3MTZkMDkxMTQ5YmFiZGU5YTg1NWVjMzcyM2VlX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgZThlMWYzMTcyMDkyNDQxYmIyZjQ1NjUwZTZmZGY4MWEaBHZkbXMiRzYzYTg3MTZkMDkxMTQ5YmFiZGU5YTg1NWVjMzcyM2VlX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgNTIzMzVhYmJjMWIwNDg0ZjkyODhkMzA1YmQwMWJhODYaBHZkbXMiRzYzYTg3MTZkMDkxMTQ5YmFiZGU5YTg1NWVjMzcyM2VlX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgOGNkZjllZDUzMDRhNGQ0MDgyZGRiNmFhZmI1YjRjYmIaBHZkbXMiRzhjZjE0ZTNkMjQ0MjQ1ZjRhZGRkNWJiZWQ0OGEzNDNkX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgOGE1ZGNiNTFmNzI2NDE2Njg1Nzg1NWYxNGMyMmNjMTIaBHZkbXMiRzhjZjE0ZTNkMjQ0MjQ1ZjRhZGRkNWJiZWQ0OGEzNDNkX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2",
            "AAAAk3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAHMIARIgYzM3ZTY2NGZmN2YwNGI0MmE0OTk5MDZjMTJkYjUzYWMaBHZkbXMiRzhjZjE0ZTNkMjQ0MjQ1ZjRhZGRkNWJiZWQ0OGEzNDNkX2FldWkxXzEyYTQxNWE5ZjRjZjQ1ZGI4ZTczM2JjNmU5NzI2YzQ2"
        ]
    
        def to_pssh_data(raw_pssh: str) -> WidevinePsshData:
            wv_pssh_data = WidevinePsshData()
            wv_pssh_data.ParseFromString(base64.b64decode(raw_pssh)[32:])
            return wv_pssh_data
    
        pssh_data = list(map(
            to_pssh_data,
            psshs
        ))
    
        def create_license_url(content_id: str):
            args = content_id.split("_")
            return f'https://content-{args[1]}.uplynk.com/wv?b={args[0]}&v={args[0]}&pbs={args[2]}'
    
        def filter_pssh(content_id: str) -> tuple[str, str]:
            collected = list(filter(
                lambda data: content_id == data.content_id,
                pssh_data
            ))
            merged = WidevinePsshData()
            merged.provider = collected[0].provider
            merged.content_id = collected[0].content_id
            for key_id in list(map(
                lambda data: data.key_ids,
                collected
            )):
                merged.key_ids.extend(key_id)
    
            return PSSH.new(
                system_id=PSSH.SystemId.Widevine,
                init_data=merged.SerializeToString()
            ).dumps(), create_license_url(merged.content_id.decode())
    
        combined = list(map(
            filter_pssh,
            set(map(
                lambda data: data.content_id,
                pssh_data
            ))
        ))
    Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2
    Quote Quote  
  3. Feels Good Man 2nHxWW6GkN1l916N3ayz8HQoi's Avatar
    Join Date
    Jan 2024
    Location
    Pepe Island
    Search Comp PM
    Apparently they fixed their m3u8 content serving. So it was some maintenance going on. @OP, you can switch between encrypted mpd and unencrypted m3u8 in the script from post #31 by switching this line to False
    Code:
    USE_MPD = False
    Originally Posted by larley View Post
    This is what I came up with:
    Are you using the latest pssh-box implementation? I had to switch some key_ids to key_id and also the import. Also, are those 3 merged psshs working for you? I get 403 in the license call.

    Edit: I notice that you generated the license url parameters from the pssh content id. Nice find. Had no idea they were connected.
    Last edited by 2nHxWW6GkN1l916N3ayz8HQoi; 11th Aug 2024 at 01:50.
    --[----->+<]>.++++++++++++.---.--------.
    [*drm mass downloader: widefrog*]~~~~~~~~~~~[*how to make your own mass downloader: guide*]
    Quote Quote  
  4. Originally Posted by 2nHxWW6GkN1l916N3ayz8HQoi View Post
    Are you using the latest pssh-box implementation?
    I don't really need it, because, when going from PSSH Box to PSSH Data, I'm just cutting off the first 32 bytes, which is the PSSH Box part, and I never convert anything back into a box, because pywidevine also just accepts PSSH Data

    Yes, they're also working, although the second one sometimes worked and sometimes didn't and the third one never did, probabaly because I was using a for-loop to iterate over the output list, and the server was giving me timeouts. (I tried adding a second delay after each request, but that didn't make it any better)
    Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2
    Quote Quote  
  5. Originally Posted by 2nHxWW6GkN1l916N3ayz8HQoi View Post
    Alright. So here's the script

    Code:
    import base64
    import json
    import os
    import re
    from os.path import join
    from urllib.parse import quote
    
    import isodate
    import requests
    import xmltodict
    from pssh_box import _parse_boxes, widevine_pssh_data_pb2, _generate_widevine_data
    from pywidevine import PSSH, Device, Cdm
    from slugify import slugify
    
    USE_MPD = True
    INPUT_FILE = "input.txt"
    WVD_FILE = "cdm.wvd"
    INTRO_DURATION = 5
    TEMP_MPD_FOLDER = "temp_mpd"
    
    DEFAULT_LICENSE_URL = 'https://content-aeui1.uplynk.com/wv'
    VIDEO_PLAYER_URL = 'https://cxm-api.fifa.com/fifaplusweb/api/videoPlayerData/{video_id}'
    PREPLAY_URL = 'https://content.uplynk.com/preplay/{content_id}/multiple.json'
    
    
    def get_keys(license_url, pssh):
        if license_url is None:
            license_url = DEFAULT_LICENSE_URL
    
        pssh = PSSH(pssh)
        device = Device.load(WVD_FILE)
        cdm = Cdm.from_device(device)
        session_id = cdm.open()
        challenge = cdm.get_license_challenge(session_id, pssh)
    
        response = requests.post(license_url, data=challenge)
        response.raise_for_status()
    
        cdm.parse_license(session_id, response.content)
        keys = []
        for key in cdm.get_keys(session_id):
            if "CONTENT" not in key.type:
                continue
            keys.append(f'{key.kid.hex}:{key.key.hex()}')
        cdm.close(session_id)
    
        return keys
    
    
    def get_mpd_command(manifest, video_title):
        manifest = manifest.replace(".m3u8?", ".mpd?")
        response = requests.get(manifest).content.decode()
        response = xmltodict.parse(response)
    
        kids_psshs = []
        periods = response["MPD"].get("Period", [])
        if type(periods) is not list:
            periods = [periods]
    
        filtered_periods = []
        for period in periods:
            if period.get("@duration", None) is None:
                continue
            duration = isodate.parse_duration(period["@duration"])
            duration = duration.total_seconds()
            if duration < INTRO_DURATION:
                continue
    
            filtered_periods.append(period)
            adaptations = period.get("AdaptationSet", [])
            if type(adaptations) is not list:
                adaptations = [adaptations]
    
            for adaptation in adaptations:
                protections = adaptation.get("ContentProtection", [])
                if type(protections) is not list:
                    protections = [protections]
    
                current_kid, current_pssh, license_url = None, None, None
                for protection in protections:
                    if protection.get("@cenc:default_KID", None) is not None:
                        current_kid = protection["@cenc:default_KID"]
                    if PSSH.SystemId.Widevine.__str__() in protection.get("@schemeIdUri", ""):
                        current_pssh = protection["cenc:pssh"]["#text"]
                        license_url = protection.get("ms:laurl", {}).get("@licenseUrl", None)
    
                    if current_kid is not None and current_pssh is not None:
                        break
    
                kids_psshs.append((current_kid.replace("-", ""), current_pssh, license_url))
    
        content_id_dict = {}
        pssh_data_dict = {}
        for k, p, l in kids_psshs:
            pssh_box = _parse_boxes(base64.b64decode(p))[0]
            pssh_data = widevine_pssh_data_pb2.WidevinePsshData()
            pssh_data.ParseFromString(pssh_box.pssh_data)
    
            if not pssh_data.HasField('content_id'):
                continue
            content_id = str(base64.b16encode(pssh_data.content_id).decode())
    
            provider = None
            if pssh_data.HasField('provider'):
                provider = pssh_data.provider
            protection_scheme = None
            if pssh_data.HasField('protection_scheme'):
                protection_scheme = pssh_data.protection_scheme
    
            content_id_dict[content_id] = content_id_dict.get(content_id, []) + [(k, l)]
            if pssh_data_dict.get(content_id, None) is None:
                pssh_data_dict[content_id] = (provider, protection_scheme, pssh_data.content_id)
    
        merged_psshs = []
        for cid, kid_license in content_id_dict.items():
            kids = [kid.encode() for kid, _ in kid_license]
            provider, protection_scheme, raw_cid = pssh_data_dict[cid]
    
            pssh_data = _generate_widevine_data(
                kids,
                content_id=raw_cid,
                provider=provider,
                protection_scheme=protection_scheme
            )
            merged_pssh = PSSH.new(init_data=pssh_data, system_id=PSSH.SystemId.Widevine)
            merged_psshs.append((merged_pssh.__str__(), kid_license[0][1]))
    
        keys = []
        for pssh, license_url in merged_psshs:
            keys += get_keys(license_url, pssh)
    
        assert len(keys) > 0
        keys = ' '.join([f'--key {k}' for k in keys])
    
        response["MPD"]["Period"] = filtered_periods
        response = xmltodict.unparse(response, pretty=True)
    
        output_path = join(TEMP_MPD_FOLDER, f"{video_title}.mpd")
        with open(output_path, "w") as f:
            f.write(response)
        return (
            f'N_m3u8DL-RE "{output_path}" {keys} --save-dir "fifa_com" --save-name "{video_title}" -M format=mkv'
            f' & '
            f'del "{output_path}"'
        )
    
    
    def get_command(url):
        video_id = re.search(r"/watch/([^/?]*)", url).group(1)
        response = requests.get(
            VIDEO_PLAYER_URL.format(video_id=video_id),
            params={'locale': 'en'}
        )
    
        status_code = response.status_code
        if status_code == 404:
            raise Exception("Content not available")
    
        response = json.loads(response.content.decode())
        video_title = slugify(response.get('title', video_id))
    
        response = response["preplayParameters"]
        preplay_url = PREPLAY_URL.format(content_id=response["contentId"])
        if response.get("queryStr", None) is not None:
            preplay_url += "?" + response["queryStr"]
        if response.get("signature", None) is not None:
            if "?" not in preplay_url:
                preplay_url += "?"
            else:
                preplay_url += "&"
            preplay_url += "sig=" + quote(response["signature"])
    
        response = json.loads(requests.get(preplay_url).content.decode())["playURL"]
    
        if USE_MPD:
            return get_mpd_command(response, video_title)
        return f'N_m3u8DL-RE "{response}" --save-dir "fifa_com" --save-name "{video_title}"'
    
    
    if __name__ == '__main__':
        if not os.path.exists(TEMP_MPD_FOLDER):
            os.makedirs(TEMP_MPD_FOLDER)
    
        with open(INPUT_FILE, 'r') as file:
            urls = file.readlines()
        urls = [u.strip() for u in urls]
        urls = list(dict.fromkeys([u for u in urls if len(u) > 0 and "plus.fifa.com" not in u]))
    
        index = 0
        nr = len(urls)
        for u in urls:
            index += 1
            print(f'[{index}/{nr}]')
    
            try:
                print(get_command(u))
            except Exception as e:
                print(f"Failed to get: {u}. Reason: {str(e)}. Try again later...")
    Does this code still work, knowing that the links have been changed to (plus.fifa.com) instead of (fifa.com)

    Image
    [Attachment 86591 - Click to enlarge]
    Quote Quote  



Similar Threads

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