DevLARLEY / amzn-480p-downloader

Downloads any Video from Prime Video in 480p quality. (Only requires an L3 CDM)
GNU General Public License v2.0
12 stars 7 forks source link

Primevideo.com #3

Open RacketWise opened 2 months ago

RacketWise commented 2 months ago

Hi there, can the script be adapted to make it work for primevideo.com?

DevLARLEY commented 2 months ago

Not yet because my vpn is being detected

RacketWise commented 2 months ago

I just made some modifications to the code.


# -*- coding: utf-8 -*-
import urllib.parse

import headers
import os, sys, json
import base64, requests, pyfiglet, xmltodict
from pywidevine.L3.cdm import deviceconfig
from pywidevine.L3.decrypt.wvdecryptcustom import WvDecrypt
import browser_cookie3

def post():
    title = pyfiglet.figlet_format('AMZN 480p Key Extractor', font='speed', width=200)
    print(f'{title}')

def extract_pssh(xml):
    pssh = []
    try:
        mpd = json.loads(json.dumps(xml))
        periods = mpd['MPD']['Period']
    except Exception:
        return
    if isinstance(periods, list):
        maxHeight = 0
        for p in periods:
            for ad_set in p['AdaptationSet']:
                if 'ContentProtection' not in ad_set:
                    continue
                if '@maxHeight' not in ad_set:
                    continue
                if int(ad_set["@maxHeight"]) <= maxHeight:
                    continue
                maxHeight = int(ad_set["@maxHeight"])
                for cont in ad_set['ContentProtection']:
                    if '@schemeIdUri' not in cont:
                        continue
                    if cont['@schemeIdUri'].lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
                        pssh.append(cont['cenc:pssh'])
    else:
        for ad_set in periods['AdaptationSet']:
            if 'ContentProtection' not in ad_set:
                continue
            for cont in ad_set['ContentProtection']:
                if '@schemeIdUri' not in cont:
                    continue
                if cont['@schemeIdUri'].lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
                    pssh.append(cont['cenc:pssh'])
    return pssh[0] if pssh else None

def get_asin(url):
    a = url.split("/")
    if "dp" in a:
        return a[a.index("dp")+1]
    if "gp" in a:
        return a[a.index("gp")+1]
    return

def get_playback_resources(asin):
    resource_url = (f"https://atv-ps-eu.primevideo.com/cdp/catalog/GetPlaybackResources" +
                    "?deviceID=" +
                    "&deviceTypeID=AOAGZA014O5RE" +
                    "&firmware=1" +
                    f"&asin={asin}" +
                    "&consumptionType=Streaming" +
                    "&desiredResources=PlaybackUrls%2CCatalogMetadata" +
                    "&resourceUsage=CacheResources" +
                    "&videoMaterialType=Feature" +
                    "&userWatchSessionId=x" +
                    "&deviceBitrateAdaptationsOverride=CVBR" +
                    "&deviceDrmOverride=CENC" +
                    "&supportedDRMKeyScheme=DUAL_KEY" +
                    "&titleDecorationScheme=primary-content")
    return json.loads(requests.post(url=resource_url, cookies=headers.cookies).text)

def get_keys(pssh, lic_url):
    wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64=None, device=deviceconfig.device_android_generic)
    challenge = wvdecrypt.get_challenge()

    json_payload = {
        'widevine2Challenge': base64.b64encode(challenge).decode('utf-8'),
        'includeHdcpTestKeyInLicense': 'true'
    }

    license = requests.post(url=lic_url, data=json_payload, cookies=headers.cookies)

    response_json = json.loads(license.content)
    try:
        lic = response_json['widevine2License']['license']
    except Exception:
        print("Unable to obtain license from server response:")
        print(response_json)
        sys.exit()
    wvdecrypt.update_license(lic)

    return wvdecrypt.start_process()

if __name__ == '__main__':
    post()

    tld = "com"

    inp = input("ASIN / Prime Video Link: ")
    if inp.startswith("http"):
        print("The link will point towards the first episode if it's a season. "
              "Obtain the asin for each episode from the network tab.\n")
        a = urllib.parse.urlparse(inp).netloc.split(".")
        a.pop(0)
        a.pop(0)
        tld = '.'.join(a)
        asin = get_asin(inp)
    else:
        asin = inp

    auto_cookie = input("Get cookies from browser? (y/N): ").lower().startswith("y")

    if tld == "de":
        cookie_names = ["ubid-acbde", "x-acbde", "at-acbde"]
    elif tld == "co.uk":
        cookie_names = ["ubid-acbuk", "x-acbuk", "at-acbuk"]
    else:
        cookie_names = ["ubid-main-av", "x-main-av", "at-main-av"]

    if auto_cookie:
        try:
            ch = browser_cookie3.chrome()
        except Exception:
            print("\033[93mSkipping chrome for cookie retrieval since it is not closed.\033[0m")
            ch = None
        ff = browser_cookie3.firefox()

        for browser in [ch, ff]:
            if browser is None:
                continue
            values = (item in [cookie.name for cookie in browser if
                               (cookie.domain == f".primevideo.com" and not cookie.is_expired())] for item in
                      cookie_names)
            if all(values):
                cookies = {
                    cookie_names[0]: [cookie.value for cookie in browser if cookie.name == cookie_names[0]][0],
                    cookie_names[1]: [cookie.value for cookie in browser if cookie.name == cookie_names[1]][0],
                    cookie_names[2]: [cookie.value for cookie in browser if cookie.name == cookie_names[2]][0]
                }
                headers.cookies = cookies
                print("\033[92mSuccessfully retrieved cookies\033[0m")
    else:
        if not 'cookies' in headers.__dict__:
            print(f"\033[91mNo cookies found in headers.py\033[0m")
            sys.exit()
        if not all(item in headers.cookies for item in cookie_names):
            print(f"\033[91mMissing cookie names in headers.py\033[0m")
            sys.exit()
        if not all(len(headers.cookies[item]) > 0 for item in cookie_names):
            print(f"\033[91mMissing cookie values in headers.py\033[0m")
            sys.exit()

    j = get_playback_resources(asin)

    if 'error' in j:
        print("\033[91mUnable to get playback resources: \033[0m", end="")
        print(f"\033[91m{j['error']['errorCode']}\033[0m")
        if inp.startswith("http"):
            print(
                f"\033[91mCheck that the TLD country ({tld}) matches the TLD of the website you got the ASIN "
                f"from.\033[0m")
        if not auto_cookie:
            print("\033[91mCheck if your cookies match the website you got the ASIN from.\033[0m")
        sys.exit()

    catalog = j["catalogMetadata"]["catalog"]
    print("\n\033[93m" + catalog['title'] + "\033[0m")
    print("\033[90m" + catalog['synopsis'] + "\033[0m")

    if not input("Get keys? (y/N): ").lower().startswith("y"):
        sys.exit()

    try:
        urls = j["playbackUrls"]["urlSets"]
    except KeyError:
        print("No manifest urls found: ", end="")
        if 'rightsException' in j["returnedTitleRendition"]["selectedEntitlement"]:
            print(j["returnedTitleRendition"]["selectedEntitlement"]["rightsException"]["errorCode"])
        sys.exit()

    pssh = None
    mpd_url = None
    for u in urls:
        m = urls[u]["urls"]["manifest"]["url"]
        mpd_url = m
        mpd = requests.get(url=m)
        if mpd.status_code == 200:
            xml = xmltodict.parse(mpd.text)
            pssh = extract_pssh(xml)
            if pssh is not None:
                break

    if pssh is None:
        print("No PSSH found.")
        sys.exit()

    print(f"\n{mpd_url}\n")

    lic_url = (
            f"https://atv-ps-eu.primevideo.com/cdp/catalog/GetPlaybackResources?deviceID=" +
            "&deviceTypeID=AOAGZA014O5RE" +
            "&firmware=1" +
            f"&asin={asin}" +
            "&consumptionType=Streaming" +
            "&desiredResources=Widevine2License" +
            "&resourceUsage=ImmediateConsumption" +
            "&videoMaterialType=Feature" +
            "&userWatchSessionId=x")

    _, keys = get_keys(pssh, lic_url)

    for key in keys:
        print(f"--key {key} ")

    if keys and input("\nDownload and decrypt? (y/N): ").lower().startswith("y"):
        try:
            os.system(f"N_m3u8DL-RE {mpd_url} {'--key ' + ' --key '.join(keys)} -sv best -sa best -M format=mkv")
        except Exception as ex:
            print(f"Error while starting N_m3u8DL: {ex}")
RacketWise commented 2 months ago

I just modified the resource_url, the cookie_names (adding -av at "ubid-main-av", "x-main-av", "at-main-av" for the .com tld, which is primevideo.com in reality). I've also modified the lic_url: "https://atv-ps-eu.primevideo.com...". I've run the script, added the asin, cdm is in order and working (tested in wks-keys). For the ASIN amzn1.dv.gti.d1f8ecd4-fcc3-4d1b-a929-90dcb8f5bd47 I get the following error as in the following image.

Cattura

The headers are as follows:

amazon uk cookies = { 'ubid-acbuk': '', 'x-acbuk': '', 'at-acbuk': '' }

amazon de cookies = { 'ubid-acbde': '', 'x-acbde': '', 'at-acbde': '' }

primevideo.com cookies = { 'ubid-main-av': '262-3083430-2068635', 'x-main-av': '8w31Nqtyuq1q3ckdJJBdGIOxR5AqGgXw', 'at-main-av': 'Atza|IwEBIM4fiYForBBRBZKpcZpGv0bH7Xg82crV7CHGwQIhTVKUnLbEly8zzVPJpB7nT-6YD7pE2OtejpC7OKksWTrsMFzlQ6cIGB660-9Po-IksQXK7b9FSQYoLCSbDtmIICygLPJzgQEyLaMDTuR2WpPM0imfjnvUnZ2Lr1mdxUBC80WT4-TMaBoVOEoOYPsXAm5rzREp4sGX133YAWuIXkdsWmT8jdhUCqGIY8CPrsv01-rxiO1xSO0MLxixJrXoyVSGPRM' }

What I don't understand is: with an L3 cdm one can download a 1080p video, but the script is parsing an mpd which limits the resolution to 480p max. Why?

DevLARLEY commented 2 months ago

Are you logged into primevideo on firefox?

RacketWise commented 2 months ago

Are you logged into primevideo on firefox?

yes

DevLARLEY commented 2 months ago

Regarding your post edit: Theoretically, any resolution can be downloaded with an L3 CDM, it's up to the streaming provider to decide what level of quality they want to offer for each 'security level'. Amazon chose to only make 480p available to a normal L3 CDM. Higher resolutions require either VMP validation or an L1 CDM. Google actually has recommendations on exactly this kind of quality/security balancing.

RacketWise commented 2 months ago

Regarding your post edit: Theoretically, any resolution can be downloaded with an L3 CDM, it's up to the streaming provider to decide what level of quality they want to offer for each 'security level'. Amazon chose to only make 480p available to a normal L3 CDM. Higher resolutions require either VMP validation or an L1 CDM. Google actually has recommendations on exactly this kind of quality/security balancing.

Ok, but why the script is returning me a GeoIP error?

DevLARLEY commented 2 months ago

GeoIP generally indicates geoblocking and, if you're using a VPN, that's probably going to be the cause of this error. But i think i've had this issue in some other case before, i think it was related to incorrect cookies but i'm not sure.

RacketWise commented 2 months ago

I'm not using a VPN. I managed also to get the script to work by changing the cdm but now I've got another problem. The parsed mpd is pulling only GB audio and not IT. In Firefox and on primevideo.com I'm on IT language when I get all the cookies and asin code... Same story also under Chrome.

RacketWise commented 2 months ago

Is there a way to pull the IT language also in the mpd?

DevLARLEY commented 2 months ago

I'm on vacation. I'll get back to you.

DevLARLEY commented 2 months ago

Could you post the MPD file/URL?

RacketWise commented 2 months ago

https://www.primevideo.com/detail/0STMV84T07DTIZVG293V3S2YKM/ref=atv_sr_fle_c_Tn74RA_1_1_1?sr=1-1&pageTypeIdSource=ASIN&pageTypeId=B0B8HY75LN&qid=1715371272740 The rig Italian Prime Video

DevLARLEY commented 2 months ago

I need the MPD URL, as i don't have primevideo right now. The script should output it on runtime.

antoniantonio791 commented 1 month ago

Cattura

Hi,this is MPD: https://ABE6DGWAAAAAAAAMCBPEUEIFTLSPC.eu-reg.dash.pv-cdn.net/dm/2$6kDUoc9gA4lmPfW63KE4knM8-R0~/e518/1582/e159/4b81-9bc9-e1ee4599fa1/425eb1c1-617a-45f4-9175-1a25df4e1be3_corrected.mpd

This is ASIN: amzn1.dv.gti.d1f8ecd4-fcc3-4d1b-a929-90dcb8f5bd47

Hope you could out the script. Regards.

RacketWise commented 1 month ago

Cattura

Hi,this is MPD: https://ABE6DGWAAAAAAAAMCBPEUEIFTLSPC.eu-reg.dash.pv-cdn.net/dm/2$6kDUoc9gA4lmPfW63KE4knM8-R0~/e518/1582/e159/4b81-9bc9-e1ee4599fa1/425eb1c1-617a-45f4-9175-1a25df4e1be3_corrected.mpd

This is ASIN: amzn1.dv.gti.d1f8ecd4-fcc3-4d1b-a929-90dcb8f5bd47

Hope you could out the script. Regards.

How did you extract the mpd? This one has IT audio... How did you do that?

antoniantonio791 commented 1 month ago

I'm a subscriber of italian prime,so the italian audio. By the way RacketWise too have italian prime,so i joined to the thread.

RacketWise commented 1 month ago

I'm a subcriper of italian prime,so the italian audio.

Anch'io sono abbonato a Prime. Tu sei riuscito a scaricare il file tramite lo script con l'audio italiano? A me da sempre e solo quello inglese e poi non arriva a individuare le chiavi restituendo l'errore PRS No Rights Invalid GeoIP. Non uso VPN.

antoniantonio791 commented 1 month ago

I'm a subcriper of italian prime,so the italian audio.

Anch'io sono abbonato a Prime. Tu sei riuscito a scaricare il file tramite lo script con l'audio italiano? A me da sempre e solo quello inglese e poi non arriva a individuare le chiavi restituendo l'errore GeoIP restricted. Non uso VPN.

I was unable to download with the script,italian even english audio. Same GeoIP error too me.

RacketWise commented 1 month ago

I'm a subcriper of italian prime,so the italian audio.

Anch'io sono abbonato a Prime. Tu sei riuscito a scaricare il file tramite lo script con l'audio italiano? A me da sempre e solo quello inglese e poi non arriva a individuare le chiavi restituendo l'errore GeoIP restricted. Non uso VPN.

I was unable to download with the script,italian even english audio.

So it doesn't work for you too... Let's hope for a fix.

antoniantonio791 commented 1 month ago

I'm a subcriper of italian prime,so the italian audio.

Anch'io sono abbonato a Prime. Tu sei riuscito a scaricare il file tramite lo script con l'audio italiano? A me da sempre e solo quello inglese e poi non arriva a individuare le chiavi restituendo l'errore GeoIP restricted. Non uso VPN.

I was unable to download with the script,italian even english audio.

So it doesn't work for you too... Let's hope for a fix.

The issue is concerned obtain license,hope for fix me too.

DevLARLEY commented 1 month ago

I can't fix it since i can't reproduce the error because i can't use my VPN to acces the site

RacketWise commented 1 month ago

I can't fix it since i can't reproduce the error because i can't use my VPN to acces the site

Thanks anyway