from __future__ import annotations import argparse, os, base64, gzip, json, logging, random, re, sys, time, uuid, zlib, requests, urllib3 from datetime import datetime, timezone from http.cookies import SimpleCookie from io import BytesIO from pathlib import Path from typing import Any, Dict, List, Optional from urllib.parse import quote, urlencode from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from pywidevine import Cdm as WidevineCdm, Device as WidevineDevice from modules import MSL_ANDROID, MSL_IOS, MSL_TV, MSL_WEB, MSL_MGK from modules.msl_mgk import UserAuthentication from modules.config import setup_config logging.basicConfig(level=logging.INFO, format="%(name)s - %(levelname)s - %(message)s") log = logging.getLogger("MSL HANDSHAKE") config = setup_config() EMAIL = config["NETFLIX"]["EMAIL"] PASSWORD = config["NETFLIX"]["PASSWORD"] # ====================================================================== # ANDROID # ====================================================================== def run_android(wvd_path: Path, new_msl: bool = False, no_verify: bool = False): log = logging.getLogger('ANDROID MSL') BASE_DIR = Path(__file__).resolve().parent OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache_android.json" AUTH_COOKIES_PATH = OUTPUT_DIR / "netflix_auth_cookies.json" USERIDTOKEN_PATH = OUTPUT_DIR / "netflix_auth_useridtoken.json" TOKENS_OUTPUT_PATH = OUTPUT_DIR / "netflix_auth_tokens.json" NETFLIX_HOME_URL = "https://www.netflix.com/" NETFLIX_CANONICAL_URL = "https://netflix.com/" LOGIN_URL = "https://www.netflix.com/login" APPBOOT_URL = "https://android15.appboot.netflix.com/appboot/NFANDROID1-PRV-P-" MSL_HANDSHAKE_ENDPOINT = "https://android.prod.ftl.netflix.com/nq/androidui/pbo_license/~1.0.0/router" VERIFY_LOGIN_URL = "https://android.prod.ftl.netflix.com/nq/androidui/samurai/v1/config" USER_AGENT = "com.netflix.mediaclient/63988 (Linux; U; Android 15; en_US; SM-F711N; Build/AP3A.240905.015.A2; Cronet/143.0.7445.0)" CLIENT_VERSION = "18.26.0" APP_VERSION = "9.60.0" HAWKINS_VERSION = "5.15.0" UI_FLAVOR = "android" OS_VERSION = "35" FORM_FACTOR = "phone" FEATURE_CAPABILITIES = "supportsStudioBranding" LOCALE = "en-US" DEVICE_MODEL = "SM-F711N" VERIFY_TLS = True RESTORE_AUTH_COOKIES = False ESN = f"NFANDROID1-PRV-P-SAMSUSM-F711N-22594-{''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(64))}" REQUEST_CLIENT_CONTEXT_UNKNOWN = '{"appView":"unknown","appState":"foreground"}' APPBOOT_REQUEST_CLIENT_CONTEXT = '{"appView":"unknown","appState":"foreground"}' session = requests.Session() session.verify = VERIFY_TLS session.headers.update( { "User-Agent": USER_AGENT, "Accept": "*/*", } ) if RESTORE_AUTH_COOKIES and AUTH_COOKIES_PATH.exists(): try: cached_auth_cookies = json.loads(AUTH_COOKIES_PATH.read_text(encoding="utf-8")) if isinstance(cached_auth_cookies, dict): session.cookies.update(cached_auth_cookies) except Exception as exc: log.warning("Saved authentication cookies could not be restored: %s", exc) log.info("Initializing session") response = session.get(NETFLIX_CANONICAL_URL, timeout=30, allow_redirects=True) response.raise_for_status() response = session.get(NETFLIX_HOME_URL, timeout=30) response.raise_for_status() log.info("Requesting initial nfvdid cookie") appboot_headers = { "Host": "android15.appboot.netflix.com", "Connection": "keep-alive", "X-Netflix.Request.Client.Context": APPBOOT_REQUEST_CLIENT_CONTEXT, "Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate, br", } response = session.post( APPBOOT_URL, params={"keyVersion": "1"}, headers=appboot_headers, timeout=30, ) nfvdid = "" for cookie in session.cookies: if cookie.name == "nfvdid": nfvdid = cookie.value break if not nfvdid: nfvdid = response.cookies.get("nfvdid", "") if not nfvdid: raise RuntimeError("The initial nfvdid cookie was not returned") log.info("Initial nfvdid cookie obtained") log.info("Starting MSL Widevine exchange") if not wvd_path.exists(): raise FileNotFoundError(f"Missing WVD file: {wvd_path}") widevine_device = WidevineDevice.load(wvd_path) cdm = WidevineCdm.from_device(widevine_device) msl_headers = MSL_ANDROID.build_request_headers( request_name="getProxyEsn", user_agent=USER_AGENT, referer=None, esn=ESN, expiry_timeout=12750, host="android15.prod.cloud.netflix.com", language="en-US,en", device_model=quote(DEVICE_MODEL, safe=""), extra_headers={ "Accept-Encoding": "gzip, deflate, br", "Content-Encoding": "msl_v1", "x-netflix.zuul.brotli.allowed": "true", "x-netflix.appver": APP_VERSION, "x-netflix.clienttype": "samurai", "x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT_UNKNOWN, "x-netflix.esnprefix": "NFANDROID1-PRV-P-", "x-netflix.request.uuid": ( "".join(random.choice("0123456789abcdef") for _ in range(8)) + "-" + "".join(random.choice("0123456789abcdef") for _ in range(4)) + "-" + "".join(random.choice("0123456789abcdef") for _ in range(4)) + "-" + "".join(random.choice("0123456789abcdef") for _ in range(4)) + "-" + "".join(random.choice("0123456789abcdef") for _ in range(12)) ), "x-netflix.androidapi": "35", "x-netflix.deviceformfactor": "PHONE", "x-netflix.devicememorylevel": "HIGH", "x-netflix.request.attempt": "1", "x-netflix.request.id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "Content-Type": "application/json", "x-netflix.client.request.name": "getProxyEsn", "x-netflix.request.routing": '{"path":"\\/nq\\/android\\/playback\\/~1.0.0\\/router"}', "user-agent": USER_AGENT, }, ) handshake_cookies = { "nfvdid": nfvdid, } msl_keys = MSL_ANDROID.handshake( msl_keys_path=str(MSL_CACHE_PATH), session=session, sender=ESN, cdm=cdm, cdm_device=str(wvd_path), new_msl=False, cookies=handshake_cookies, drm="widevine", endpoint=MSL_HANDSHAKE_ENDPOINT, headers=msl_headers, ) msl_client = MSL_ANDROID( session=session, keys=msl_keys, message_id=random.randint(0, 2**52), sender=ESN, drm="widevine", ) flow_session_id = "" for cookie in session.cookies: if cookie.name == "nfvdid": nfvdid = cookie.value elif cookie.name == "flwssn": flow_session_id = cookie.value log.info("MSL Widevine exchange completed") log.info("Loading login page to collect session cookies") response = session.get(LOGIN_URL, timeout=30) response.raise_for_status() cookie_dict = session.cookies.get_dict() flow_session_id = cookie_dict.get("flwssn", flow_session_id) if not flow_session_id: raise RuntimeError("The flwssn flow session cookie is missing before VerifyLoginMslRequest") if "NetflixId" not in cookie_dict or "SecureNetflixId" not in cookie_dict: log.warning("NetflixId or SecureNetflixId cookie is missing before VerifyLoginMslRequest") log.info("Submitting VerifyLoginMslRequest") confirm_login_query = { "api": "33", "appType": "samurai", "appVer": "62902", "appVersion": "9.18.0", "chipset": "sm8150", "chipsetHardware": "qcom", "clientAppState": "FOREGROUND", "clientAppVersionState": "NORMAL", "countryIsoCode": "US", "ctgr": "phone", "dbg": "false", "deviceLocale": "en-US", "devmod": f"samsung_{DEVICE_MODEL}", "ffbc": "phone", "flwssn": flow_session_id, "installType": "regular", "isAutomation": "false", "isConsumptionOnly": "true", "isNetflixPreloaded": "false", "isPlayBillingEnabled": "true", "isStubInSystemPartition": "false", "lackLocale": "false", "landingOrigin": "https://www.netflix.com", "mId": "SAMSUSM-F711N", "memLevel": "HIGH", "method": "get", "mnf": "samsung", "model": DEVICE_MODEL, "netflixClientPlatform": "androidNative", "netflixId": cookie_dict.get("NetflixId", ""), "networkType": "wifi", "osBoard": "kona", "osDevice": "bloom", "osDisplay": "RP1A.200720.012", "password": PASSWORD, "path": '["signInVerify"]', "pathFormat": "hierarchical", "platform": "android", "preloadSignupRoValue": "", "progressive": "false", "qlty": "hd", "recaptchaResponseTime": 445, "recaptchaResponseToken": "", "responseFormat": "json", "roBspVer": "RP1A.200720.012", "secureNetflixId": cookie_dict.get("SecureNetflixId", ""), "sid": "7176", "store": "google", "userLoginId": EMAIL, } confirm_login_headers = { "X-Netflix.Request.NqTracking": "VerifyLoginMslRequest", "X-Netflix.Client.Request.Name": "VerifyLoginMslRequest", "X-Netflix.Request.Client.Context": '{"appState":"foreground"}', "X-Netflix-Esn": ESN, "X-Netflix.EsnPrefix": "NFANDROID1-PRV-P-", "X-Netflix.msl-header-friendly-client": "true", "content-encoding": "msl_v1", } try: confirm_login_header, confirm_login_payload_chunks = msl_client.send_message(endpoint=VERIFY_LOGIN_URL, params=confirm_login_query, application_data={}, headers=confirm_login_headers) except Exception: log.error("VerifyLoginMslRequest failed") log.debug("Request URL: %s", VERIFY_LOGIN_URL) log.debug("Request params: %s", json.dumps(confirm_login_query, indent=2)) log.debug("Request headers: %s", json.dumps(confirm_login_headers, indent=2)) log.debug("Session cookies: %s", json.dumps(session.cookies.get_dict(), indent=2)) log.exception("Exception occurred") sys.exit(1) if ( isinstance(confirm_login_payload_chunks, dict) and "errorCode" in confirm_login_payload_chunks.get("jsonGraph", {}).get("signInVerify", {}).get("value", {}).get("fields", {}) ): error_code = ( confirm_login_payload_chunks.get("jsonGraph", {}) .get("signInVerify", {}) .get("value", {}) .get("fields", {}) .get("errorCode", {}) .get("value") ) log.error("Login errorCode: %s", error_code) sys.exit(1) if "headerdata" not in confirm_login_header: log.critical("Missing 'headerdata' in MSL response") sys.exit(1) try: encrypted_header_b64 = confirm_login_header["headerdata"] encryption_key_value = msl_client.keys.encryption sign_key_value = msl_client.keys.sign if isinstance(encryption_key_value, str): try: encryption_key = bytes.fromhex(encryption_key_value) except ValueError: encryption_key = encryption_key_value.encode("utf-8") else: encryption_key = encryption_key_value if isinstance(sign_key_value, str): try: sign_key = bytes.fromhex(sign_key_value) except ValueError: sign_key = sign_key_value.encode("utf-8") else: sign_key = sign_key_value if not encryption_key: raise RuntimeError("The encryption key is missing") if not sign_key: raise RuntimeError("The sign key is missing") encrypted_header = json.loads(base64.b64decode(encrypted_header_b64)) iv = base64.b64decode(encrypted_header["iv"]) ciphertext = base64.b64decode(encrypted_header["ciphertext"]) cipher = AES.new(encryption_key, AES.MODE_CBC, iv) decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size) header_data = json.loads(decrypted.decode("utf-8")) except Exception: log.exception("Failed to decrypt MSL header") sys.exit(1) tokens = header_data.get("useridtoken") if not tokens: log.error("Authentication failed: invalid ESN, email, or password") sys.exit(1) try: TOKENS_OUTPUT_PATH.write_text(json.dumps(header_data, indent=4), encoding="utf-8") USERIDTOKEN_PATH.write_text(json.dumps(tokens, indent=2), encoding="utf-8") log.info("User ID token data saved to: %s", TOKENS_OUTPUT_PATH) log.info("User ID token saved to: %s", USERIDTOKEN_PATH) except Exception: log.exception("Failed to save token files") sys.exit(1) auth_cookies = {} for cookie in session.cookies: auth_cookies[cookie.name] = cookie.value try: AUTH_COOKIES_PATH.write_text(json.dumps(auth_cookies, indent=2), encoding="utf-8") log.info("Authentication cookies saved to: %s", AUTH_COOKIES_PATH) except Exception: log.exception("Failed to save cookies") sys.exit(1) result = { "useridtoken": tokens, "auth_cookies": auth_cookies, "header_data": header_data, } log.info("VerifyLoginMslRequest succeeded") print(json.dumps(result, indent=2)) # ====================================================================== # iOS # ====================================================================== def run_ios(wvd_path: Path, new_msl: bool = False, no_verify: bool = False): from Crypto.Cipher import AES from Crypto.Util.Padding import unpad log = logging.getLogger('netflix_ios_login') from typing import Any, Dict BASE_DIR = Path(__file__).resolve().parent OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache_ios.json" AUTH_COOKIES_PATH = OUTPUT_DIR / "netflix_auth_cookies.json" NETFLIX_HOME_URL = "https://www.netflix.com/" NETFLIX_CANONICAL_URL = "https://netflix.com/" GRAPHQL_URL = "https://ios.prod.cloud.netflix.com/graphql" LOGIN_URL = "https://www.netflix.com/login" BROWSE_URL = "https://www.netflix.com/browse" APPBOOT_URL = "https://ios18.appboot.netflix.com/appboot/NFANDROID1-PRV-P-" MSL_HANDSHAKE_ENDPOINT = "https://ios.prod.ftl.netflix.com/nq/iosplatform/pbo_license/~1.0.0/router" USER_AGENT = "Netflix/5850 CFNetwork/3826.600.41 Darwin/24.6.0" CLIENT_VERSION = "18.26.0" APP_VERSION = "18.26.0" HAWKINS_VERSION = "5.16.0" UI_FLAVOR = "argo" OS_VERSION = "18.6.2" FORM_FACTOR = "phone" FEATURE_CAPABILITIES = "supportsStudioBranding" LOCALE = "en-US" DEVICE_MODEL = "iPhone15,3" ESN = f"NFANDROID1-PRV-P-IPHONE15=3-22594-{''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(64))}" REQUEST_CLIENT_CONTEXT_LANDING = '{"appView":"nmLanding","appState":"foreground"}' REQUEST_CLIENT_CONTEXT_IDENTIFIER = '{"appView":"login","appState":"foreground"}' REQUEST_CLIENT_CONTEXT_PASSWORD = '{"appView":"passwordLogin","appState":"foreground"}' REQUEST_CLIENT_CONTEXT_PROFILES = '{"appView":"profilesGate","appState":"foreground"}' APPBOOT_CLIENT_CONTEXT = '{"appState":"foreground","reason":"user-action"}' APPBOOT_REQUEST_CLIENT_CONTEXT = '{"appView":"unknown","appState":"foreground"}' RECAPTCHA_SITE_KEY = "6Lf8hrcUAAAAAIpQAFW2VFjtiYnThOjZOA5xvLyR" QUERY_IDS = { "MembershipStatus": {"id": "3f50f3b3-fff8-48c0-bbd3-5fa2cb04b3c1", "version": 102}, "CLCSScreenUpdate": {"id": "1c276cdf-caef-49cf-b38e-384972c2b47e", "version": 102}, "CLCSSendFeedback": {"id": "079b2271-196b-4edd-b65c-e9439b22e305", "version": 102}, "CLCSInterstitialProfileGate": {"id": "b6e10c7d-0e6f-4921-83b5-177995a80d97", "version": 102}, } recaptcha_token = "" verify_tls = True restore_auth_cookies = False session = requests.Session() session.verify = verify_tls session.headers.update({ "User-Agent": USER_AGENT, "Accept": "*/*", }) if restore_auth_cookies and AUTH_COOKIES_PATH.exists(): try: cached_auth_cookies = json.loads(AUTH_COOKIES_PATH.read_text(encoding="utf-8")) if isinstance(cached_auth_cookies, dict): session.cookies.update(cached_auth_cookies) except Exception as exc: log.warning("Saved auth cookies could not be restored: %s", exc) log.info("Initializing session") response = session.get(NETFLIX_CANONICAL_URL, timeout=30, allow_redirects=True) response.raise_for_status() response = session.get(NETFLIX_HOME_URL, timeout=30) response.raise_for_status() log.info("Requesting initial nfvdid cookie") appboot_request_id = "".join(random.choice("0123456789abcdef") for _ in range(32)) appboot_headers = { "Host": "ios18.appboot.netflix.com", "X-Netflix.Client.appVersion": APP_VERSION, "Accept": "*/*", "X-Netflix.Request.Id": appboot_request_id, "X-Netflix.APIAction": "appboot", "X-Netflix.Client.Context": APPBOOT_CLIENT_CONTEXT, "X-Netflix.Client.Request.Name": "appboot", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Content-Type": "application/x-www-form-urlencoded", "X-Netflix.Request.Attempt": "1", "X-Netflix.Request.Client.Context": APPBOOT_REQUEST_CLIENT_CONTEXT, "User-Agent": USER_AGENT, } response = session.post( APPBOOT_URL, params={"keyVersion": "1"}, headers=appboot_headers, timeout=30, ) nfvdid = "" for cookie in session.cookies: if cookie.name == "nfvdid": nfvdid = cookie.value break if not nfvdid: nfvdid = response.cookies.get("nfvdid", "") if not nfvdid: raise RuntimeError("The initial nfvdid cookie was not returned") log.info("Initial nfvdid cookie obtained") log.info("Starting MSL Widevine exchange") if not wvd_path.exists(): raise FileNotFoundError(f"Missing WVD file: {wvd_path}") device = WidevineDevice.load(wvd_path) cdm = WidevineCdm.from_device(device) msl_headers = MSL_IOS.build_request_headers( request_name="mintCookies", user_agent=USER_AGENT, referer=None, esn=ESN, expiry_timeout=12750, host="ios.prod.ftl.netflix.com", language="en-US,en", device_model=quote(DEVICE_MODEL, safe=""), extra_headers={ "Accept-Encoding": "deflate,gzip", "Accept-Language": "en-US,en;q=0.9", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, "X-Netflix.Client.Request.Name": "mintCookies", "X-Netflix.Request.Client.Context": '{"appView":"login","appState":"foreground"}', }, ) handshake_cookies = { "nfvdid": nfvdid, } msl_keys = MSL_IOS.handshake( msl_keys_path=str(MSL_CACHE_PATH), session=session, sender=ESN, cdm=cdm, cdm_device=str(wvd_path), new_msl=False, cookies=handshake_cookies, drm="widevine", endpoint=MSL_HANDSHAKE_ENDPOINT, headers=msl_headers, ) msl_client = MSL_IOS( session=session, keys=msl_keys, message_id=random.randint(0, pow(2, 52)), sender=ESN, drm="widevine", ) flow_session_id = "" for cookie in session.cookies: if cookie.name == "nfvdid": nfvdid = cookie.value elif cookie.name == "flwssn": flow_session_id = cookie.value log.info("MSL Widevine exchange completed") log.info("Submitting membership request") operation_name = "MembershipStatus" variables = {} headers = { "Host": "ios.prod.cloud.netflix.com", "Connection": "keep-alive", "X-Netflix.Request.Client.Context": REQUEST_CLIENT_CONTEXT_LANDING, "Content-Encoding": "msl_v1", "x-netflix.context.feature-capabilities": FEATURE_CAPABILITIES, "x-netflix.context.operation-name": operation_name, "X-Netflix.request.expiry.timeout": "15000", "X-Netflix.Request.Id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.form-factor": FORM_FACTOR, "X-Netflix.Request.Attempt": "1", "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept-Language": "en-US,en;q=0.9", "Accept": "*/*", "Content-Type": "application/json", "x-netflix.context.locales": LOCALE, "x-netflix.context.os-version": OS_VERSION, "Accept-Encoding": "gzip, deflate, br", "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.ui-flavor": UI_FLAVOR, } body = { "operationName": operation_name, "variables": variables, "extensions": {"persistedQuery": QUERY_IDS[operation_name]}, } membership_header, membership_status_response = msl_client.send_message( endpoint=GRAPHQL_URL, params={}, application_data=body, headers=headers, ) if isinstance(membership_status_response, dict) and "errors" in membership_status_response: raise RuntimeError(json.dumps(membership_status_response["errors"], indent=2)) log.info("Loading login page") response = session.get(LOGIN_URL, timeout=30) response.raise_for_status() login_html = response.text clcs_session_id = None clcs_patterns = [ r'"clcsSessionId"\s*:\s*"([0-9a-f\-]{36})"', r'\\"clcsSessionId\\"\s*:\s*\\"([0-9a-f\-]{36})\\"', r'"serverState"\s*:\s*"[^\"]*clcsSessionId\\":\\"([0-9a-f\-]{36})', r'"trackingInfo"\s*:\s*"[^\"]*clcsSessionId\\":\\"([0-9a-f\-]{36})', r'"sessionId"\s*:\s*"([0-9a-f\-]{36})"', ] for pattern in clcs_patterns: match = re.search(pattern, login_html) if match: clcs_session_id = match.group(1) break if not clcs_session_id: raise RuntimeError("Could not extract clcsSessionId from the login page HTML") rendition_id = None rendition_patterns = [ r'"renditionId"\s*:\s*"([0-9a-f\-]{36})"', r'\\"renditionId\\"\s*:\s*\\"([0-9a-f\-]{36})\\"', ] for pattern in rendition_patterns: match = re.search(pattern, login_html) if match: rendition_id = match.group(1) break if not rendition_id: raise RuntimeError("Could not extract the initial renditionId from the login page HTML") log.info("Submitting password screen update") session_context: Dict[str, Any] = { "session-breadcrumbs": {"funnel_name": "loginWeb"}, } session_context.update({ "login.navigationSettings": {"hideOtpToggle": True}, }) full_server_state = { "realm": "growth", "name": "PASSWORD_LOGIN", "clcsSessionId": clcs_session_id, "sessionContext": session_context, } full_screen_update = { "realm": "custom", "name": "growthLoginByPassword", "metadata": {"recaptchaSiteKey": RECAPTCHA_SITE_KEY}, "loggingAction": "Submitted", "loggingCommand": "SubmitCommand", "referrerRenditionId": rendition_id, } full_variables = { "format": "HTML", "imageFormat": "PNG", "locale": "en-US", "serverState": json.dumps(full_server_state, separators=(",", ":")), "serverScreenUpdate": json.dumps(full_screen_update, separators=(",", ":")), "inputFields": [ {"name": "password", "value": {"stringValue": PASSWORD}}, {"name": "userLoginId", "value": {"stringValue": EMAIL}}, {"name": "countryCode", "value": {"stringValue": "1"}}, {"name": "countryIsoCode", "value": {"stringValue": "US"}}, {"name": "recaptchaResponseTime", "value": {"intValue": 445}}, {"name": "recaptchaResponseToken", "value": {"stringValue": recaptcha_token}}, ], } try: operation_name = "CLCSScreenUpdate" headers = { "Host": "ios.prod.cloud.netflix.com", "Connection": "keep-alive", "X-Netflix.Request.Client.Context": REQUEST_CLIENT_CONTEXT_PASSWORD, "Content-Encoding": "msl_v1", "x-netflix.context.feature-capabilities": FEATURE_CAPABILITIES, "x-netflix.context.operation-name": operation_name, "X-Netflix.request.expiry.timeout": "15000", "X-Netflix.Request.Id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.form-factor": FORM_FACTOR, "X-Netflix.Request.Attempt": "1", "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept-Language": "en-US,en;q=0.9", "Accept": "*/*", "Content-Type": "application/json", "x-netflix.context.locales": LOCALE, "x-netflix.context.os-version": OS_VERSION, "Accept-Encoding": "gzip, deflate, br", "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.ui-flavor": UI_FLAVOR, } body = { "operationName": operation_name, "variables": full_variables, "extensions": { "persistedQuery": QUERY_IDS[operation_name] }, } login_header, login_response = msl_client.send_message(endpoint=GRAPHQL_URL, params={}, application_data=body, headers=headers) auth_cookies = {} for cookie in session.cookies: auth_cookies[cookie.name] = cookie.value data = login_response.get("data", {}) if isinstance(login_response, dict) else {} result = data.get("result", {}) if isinstance(data, dict) else {} status = result.get("status") encrypted_header_b64 = login_header.get("headerdata") header_data = {} if encrypted_header_b64: encryption_key_value = msl_client.keys.encryption sign_key_value = msl_client.keys.sign if isinstance(encryption_key_value, str): try: encryption_key = bytes.fromhex(encryption_key_value) except ValueError: encryption_key = encryption_key_value.encode("utf-8") else: encryption_key = encryption_key_value if isinstance(sign_key_value, str): try: sign_key = bytes.fromhex(sign_key_value) except ValueError: sign_key = sign_key_value.encode("utf-8") else: sign_key = sign_key_value if not encryption_key: raise RuntimeError("The encryption key is missing") if not sign_key: raise RuntimeError("The sign key is missing") encrypted_header = json.loads(base64.b64decode(encrypted_header_b64)) iv = base64.b64decode(encrypted_header["iv"]) ciphertext = base64.b64decode(encrypted_header["ciphertext"]) cipher = AES.new(encryption_key, AES.MODE_CBC, iv) decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size) header_data = json.loads(decrypted.decode("utf-8")) except Exception: log.exception("Failed to process the login response") sys.exit(1) if status == "SUCCESS": log.info("LOGIN SUCCESSFUL") try: AUTH_COOKIES_PATH.write_text( json.dumps(auth_cookies, indent=2), encoding="utf-8" ) log.info("Authentication cookies saved") except Exception: log.exception("Failed to save cookies") sys.exit(1) else: log.error("LOGIN FAILED") sys.exit(1) # ====================================================================== # TV (email/password) # ====================================================================== def run_tv(wvd_path: Path, new_msl: bool = False, no_verify: bool = False): from Crypto.Cipher import AES from Crypto.Util.Padding import unpad log = logging.getLogger('netflix_tv_login') BASE_DIR = Path(__file__).resolve().parent OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache.json" USER_ID_TOKEN_PATH = OUTPUT_DIR / "useridtoken.json" MSL_TRACE_PATH = OUTPUT_DIR / "msl_debug_trace.json" NETFLIX_COOKIES_PATH = OUTPUT_DIR / "netflix_cookies.json" LOGIN_RESPONSE_PATH = OUTPUT_DIR / "password_login_response.json" MSL_HANDSHAKE_ENDPOINT = "https://nrdp25.prod.ftl.netflix.com/nq/nrdjs/pbo_tokens/%5E1.0.0/router" MSL_TV_ENDPOINT = "https://nrdp25.prod.ftl.netflix.com/nq/nrdjs/pbo_tokens/%5E1.0.0/router" PBO_CONFIG_ENDPOINT = "https://nrdp25.prod.ftl.netflix.com/nq/nrdjs/pbo_config/%5E1.0.0/router?ab_ui_ver=darwin&nrdapp_version=2025.2.3.0" DEVICE_TYPE = "NFANDROID2-PRV-NVIDIASHIELDANDROIDTV2019" DEVICE_MODEL = "NVIDIA_SHIELD Android TV" DEVICE_NAME = "SHIELD" ANDROID_BUILD_FINGERPRINT = "12.1.9-23083 R 2025.2 android-30-JPLAYER2 ninja_6==NVIDIA/mdarcy/mdarcy:11/RQ1A.210105.003/7825230_4040.2147:user/release-keys" APP_VERSION = "UI-release-20260408_44798-gibbon-r100-aui-nrdjs=v3.12.55" AUI_SW_VERSION = "UI-release-20260408_44798-gibbon-sapphire-darwinql" SDK_VERSION = "2025.2.3.0" CLIENT_VERSION = "v3.12.55" NETJS_VERSION = "3.0.5" APK_VERSION = "12.1.9" UI_SEM_VER = "44798.0.0" ESN = f"{DEVICE_TYPE}-11233-{''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(64))}" IMPORTANT_COOKIE_NAMES = ( "netflix-mfa-nonce", "NetflixId", "SecureNetflixId", "nfvdid", "gsid", ) QUERY_IDS = { "clcsLegacyMoneyballInitiateSession": {"id": "5152154d-6b61-4333-a738-92dc4ab712bd", "version": 102}, "clcsLegacyMoneyballSubmit": {"id": "e8ef3234-6525-4975-8796-1299602e3297", "version": 102}, "clcsScreenUpdate": {"id": "8daa70b0-fc21-4b5e-8c7e-ce0f31c8ca66", "version": 102}, "useNavItemsQuery": {"id": "77a2fe81-a789-4b80-8c4c-0e962194cd09", "version": 102}, } REQUEST_ARGS = [ {"name": "deviceModel", "value": {"stringValue": DEVICE_MODEL}}, {"name": "deviceName", "value": {"stringValue": DEVICE_NAME}}, {"name": "deviceTypeOverride", "value": {"stringValue": DEVICE_TYPE}}, {"name": "esn", "value": {"stringValue": ESN}}, {"name": "fetchPartnerStrings", "value": {"booleanValue": False}}, {"name": "isSuspendedMode", "value": {"booleanValue": False}}, {"name": "nglVersion", "value": {"stringValue": "NGL_3"}}, {"name": "resolution", "value": {"stringValue": "720p"}}, {"name": "secureVLV", "value": {"stringValue": "true"}}, {"name": "swVersion", "value": {"stringValue": AUI_SW_VERSION}}, {"name": "ui_trace_tag", "value": {"stringValue": "aui-ql"}}, {"name": "allocAutomation", "value": {"booleanValue": False}}, {"name": "availableLocales", "value": {"stringValue": "zh,ta,ml,ko,te,gu,zh,kn,ur,ja"}}, {"name": "suppScripts", "value": {"stringValue": "Hant,Tibt,Thai,Taml,Sinh,Orya,Mlym,Laoo,Armn,Geor,Kore,Telu,Beng,*,Hebr,Cyrl,Gujr,Hans,Deva,Guru,Cans,Ethi,Cher,Mymr,Knda,Grek,Latn,Arab,Jpan"}}, {"name": "deviceLocale", "value": {"stringValue": "en-CA"}}, {"name": "inAppSwVersion", "value": {"stringValue": APP_VERSION}}, {"name": "appVersion", "value": {"stringValue": APP_VERSION}}, {"name": "hasGooglePlayServiceOnTenfoot", "value": {"booleanValue": True}}, {"name": "ab_ui_ver", "value": {"stringValue": "darwin"}}, {"name": "application_name", "value": {"stringValue": "htmltvui"}}, {"name": "application_v", "value": {"stringValue": APP_VERSION}}, {"name": "dh", "value": {"stringValue": "720"}}, {"name": "dw", "value": {"stringValue": "1280"}}, {"name": "falcor_server", "value": {"stringValue": "0.1.0"}}, {"name": "materialize", "value": {"booleanValue": True}}, {"name": "mdxlib_version", "value": {"stringValue": SDK_VERSION}}, {"name": "nrdapp_version", "value": {"stringValue": SDK_VERSION}}, {"name": "nrdlib_version", "value": {"stringValue": SDK_VERSION}}, {"name": "nrdp", "value": {"booleanValue": True}}, {"name": "revision", "value": {"stringValue": "latest"}}, {"name": "sdk_version", "value": {"stringValue": SDK_VERSION}}, {"name": "sw_version", "value": {"stringValue": ANDROID_BUILD_FINGERPRINT}}, {"name": "tag", "value": {"stringValue": "latest"}}, {"name": "ui_sem_ver", "value": {"stringValue": UI_SEM_VER}}, {"name": "webapiConfigAppName", "value": {"stringValue": "htmltvui"}}, {"name": "withSize", "value": {"booleanValue": True}}, ] REQUEST_ARGS_DICT: Dict[str, Any] = {} for arg in REQUEST_ARGS: value = arg["value"] if "stringValue" in value: REQUEST_ARGS_DICT[arg["name"]] = value["stringValue"] elif "booleanValue" in value: REQUEST_ARGS_DICT[arg["name"]] = value["booleanValue"] PBO_COMMON = { "sdk": SDK_VERSION, "platform": SDK_VERSION, "application": ANDROID_BUILD_FINGERPRINT, "uiversion": APP_VERSION, "uiPlatform": "tv_ui", "clientVersion": CLIENT_VERSION, "apkVersion": APK_VERSION, } MSL_TRACE: List[Dict[str, Any]] = [] urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session = requests.Session() session.verify = False launch_uid = str(uuid.uuid4()) aui_referer = ( "https://secure.netflix.com/us/tvui/aui/20260408_44798/release_v8/auiStartup.js" f"?q=source_type%3D2%26launchUID%3D{launch_uid}&dw=1280&dh=720&dar=16_9" "®=false&noMemberTarget=true" ) runtime_referer = ( "https://secure.netflix.com/us/tvui/ql/20260407/44745/release_v8/darwinBootstrap.js" "?startup_key=429c6159fd3e080b97d6df5bf5ce8e38b0ecc222773811cd12d8251aeea4a738" f"&device_type={DEVICE_TYPE}" f"&e={quote(ESN, safe='')}" "&env=prod&fromNM=true&nm_prefetch=true&nrdapp_version=2025.2.3.0&plain=true&script_engine=v8" f"&sessionId={uuid.uuid4()}&authType=login&authclid={uuid.uuid4()}" f"&q=source_type%3D2%26launchUID%3D{launch_uid}%26source_type_payload%3D" ) graphql_url = "https://nrdp.prod.cloud.netflix.com/graphql" log.info("Fetching nfvdid from Android TV config endpoint") response = session.get( "https://androidtv.prod.cloud.netflix.com/android/ninja/config", params={ "responseFormat": "json", "progressive": "false", "method": "get", "routing": "redirect", "appType": "ninja", "mnf": "NVIDIA", "mId": "SHIELD=ANDROID=TV", "appVer": "23083", "appVerName": "12.1.9 build 23083", "api": "30", "modelgroup": "NVIDIASHIELDANDROIDTV2019", "oemmodel": "", "esn": ESN, "osBoard": "darcy", "osDevice": "mdarcy", "osDisplay": "RQ1A.210105.003.7825230_4040.2147", "osFingerprint": "NVIDIA/mdarcy/mdarcy:11/RQ1A.210105.003/7825230_4040.2147:user/release-keys", "osCpu": "armeabi-v7a", "osProduct": "mdarcy", "validation": "ninja_6", "ramSizeMB": "2946", "path": ["['deviceConfig']", "['fpConfig']"], }, headers={ "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11; SHIELD Android TV Build/RQ1A.210105.003)", "Accept": "*/*", "X-Netflix.Client.Request.Name": "androidninjaconfig", "X-Netflix.Request.Client.Context": '{"appState":"foreground"}', }, timeout=30, ) response.raise_for_status() nfvdid = session.cookies.get("nfvdid") log.info("nfvdid: %s", (nfvdid[:80] + "...") if nfvdid else "not received") log.info("Bootstrap AUI and pre-login pathEvaluator") try: session.headers.clear() session.get( aui_referer, headers={ "User-Agent": f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", "Accept": "application/javascript,text/javascript,application/x-javascript", }, timeout=30, ) session.get( "https://nrdp.prod.cloud.netflix.com/healthcheck", headers={ "User-Agent": f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", "Accept": "*/*", "x-netflix.context.sdk-version": SDK_VERSION, "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": NETJS_VERSION, "X-Netflix.request.attempt": "1", "Referer": aui_referer, }, timeout=30, ) params = [ ("ab_ui_ver", "darwin"), ("application_name", "htmltvui"), ("application_v", APP_VERSION), ("dh", "720"), ("dw", "1280"), ("falcor_server", "0.1.0"), ("materialize", "true"), ("mdxlib_version", SDK_VERSION), ("nrdapp_version", SDK_VERSION), ("nrdlib_version", SDK_VERSION), ("nrdp", "true"), ("revision", "latest"), ("sdk_version", SDK_VERSION), ("sw_version", ANDROID_BUILD_FINGERPRINT), ("tag", "latest"), ("ui_sem_ver", UI_SEM_VER), ("webapiConfigAppName", "htmltvui"), ("withSize", "true"), ("availableLocales", "zh,ta,ml,ko,te,gu,zh,kn,ur,ja"), ("deviceLocale", "en-CA"), ("deviceModel", DEVICE_MODEL), ("deviceName", DEVICE_NAME), ("deviceTypeOverride", DEVICE_TYPE), ("esn", ESN), ("hasGooglePlayServiceOnTenfoot", "true"), ("isSuspendedMode", "false"), ("netflixClientPlatform", "tenfootMDS"), ("nglVersion", "NGL_3"), ("resolution", "720p"), ("secureVLV", "true"), ("suppScripts", "Hant,Tibt,Thai,Taml,Sinh,Orya,Mlym,Laoo,Armn,Geor,Kore,Telu,Beng,*,Hebr,Cyrl,Gujr,Hans,Deva,Guru,Cans,Ethi,Cher,Mymr,Knda,Grek,Latn,Arab,Jpan"), ("swVersion", AUI_SW_VERSION), ("ui_trace_tag", "aui-ql"), ("inAppSwVersion", APP_VERSION), ("path", '["aui",["appconfig","partnerData","requestContext","userContext"]]'), ("path", '["aui","truths",["project.bao.ui.enabled","tvui.aui.bugsnag.enabled","tvui.aui.clcs.enabled"]]'), ("json", "true"), ("method", "get"), ("seed", str(random.random())), ] session.headers.clear() session.headers.update( { "User-Agent": f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", "Accept": "*/*", "Accept-Encoding": "deflate,gzip", "x-netflix.context.sdk-version": SDK_VERSION, "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "X-Netflix.request.expiry.timeout": "20000", "X-Netflix.Client.Request.Name": "ui/falcorUnclassified", "X-Netflix.Request.Routing": '{"control_tag":"auinqtv","path":"/nq/aui/endpoint/%5E1.0.0-tv/pathEvaluator"}', "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": NETJS_VERSION, "X-Netflix.request.attempt": "1", "Referer": aui_referer, } ) response = session.get("https://api-global.netflix.com/aui/pathEvaluator/tv/latest?" + urlencode(params, doseq=True), timeout=30) response.raise_for_status() log.info("AUI bootstrap OK") except Exception as exc: log.warning("AUI bootstrap failed: %s", exc) log.info("MSL handshake and mintCookies") msl = None try: cached_keys = MSL_TV.load_cache_data(MSL_CACHE_PATH) except Exception: cached_keys = None try: if cached_keys and getattr(cached_keys, "mastertoken", None) and getattr(cached_keys, "encryption", None) and getattr(cached_keys, "sign", None): msl = MSL_TV( session=session, keys=cached_keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) log.info("Using cached MSL keys") else: if not wvd_path.exists(): raise FileNotFoundError(f"Missing WVD file: {wvd_path}") device = WidevineDevice.load(wvd_path) cdm = WidevineCdm.from_device(device) cdm_device = str(wvd_path) cookies_for_handshake: Dict[str, str] = {} if nfvdid: cookies_for_handshake["nfvdid"] = nfvdid msl_headers = MSL_TV.build_request_headers( request_name="mintCookies", user_agent=f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", referer=None, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.client.nrdjs.version": CLIENT_VERSION, "x-netflix.esn": ESN, }, ) keys = MSL_TV.handshake( msl_keys_path=str(MSL_CACHE_PATH), session=session, sender=ESN, cdm=cdm, cdm_device=cdm_device, new_msl=False, cookies=cookies_for_handshake, drm="widevine", endpoint=MSL_HANDSHAKE_ENDPOINT, headers=msl_headers, ) if not keys or not keys.mastertoken: raise RuntimeError("MSL handshake did not return a valid master token") msl = MSL_TV( session=session, keys=keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) log.info("MSL handshake OK") if not (msl.keys and msl.keys.mastertoken and msl.keys.encryption and msl.keys.sign): cached_keys = MSL_TV.load_cache_data(MSL_CACHE_PATH) if cached_keys is None: raise RuntimeError("MSL cache is empty or expired") msl = MSL_TV( session=msl.session, keys=cached_keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) msl.session.headers.clear() mint_headers = MSL_TV.build_request_headers( request_name="mintCookies", user_agent=f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", referer=runtime_referer, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) header, payload = msl.send_message( endpoint=MSL_TV_ENDPOINT, params={}, application_data={ "version": 2, "common": dict(PBO_COMMON), "url": "/mintCookies", "languages": ["en-CA"], "params": {}, }, headers=mint_headers, ) payload_type = type(payload).__name__ parsed_payload = None text_payload = None if isinstance(payload, dict): payload_type = "msl_payload" parsed_payload = payload elif isinstance(payload, list): payload_type = "json_array" parsed_payload = {"items": payload} elif isinstance(payload, str): cleaned = payload.rstrip("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10") text_payload = cleaned try: maybe_json = json.loads(cleaned) if isinstance(maybe_json, dict): parsed_payload = maybe_json payload_type = "text" except Exception: payload_type = "text" key_id = "" if msl.keys.mastertoken: token_data = json.loads(base64.b64decode(msl.keys.mastertoken["tokendata"]).decode("utf-8")) key_id = str(token_data.get("sequencenumber", "")).encode("utf-8").hex() event: Dict[str, Any] = { "_type": "decrypt", "_mslId": msl.message_id, "_timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), "_keyId": key_id, "_dataType": payload_type, } if text_payload is not None: event["_text"] = text_payload if parsed_payload is not None: event["_payload"] = parsed_payload MSL_TRACE.append(event) useridtoken = None stack = [parsed_payload if parsed_payload is not None else payload] while stack: current = stack.pop() if isinstance(current, dict): if useridtoken is None and set(current.keys()) >= {"tokendata", "signature"}: useridtoken = current for child in current.values(): stack.append(child) elif isinstance(current, list): for item in current: stack.append(item) if useridtoken: USER_ID_TOKEN_PATH.write_text(json.dumps(useridtoken, indent=2), encoding="utf-8") log.info("useridtoken saved to %s", USER_ID_TOKEN_PATH.name) log.info("Cookies after mintCookies: %s", [cookie.name for cookie in session.cookies]) except Exception as exc: log.warning("MSL setup or mintCookies failed: %s", exc) log.warning("Continuing with the HAR login order anyway") log.info("CLCS initiate session") trace_uuid = str(uuid.uuid4()) session.headers.clear() session.headers.update( { "Language": "en-CA,en-US,en", "User-Agent": f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", "Accept": "*/*", "Accept-Language": "en-CA,en-US,en", "Accept-Encoding": "deflate,gzip", "Content-Type": "application/json", "Connection": "Keep-Alive", "x-netflix.context.sdk-version": SDK_VERSION, "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "x-netflix.request.expiry.timeout": "20000", "x-Netflix.context.app-version": UI_SEM_VER, "x-Netflix.context.cloud-games-enabled": "false", "X-Netflix.context.device-height": "720", "x-Netflix.context.device-image-capability": "scalingFactor=1.0;supportedFormats=jpg,png,astc", "x-Netflix.context.dt": "", "x-Netflix.context.hawkins-version": "5.13.0", "X-Netflix.context.locales": '["en-CA","en-US","en"]', "X-Netflix.context.ui-flavor": "photon", "X-Netflix.request.device-model": quote(DEVICE_MODEL, safe=""), "X-Netflix.request.is-suspended": "false", "x-netflix.request.clcs.bucket": "high", "X-Netflix.request.toplevel.uuid": trace_uuid, "X-Netflix.tracing.cl.userActionId": trace_uuid, "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": NETJS_VERSION, "X-Netflix.request.attempt": "1", "X-Netflix.context.operation-name": "clcsLegacyMoneyballInitiateSession", "Referer": aui_referer, } ) cookie_values: List[str] = [] headers = dict(session.headers) cookie_header_map: Dict[str, str] = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + f"?device_type={DEVICE_TYPE}&esn={quote(ESN, safe='')}&o=clcsLegacyMoneyballInitiateSession", json={ "extensions": {"persistedQuery": QUERY_IDS["clcsLegacyMoneyballInitiateSession"]}, "operationName": "clcsLegacyMoneyballInitiateSession", "variables": { "action": "", "flow": "tenfootSignUp", "hasGooglePlayService": False, "imageFormat": "ASTC", "inputFields": [], "legacyRequestArguments": REQUEST_ARGS, "mode": "none", "resolutionMode": "TV_720P", "supportedVideoFormat": "mp4", }, }, headers=headers, timeout=30, ) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) response.raise_for_status() init_data = response.json() flow: Dict[str, str] = {} data = init_data.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow.get("clcsSessionId"): flow["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId") and not flow.get("renditionId"): flow["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow.get("flowSessionId"): flow["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode") and not flow.get("mode"): flow["mode"] = payload.get("mode", "") if value.get("membershipStatus"): flow["membershipStatus"] = value.get("membershipStatus", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) flow_session_id = flow.get("flowSessionId", "") clcs_session_id = flow.get("clcsSessionId", "") rendition_id = flow.get("renditionId", "") log.info("flowSessionId: %s", flow_session_id) log.info("clcsSessionId: %s", clcs_session_id) log.info("initial renditionId: %s", rendition_id) if not flow_session_id or not clcs_session_id: raise RuntimeError("Failed to extract flow/session IDs from initiate session response") log.info("Move from welcome landing into web sign-in and password path") # Step 5a: signInAction on welcomeContentLanding trace_uuid = str(uuid.uuid4()) session.headers["X-Netflix.request.id"] = "".join(random.choice("0123456789ABCDEF") for _ in range(32)) session.headers["X-Netflix.request.toplevel.uuid"] = trace_uuid session.headers["X-Netflix.tracing.cl.userActionId"] = trace_uuid session.headers["X-Netflix.context.operation-name"] = "clcsLegacyMoneyballSubmit" server_state = json.dumps( { "realm": "moneyball", "flow": "tenfootSignUp", "mode": "welcomeContentLanding", "flowSessionId": flow_session_id, "requestArguments": REQUEST_ARGS_DICT, "clcsSessionId": clcs_session_id, }, separators=(",", ":"), ) cookie_values = [] headers = dict(session.headers) cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + f"?device_type={DEVICE_TYPE}&esn={quote(ESN, safe='')}&o=clcsLegacyMoneyballSubmit", json={ "extensions": {"persistedQuery": QUERY_IDS["clcsLegacyMoneyballSubmit"]}, "operationName": "clcsLegacyMoneyballSubmit", "variables": { "action": "signInAction", "flow": "tenfootSignUp", "flwssn": flow_session_id, "imageFormat": "ASTC", "inputFields": [], "mode": "welcomeContentLanding", "requestArguments": REQUEST_ARGS, "resolutionMode": "TV_720P", "serverState": server_state, }, }, headers=headers, timeout=30, ) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) response.raise_for_status() submit_data = response.json() mfa_nonce = session.cookies.get("netflix-mfa-nonce") log.info("netflix-mfa-nonce: %s", (mfa_nonce[:80] + "...") if mfa_nonce else "missing") flow_update: Dict[str, str] = {} data = submit_data.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow_update.get("clcsSessionId"): flow_update["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow_update["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow_update.get("flowSessionId"): flow_update["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow_update["mode"] = payload.get("mode", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) flow_session_id = flow_update.get("flowSessionId", flow_session_id) clcs_session_id = flow_update.get("clcsSessionId", clcs_session_id) rendition_id = flow_update.get("renditionId", rendition_id) # Step 5b: lrudSignInAction on webSignIn trace_uuid = str(uuid.uuid4()) session.headers["X-Netflix.request.id"] = "".join(random.choice("0123456789ABCDEF") for _ in range(32)) session.headers["X-Netflix.request.toplevel.uuid"] = trace_uuid session.headers["X-Netflix.tracing.cl.userActionId"] = trace_uuid session.headers["X-Netflix.context.operation-name"] = "clcsScreenUpdate" server_state = json.dumps( { "realm": "moneyball", "flow": "tenfootSignUp", "mode": "webSignIn", "flowSessionId": flow_session_id, "requestArguments": REQUEST_ARGS_DICT, "clcsSessionId": clcs_session_id, }, separators=(",", ":"), ) cookie_values = [] headers = dict(session.headers) cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + f"?device_type={DEVICE_TYPE}&esn={quote(ESN, safe='')}&o=clcsScreenUpdate", json={ "extensions": {"persistedQuery": QUERY_IDS["clcsScreenUpdate"]}, "operationName": "clcsScreenUpdate", "variables": { "imageFormat": "PNG", "inputFields": [], "resolutionMode": "TV_720P", "serverScreenUpdate": json.dumps( { "realm": "moneyball", "action": "lrudSignInAction", "loggingAction": "Submitted", "loggingCommand": "SubmitCommand", "referrerRenditionId": rendition_id, }, separators=(",", ":"), ), "serverState": server_state, }, }, headers=headers, timeout=30, ) response.raise_for_status() step_web_signin = response.json() flow_update = {} data = step_web_signin.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow_update.get("clcsSessionId"): flow_update["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow_update["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow_update.get("flowSessionId"): flow_update["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow_update["mode"] = payload.get("mode", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) flow_session_id = flow_update.get("flowSessionId", flow_session_id) clcs_session_id = flow_update.get("clcsSessionId", clcs_session_id) rendition_id = flow_update.get("renditionId", rendition_id) # Step 5c: submitUserIdAction on enterMemberCredentials trace_uuid = str(uuid.uuid4()) session.headers["X-Netflix.request.id"] = "".join(random.choice("0123456789ABCDEF") for _ in range(32)) session.headers["X-Netflix.request.toplevel.uuid"] = trace_uuid session.headers["X-Netflix.tracing.cl.userActionId"] = trace_uuid session.headers["X-Netflix.context.operation-name"] = "clcsScreenUpdate" server_state = json.dumps( { "realm": "moneyball", "flow": "tenfootSignUp", "mode": "enterMemberCredentials", "flowSessionId": flow_session_id, "requestArguments": REQUEST_ARGS_DICT, "clcsSessionId": clcs_session_id, }, separators=(",", ":"), ) cookie_values = [] headers = dict(session.headers) cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + f"?device_type={DEVICE_TYPE}&esn={quote(ESN, safe='')}&o=clcsScreenUpdate", json={ "extensions": {"persistedQuery": QUERY_IDS["clcsScreenUpdate"]}, "operationName": "clcsScreenUpdate", "variables": { "imageFormat": "PNG", "inputFields": [ {"name": "userLoginId", "value": {"stringValue": EMAIL}}, ], "resolutionMode": "TV_720P", "serverScreenUpdate": json.dumps( { "realm": "moneyball", "action": "submitUserIdAction", "loggingAction": "Submitted", "loggingCommand": "SubmitCommand", "referrerRenditionId": rendition_id, }, separators=(",", ":"), ), "serverState": server_state, }, }, headers=headers, timeout=30, ) response.raise_for_status() step_user = response.json() flow_update = {} data = step_user.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow_update.get("clcsSessionId"): flow_update["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow_update["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow_update.get("flowSessionId"): flow_update["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow_update["mode"] = payload.get("mode", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) flow_session_id = flow_update.get("flowSessionId", flow_session_id) clcs_session_id = flow_update.get("clcsSessionId", clcs_session_id) rendition_id = flow_update.get("renditionId", rendition_id) # Step 5d: usePasswordAction on loginLinkOption trace_uuid = str(uuid.uuid4()) session.headers["X-Netflix.request.id"] = "".join(random.choice("0123456789ABCDEF") for _ in range(32)) session.headers["X-Netflix.request.toplevel.uuid"] = trace_uuid session.headers["X-Netflix.tracing.cl.userActionId"] = trace_uuid session.headers["X-Netflix.context.operation-name"] = "clcsScreenUpdate" server_state = json.dumps( { "realm": "moneyball", "flow": "tenfootSignUp", "mode": "loginLinkOption", "flowSessionId": flow_session_id, "requestArguments": REQUEST_ARGS_DICT, "clcsSessionId": clcs_session_id, }, separators=(",", ":"), ) cookie_values = [] headers = dict(session.headers) cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + f"?device_type={DEVICE_TYPE}&esn={quote(ESN, safe='')}&o=clcsScreenUpdate", json={ "extensions": {"persistedQuery": QUERY_IDS["clcsScreenUpdate"]}, "operationName": "clcsScreenUpdate", "variables": { "imageFormat": "PNG", "inputFields": [], "resolutionMode": "TV_720P", "serverScreenUpdate": json.dumps( { "realm": "moneyball", "action": "usePasswordAction", "replaceCurrentScreen": True, "loggingAction": "Submitted", "loggingCommand": "SubmitCommand", "referrerRenditionId": rendition_id, }, separators=(",", ":"), ), "serverState": server_state, }, }, headers=headers, timeout=30, ) response.raise_for_status() step_password_path = response.json() flow_update = {} data = step_password_path.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow_update.get("clcsSessionId"): flow_update["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow_update["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow_update.get("flowSessionId"): flow_update["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow_update["mode"] = payload.get("mode", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) flow_session_id = flow_update.get("flowSessionId", flow_session_id) clcs_session_id = flow_update.get("clcsSessionId", clcs_session_id) rendition_id = flow_update.get("renditionId", rendition_id) log.info("Credential path ready, current renditionId: %s", rendition_id) log.info("Submit email and password") trace_uuid = str(uuid.uuid4()) session.headers["X-Netflix.request.id"] = "".join(random.choice("0123456789ABCDEF") for _ in range(32)) session.headers["X-Netflix.request.toplevel.uuid"] = trace_uuid session.headers["X-Netflix.tracing.cl.userActionId"] = trace_uuid session.headers["X-Netflix.context.operation-name"] = "clcsScreenUpdate" server_state = json.dumps( { "realm": "moneyball", "flow": "tenfootSignUp", "mode": "enterMemberCredentials", "flowSessionId": flow_session_id, "requestArguments": REQUEST_ARGS_DICT, "clcsSessionId": clcs_session_id, }, separators=(",", ":"), ) cookie_values = [] headers = dict(session.headers) cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + f"?device_type={DEVICE_TYPE}&esn={quote(ESN, safe='')}&o=clcsScreenUpdate", json={ "extensions": {"persistedQuery": QUERY_IDS["clcsScreenUpdate"]}, "operationName": "clcsScreenUpdate", "variables": { "imageFormat": "PNG", "inputFields": [ {"name": "userLoginId", "value": {"stringValue": EMAIL}}, {"name": "password", "value": {"stringValue": PASSWORD}}, ], "resolutionMode": "TV_720P", "serverScreenUpdate": json.dumps( { "realm": "moneyball", "action": "nextAction", "loggingAction": "Submitted", "loggingCommand": "SubmitCommand", "referrerRenditionId": rendition_id, }, separators=(",", ":"), ), "serverState": server_state, }, }, headers=headers, timeout=30, ) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) response.raise_for_status() login_data = response.json() LOGIN_RESPONSE_PATH.write_text(json.dumps(login_data, indent=2, ensure_ascii=False), encoding="utf-8") flow_result: Dict[str, str] = {} data = login_data.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow_result.get("clcsSessionId"): flow_result["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow_result["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow_result.get("flowSessionId"): flow_result["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow_result["mode"] = payload.get("mode", "") if value.get("membershipStatus"): flow_result["membershipStatus"] = value.get("membershipStatus", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) membership = flow_result.get("membershipStatus", "") flow_session_id = flow_result.get("flowSessionId", flow_session_id) clcs_session_id = flow_result.get("clcsSessionId", clcs_session_id) rendition_id = flow_result.get("renditionId", rendition_id) log.info("Membership after credential submit: %s", membership) log.info("Post-login bootstrap to obtain gsid") trace_uuid = str(uuid.uuid4()) session.headers.clear() session.headers.update( { "Language": "en-CA,en-US,en", "User-Agent": f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", "Accept": "*/*", "Accept-Language": "en-CA,en-US,en", "Accept-Encoding": "deflate,gzip", "Content-Type": "application/json", "Connection": "Keep-Alive", "x-netflix.context.sdk-version": SDK_VERSION, "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"browseTitles","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "x-netflix.request.expiry.timeout": "20000", "x-Netflix.context.app-version": UI_SEM_VER, "x-Netflix.context.cloud-games-enabled": "false", "X-Netflix.context.device-height": "720", "x-Netflix.context.device-image-capability": "scalingFactor=1.0;supportedFormats=jpg,png,astc,webp", "x-Netflix.context.dt": "", "x-Netflix.context.hawkins-version": "5.13.0", "X-Netflix.context.locales": '["en-CA","en-US","en"]', "X-Netflix.context.ui-flavor": "photon", "X-Netflix.request.device-model": quote(DEVICE_MODEL, safe=""), "X-Netflix.request.is-suspended": "false", "x-netflix.request.clcs.bucket": "high", "X-Netflix.request.toplevel.uuid": trace_uuid, "X-Netflix.tracing.cl.userActionId": trace_uuid, "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": NETJS_VERSION, "X-Netflix.request.attempt": "1", "X-Netflix.context.operation-name": "useNavItemsQuery", "Referer": runtime_referer, } ) cookie_values = [] headers = dict(session.headers) cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) if cookie_header: headers["Cookie"] = cookie_header response = session.post( graphql_url + "?o=useNavItemsQuery", json={ "extensions": {"persistedQuery": QUERY_IDS["useNavItemsQuery"]}, "operationName": "useNavItemsQuery", "query": None, "variables": { "artworkCapability": { "artworkResolution": "TVUI_720P", "deviceResolution": "TVUI_720P", "disablePersonalization": False, "supportsAstcFormat": True, "useWebPForAllImages": True, "useWebPForLargeImages": True, } }, }, headers=headers, timeout=30, ) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) response.raise_for_status() gsid = session.cookies.get("gsid") log.info("gsid: %s", gsid if gsid else "missing") log.info("Post-login PBO config and token refresh") if msl is not None and "NetflixId" in session.cookies.get_dict() and "SecureNetflixId" in session.cookies.get_dict(): try: if not (msl.keys and msl.keys.mastertoken and msl.keys.encryption and msl.keys.sign): cached_keys = MSL_TV.load_cache_data(MSL_CACHE_PATH) if cached_keys is None: raise RuntimeError("MSL cache is empty or expired") msl = MSL_TV( session=msl.session, keys=cached_keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) msl.session.headers.clear() config_headers = MSL_TV.build_request_headers( request_name="config", user_agent=f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", referer=aui_referer, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) msl.send_message( endpoint=PBO_CONFIG_ENDPOINT, params={}, application_data={"method": "config", "params": {}}, headers=config_headers, ) for request_name, route, referer_to_use in [ ("getPartnerToken", "/getPartnerToken", aui_referer), ("ping", "/ping", runtime_referer), ("getPartnerToken", "/getPartnerToken", runtime_referer), ]: if not (msl.keys and msl.keys.mastertoken and msl.keys.encryption and msl.keys.sign): cached_keys = MSL_TV.load_cache_data(MSL_CACHE_PATH) if cached_keys is None: raise RuntimeError("MSL cache is empty or expired") msl = MSL_TV( session=msl.session, keys=cached_keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) msl.session.headers.clear() optional_headers = MSL_TV.build_request_headers( request_name=request_name, user_agent=f"Netflix/{SDK_VERSION} (DEVTYPE={DEVICE_TYPE}; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", referer=referer_to_use, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) header, payload = msl.send_message( endpoint=MSL_TV_ENDPOINT, params={}, application_data={ "version": 2, "common": dict(PBO_COMMON), "url": route, "languages": ["en-CA"], "params": {}, }, headers=optional_headers, ) payload_type = type(payload).__name__ parsed_payload = None text_payload = None if isinstance(payload, dict): payload_type = "msl_payload" parsed_payload = payload elif isinstance(payload, list): payload_type = "json_array" parsed_payload = {"items": payload} elif isinstance(payload, str): cleaned = payload.rstrip("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10") text_payload = cleaned try: maybe_json = json.loads(cleaned) if isinstance(maybe_json, dict): parsed_payload = maybe_json payload_type = "text" except Exception: payload_type = "text" event = { "_type": "decrypt", "_mslId": msl.message_id, "_timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), "_dataType": payload_type, } if text_payload is not None: event["_text"] = text_payload if parsed_payload is not None: event["_payload"] = parsed_payload MSL_TRACE.append(event) if isinstance(header, dict) and "headerdata" in header: try: encrypted_header = json.loads(base64.b64decode(header["headerdata"])) iv = base64.b64decode(encrypted_header["iv"]) ciphertext = base64.b64decode(encrypted_header["ciphertext"]) cipher = AES.new(msl.keys.encryption, AES.MODE_CBC, iv) decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size) header_data = json.loads(decrypted.decode("utf-8")) tokens = header_data.get("useridtoken") if tokens: USER_ID_TOKEN_PATH.write_text(json.dumps(tokens, indent=2), encoding="utf-8") log.info("useridtoken refreshed from %s", route) except Exception: pass except Exception as exc: log.warning("Post-login MSL refresh failed: %s", exc) log.info("Save filtered cookies") preferred: Dict[str, Any] = {} for cookie in session.cookies: if cookie.name not in IMPORTANT_COOKIE_NAMES: continue current = preferred.get(cookie.name) score = (cookie.domain == ".netflix.com", cookie.path == "/", bool(cookie.value)) if current is None or score >= current[0]: preferred[cookie.name] = (score, cookie) for cookie in list(session.cookies): winner = preferred.get(cookie.name) if not winner: continue winner_cookie = winner[1] if (cookie.domain, cookie.path, cookie.value) != (winner_cookie.domain, winner_cookie.path, winner_cookie.value): try: session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) except Exception: pass cookies: Dict[str, str] = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookies: cookies[cookie.name] = cookie.value NETFLIX_COOKIES_PATH.write_text(json.dumps(cookies, indent=2), encoding="utf-8") MSL_TRACE_PATH.write_text(json.dumps(MSL_TRACE, indent=2, ensure_ascii=False), encoding="utf-8") log.info("Final status") if membership == "CURRENT_MEMBER" and "NetflixId" in cookies and "SecureNetflixId" in cookies: log.info("LOGIN SUCCESSFUL") log.info("NetflixId: %s", f"{cookies['NetflixId'][:80]}...") log.info("SecureNetflixId: %s", f"{cookies['SecureNetflixId'][:80]}...") log.info("nfvdid: %s", f"{cookies.get('nfvdid', 'N/A')[:80]}...") log.info("netflix-mfa-nonce: %s", f"{cookies.get('netflix-mfa-nonce', 'N/A')[:80]}...") log.info("gsid: %s", cookies.get("gsid", "N/A")) else: log.error("LOGIN FAILED") exit(1) result = { "cookies": cookies, "session": session, "flow_session_id": flow_session_id, "clcs_session_id": clcs_session_id, "response": login_data, "useridtoken_path": str(USER_ID_TOKEN_PATH) if USER_ID_TOKEN_PATH.exists() else None, "msl_trace_path": str(MSL_TRACE_PATH), "login_response_path": str(LOGIN_RESPONSE_PATH), } log.info("Cookies saved to %s", NETFLIX_COOKIES_PATH.name) log.info("MSL decrypt trace saved to %s", MSL_TRACE_PATH.name) log.info("Password login response saved to %s", LOGIN_RESPONSE_PATH.name) if result["useridtoken_path"]: log.info("useridtoken saved to %s", Path(result["useridtoken_path"]).name) else: log.info("useridtoken was not observed during this run") # ====================================================================== # TV OTP (pairing code) # ====================================================================== def run_tv_otp(wvd_path: Path, new_msl: bool = False, no_verify: bool = False): log = logging.getLogger('netflix_tv_login') from typing import Any, Dict, List BASE_DIR = Path(__file__).resolve().parent OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache.json" USER_ID_TOKEN_PATH = OUTPUT_DIR / "useridtoken.json" NETFLIX_COOKIES_PATH = OUTPUT_DIR / "netflix_cookies.json" MSL_HANDSHAKE_ENDPOINT = "https://nrdp25.prod.ftl.netflix.com/nq/nrdjs/pbo_tokens/%5E1.0.0/router" MSL_TV_ENDPOINT = "https://nrdp25.prod.ftl.netflix.com/nq/nrdjs/pbo_tokens/%5E1.0.0/router" PBO_CONFIG_ENDPOINT = "https://nrdp25.prod.ftl.netflix.com/nq/nrdjs/pbo_config/%5E1.0.0/router?ab_ui_ver=darwin&nrdapp_version=2025.2.3.0" DEVICE_TYPE = "NFANDROID2-PRV-NVIDIASHIELDANDROIDTV2019" DEVICE_MODEL = "NVIDIA_SHIELD Android TV" DEVICE_NAME = "SHIELD" ANDROID_BUILD_FINGERPRINT = "12.1.9-23083 R 2025.2 android-30-JPLAYER2 ninja_6==NVIDIA/mdarcy/mdarcy:11/RQ1A.210105.003/7825230_4040.2147:user/release-keys" APP_VERSION = "UI-release-20260408_44798-gibbon-r100-aui-nrdjs=v3.12.55" ESN = f"NFANDROID2-PRV-NVIDIASHIELDANDROIDTV2019-NVIDISHIELD=ANDROID=TV-11233-{''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(64))}" IMPORTANT_COOKIE_NAMES = ("netflix-mfa-nonce", "NetflixId", "SecureNetflixId", "nfvdid", "gsid") QUERY_IDS = { "clcsLegacyMoneyballInitiateSession": {"id": "5152154d-6b61-4333-a738-92dc4ab712bd", "version": 102}, "clcsLegacyMoneyballSubmit": {"id": "e8ef3234-6525-4975-8796-1299602e3297", "version": 102}, "clcsScreenUpdate": {"id": "8daa70b0-fc21-4b5e-8c7e-ce0f31c8ca66", "version": 102}, "useNavItemsQuery": {"id": "77a2fe81-a789-4b80-8c4c-0e962194cd09", "version": 102}, } REQUEST_ARGS = [ {"name": "deviceModel", "value": {"stringValue": DEVICE_MODEL}}, {"name": "deviceName", "value": {"stringValue": DEVICE_NAME}}, {"name": "deviceTypeOverride", "value": {"stringValue": DEVICE_TYPE}}, {"name": "esn", "value": {"stringValue": ESN}}, {"name": "fetchPartnerStrings", "value": {"booleanValue": False}}, {"name": "isSuspendedMode", "value": {"booleanValue": False}}, {"name": "nglVersion", "value": {"stringValue": "NGL_3"}}, {"name": "resolution", "value": {"stringValue": "720p"}}, {"name": "secureVLV", "value": {"stringValue": "true"}}, {"name": "swVersion", "value": {"stringValue": "UI-release-20260408_44798-gibbon-sapphire-darwinql"}}, {"name": "ui_trace_tag", "value": {"stringValue": "aui-ql"}}, {"name": "sourceType", "value": {"stringValue": "2"}}, {"name": "allocAutomation", "value": {"booleanValue": False}}, {"name": "availableLocales", "value": {"stringValue": "zh,ta,ml,ko,te,gu,zh,kn,ur,ja"}}, {"name": "suppScripts", "value": {"stringValue": "Hant,Tibt,Thai,Taml,Sinh,Orya,Mlym,Laoo,Armn,Geor,Kore,Telu,Beng,*,Hebr,Cyrl,Gujr,Hans,Deva,Guru,Cans,Ethi,Cher,Mymr,Knda,Grek,Latn,Arab,Jpan"}}, {"name": "deviceLocale", "value": {"stringValue": "en-CA"}}, {"name": "inAppSwVersion", "value": {"stringValue": APP_VERSION}}, {"name": "appVersion", "value": {"stringValue": APP_VERSION}}, {"name": "hasGooglePlayServiceOnTenfoot", "value": {"booleanValue": True}}, {"name": "ab_ui_ver", "value": {"stringValue": "darwin"}}, {"name": "application_name", "value": {"stringValue": "htmltvui"}}, {"name": "application_v", "value": {"stringValue": APP_VERSION}}, {"name": "dh", "value": {"stringValue": "720"}}, {"name": "dw", "value": {"stringValue": "1280"}}, {"name": "falcor_server", "value": {"stringValue": "0.1.0"}}, {"name": "materialize", "value": {"booleanValue": True}}, {"name": "mdxlib_version", "value": {"stringValue": "2025.2.3.0"}}, {"name": "nrdapp_version", "value": {"stringValue": "2025.2.3.0"}}, {"name": "nrdlib_version", "value": {"stringValue": "2025.2.3.0"}}, {"name": "nrdp", "value": {"booleanValue": True}}, {"name": "revision", "value": {"stringValue": "latest"}}, {"name": "sdk_version", "value": {"stringValue": "2025.2.3.0"}}, {"name": "sw_version", "value": {"stringValue": ANDROID_BUILD_FINGERPRINT}}, {"name": "tag", "value": {"stringValue": "latest"}}, {"name": "ui_sem_ver", "value": {"stringValue": "44798.0.0"}}, {"name": "webapiConfigAppName", "value": {"stringValue": "htmltvui"}}, {"name": "withSize", "value": {"booleanValue": True}}, ] HEADERS = { "User-Agent": "Netflix/2025.2.3.0 (DEVTYPE=NFANDROID2-PRV-NVIDIASHIELDANDROIDTV2019; Milo=1.0.6315; build_number=6315; build_sha=a1b915de)", "Accept": "*/*", "Accept-Encoding": "deflate,gzip", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "x-netflix.request.expiry.timeout": "12750", "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.client.nrdjs.version": "v3.12.55", "Content-Type": "application/json", "Content-Encoding": "msl_v1", "X-Netflix.Client.Request.Name": "mintCookies", "X-Netflix.Request.NonJson.Headers": "true", "X-Netflix.Request.Client.Context": '{"appstate":"foreground","reason":"unknown"}', "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "x-netflix.esn": ESN, } PBO_COMMON = { "sdk": "2025.2.3.0", "platform": "2025.2.3.0", "application": ANDROID_BUILD_FINGERPRINT, "uiversion": APP_VERSION, "uiPlatform": "tv_ui", "clientVersion": "v3.12.55", "apkVersion": "12.1.9", } AUI_STARTUP_URL = ( "https://secure.netflix.com/us/tvui/aui/20260408_44798/release_v8/auiStartup.js" "?q=source_type%3D2%26launchUID%3D{launch_uid}&dw=1280&dh=720&dar=16_9" "®=false&noMemberTarget=true" ) ANDROID_CONFIG_URL = "https://androidtv.prod.cloud.netflix.com/android/ninja/config" ANDROID_CONFIG_PARAMS = { "responseFormat": "json", "progressive": "false", "method": "get", "routing": "redirect", "appType": "ninja", "mnf": "NVIDIA", "mId": "SHIELD=ANDROID=TV", "appVer": "23083", "appVerName": "12.1.9 build 23083", "api": "30", "modelgroup": "NVIDIASHIELDANDROIDTV2019", "oemmodel": "", "esn": ESN, "osBoard": "darcy", "osDevice": "mdarcy", "osDisplay": "RQ1A.210105.003.7825230_4040.2147", "osFingerprint": "NVIDIA/mdarcy/mdarcy:11/RQ1A.210105.003/7825230_4040.2147:user/release-keys", "osCpu": "armeabi-v7a", "osProduct": "mdarcy", "validation": "ninja_6", "ramSizeMB": "2946", "path": ["['deviceConfig']", "['fpConfig']"], } MSL_TRACE: List[Dict[str, Any]] = [] urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session = requests.Session() session.verify = False launch_uid = str(uuid.uuid4()) aui_referer = AUI_STARTUP_URL.format(launch_uid=launch_uid) runtime_referer = ( "https://secure.netflix.com/us/tvui/ql/20260407/44745/release_v8/darwinBootstrap.js" "?startup_key=429c6159fd3e080b97d6df5bf5ce8e38b0ecc222773811cd12d8251aeea4a738" f"&device_type={DEVICE_TYPE}" f"&e={quote(ESN, safe='')}" "&env=prod&fromNM=true&nm_prefetch=true&nrdapp_version=2025.2.3.0&plain=true&script_engine=v8" f"&sessionId={uuid.uuid4()}&authType=login&authclid={uuid.uuid4()}" f"&q=source_type%3D2%26launchUID%3D{launch_uid}%26source_type_payload%3D" ) log.info("Fetching nfvdid from Android TV config endpoint") response = session.get( ANDROID_CONFIG_URL, params=ANDROID_CONFIG_PARAMS, headers={ "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11; SHIELD Android TV Build/RQ1A.210105.003)", "Accept": "*/*", "X-Netflix.Client.Request.Name": "androidninjaconfig", "X-Netflix.Request.Client.Context": '{"appState":"foreground"}', }, timeout=30, ) log.info("Config response HTTP %d", response.status_code) if response.status_code != 200: log.warning("Config request failed: %s", response.text[:500]) nfvdid = session.cookies.get("nfvdid") log.info("nfvdid: %s", (nfvdid[:60] + "...") if nfvdid else "not received") log.info("Bootstrap TV UI") try: session.headers.clear() ua = HEADERS["User-Agent"] session.get( aui_referer, headers={"User-Agent": ua, "Accept": "application/javascript,text/javascript,application/x-javascript"}, timeout=30, ) session.get( "https://nrdp.prod.cloud.netflix.com/healthcheck", headers={ "User-Agent": ua, "Accept": "*/*", "x-netflix.context.sdk-version": "2025.2.3.0", "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "Referer": aui_referer, }, timeout=30, ) log.info("Bootstrap OK") except Exception as exc: log.warning("Bootstrap failed: %s", exc) log.info("pre-mint pathEvaluator") try: params = [ ("ab_ui_ver", "darwin"), ("application_name", "htmltvui"), ("application_v", APP_VERSION), ("dh", "720"), ("dw", "1280"), ("falcor_server", "0.1.0"), ("materialize", "true"), ("mdxlib_version", "2025.2.3.0"), ("nrdapp_version", "2025.2.3.0"), ("nrdlib_version", "2025.2.3.0"), ("nrdp", "true"), ("revision", "latest"), ("sdk_version", "2025.2.3.0"), ("sw_version", ANDROID_BUILD_FINGERPRINT), ("tag", "latest"), ("ui_sem_ver", "44798.0.0"), ("webapiConfigAppName", "htmltvui"), ("withSize", "true"), ("availableLocales", "zh,ta,ml,ko,te,gu,zh,kn,ur,ja"), ("deviceLocale", "en-CA"), ("deviceModel", DEVICE_MODEL), ("deviceName", DEVICE_NAME), ("deviceTypeOverride", DEVICE_TYPE), ("esn", ESN), ("hasGooglePlayServiceOnTenfoot", "true"), ("isSuspendedMode", "false"), ("netflixClientPlatform", "tenfootMDS"), ("nglVersion", "NGL_3"), ("resolution", "720p"), ("secureVLV", "true"), ("suppScripts", "Hant,Tibt,Thai,Taml,Sinh,Orya,Mlym,Laoo,Armn,Geor,Kore,Telu,Beng,*,Hebr,Cyrl,Gujr,Hans,Deva,Guru,Cans,Ethi,Cher,Mymr,Knda,Grek,Latn,Arab,Jpan"), ("swVersion", "UI-release-20260408_44798-gibbon-sapphire-darwinql"), ("ui_trace_tag", "aui-ql"), ("inAppSwVersion", APP_VERSION), ] params.extend([ ("path", '["aui",["appconfig","partnerData","requestContext","userContext"]]'), ("path", '["aui","truths",["project.bao.ui.enabled","tvui.aui.bugsnag.enabled","tvui.aui.clcs.enabled","tvui.aui.improvedPollingModeMismatchCheck.enabled","tvui.aui.partner.bundle.server.driven.tou.enabled","tvui.aui.partner.fullHd.enabled","tvui.aui.preApp.enabled","tvui.aui.showDeviceSupportMenu.enabled","tvui.aui.speech.enabled","tvui.aui.welcomeContentLandingPointer.enabled","tvui.gibbon.aui.enableRouteTransition.enabled","tvui.gibbon.aui.fetchAllTranslationsWithGql","tvui.gibbon.aui.fetchAllTranslationsWithGqlVerboseLogging","tvui.gibbon.aui.flushFontsOnStartup","tvui.gibbon.aui.useNetflixSans"]]'), ("json", "true"), ("method", "get"), ("seed", "0.7002274648406179"), ]) session.headers.clear() session.headers.update( { "User-Agent": HEADERS["User-Agent"], "Accept": "*/*", "Accept-Encoding": "deflate,gzip", "x-netflix.context.sdk-version": "2025.2.3.0", "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "X-Netflix.request.expiry.timeout": "20000", "X-Netflix.Client.Request.Name": "ui/falcorUnclassified", "X-Netflix.Request.Routing": '{"control_tag":"auinqtv","path":"/nq/aui/endpoint/%5E1.0.0-tv/pathEvaluator"}', "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "Referer": aui_referer, } ) url = "https://api-global.netflix.com/aui/pathEvaluator/tv/latest?" + urlencode(params, doseq=True) response = session.get(url, timeout=30) if response.status_code != 200: raise RuntimeError(f"pathEvaluator failed: HTTP {response.status_code} {response.text[:500]}") log.info("pre-mint pathEvaluator OK") except Exception as exc: log.warning("pre-mint pathEvaluator failed: %s", exc) log.info("MSL Widevine key exchange + mintCookies") msl = None try: if not wvd_path.exists(): raise FileNotFoundError(f"Missing WVD file: {wvd_path}") device = WidevineDevice.load(wvd_path) cdm = WidevineCdm.from_device(device) cdm_device = str(wvd_path) cookies_for_handshake = {} nfvdid_cookie = session.cookies.get("nfvdid") if nfvdid_cookie: cookies_for_handshake["nfvdid"] = nfvdid_cookie log.info("Performing MSL Widevine key exchange") session.headers.clear() msl_headers = MSL_TV.build_request_headers( request_name="mintCookies", user_agent=HEADERS["User-Agent"], referer=None, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) keys = MSL_TV.handshake( msl_keys_path=str(MSL_CACHE_PATH), session=session, sender=ESN, cdm=cdm, cdm_device=cdm_device, new_msl=False, cookies=cookies_for_handshake, drm="widevine", endpoint=MSL_HANDSHAKE_ENDPOINT, headers=msl_headers, ) if not keys or not keys.mastertoken: raise RuntimeError("TV_MSL handshake did not return a valid master token") token_data = json.loads(base64.b64decode(keys.mastertoken["tokendata"]).decode("utf-8")) log.info("Mastertoken acquired seq=%d serial=%d", token_data["sequencenumber"], token_data["serialnumber"]) msl = MSL_TV( session=session, keys=keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) if not (msl.keys and msl.keys.mastertoken and msl.keys.encryption and msl.keys.sign): cached_keys = MSL_TV.load_cache_data(MSL_CACHE_PATH) if cached_keys is None: raise RuntimeError("MSL cache is empty or expired and the active MSL instance is unusable") msl = MSL_TV( session=msl.session, keys=cached_keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) msl.session.headers.clear() mint_headers = MSL_TV.build_request_headers( request_name="mintCookies", user_agent=HEADERS["User-Agent"], referer=runtime_referer, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) header, payload = msl.send_message( endpoint=MSL_TV_ENDPOINT, params={}, application_data={ "version": 2, "common": dict(PBO_COMMON), "url": "/mintCookies", "languages": ["en-CA"], "params": {}, }, headers=mint_headers, ) payload_type = type(payload).__name__ parsed_payload = None text_payload = None if isinstance(payload, dict): payload_type = "msl_payload" parsed_payload = payload elif isinstance(payload, list): payload_type = "json_array" parsed_payload = {"items": payload} elif isinstance(payload, str): cleaned = payload.rstrip("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10") try: maybe_json = json.loads(cleaned) except Exception: maybe_json = cleaned text_payload = cleaned if isinstance(maybe_json, dict): parsed_payload = maybe_json payload_type = "text" else: payload_type = "text" key_id = "" if msl.keys.mastertoken: token_data = json.loads(base64.b64decode(msl.keys.mastertoken["tokendata"]).decode("utf-8")) sequence_number = str(token_data.get("sequencenumber", "")) key_id = sequence_number.encode("utf-8").hex() event: Dict[str, Any] = { "_type": "decrypt", "_mslId": msl.message_id, "_timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), "_keyId": key_id, "_iv": None, "_ciphertextLen": None, "_plaintextLen": len(text_payload.encode("utf-8")) if isinstance(text_payload, str) else None, "_dataType": payload_type, } if text_payload is not None: event["_text"] = text_payload if parsed_payload is not None: event["_payload"] = parsed_payload useridtoken = None servicetokens: List[Dict[str, Any]] = [] stack = [parsed_payload if parsed_payload is not None else payload] while stack: current = stack.pop() if isinstance(current, dict): if set(current.keys()) >= {"tokendata", "signature"}: if useridtoken is None: useridtoken = current else: servicetokens.append(current) for child in current.values(): stack.append(child) elif isinstance(current, list): for item in current: stack.append(item) if useridtoken: USER_ID_TOKEN_PATH.write_text(json.dumps(useridtoken, indent=2), encoding="utf-8") log.info("useridtoken saved to %s", USER_ID_TOKEN_PATH) MSL_TRACE.append(event) cookie_names = [cookie.name for cookie in msl.session.cookies] log.info("Cookies after mintCookies: %s", cookie_names) if "NetflixId" not in cookie_names: log.warning("mintCookies did not return NetflixId; payload=%s", str(payload)[:500]) log.info("mintCookies payload type: %s", event.get("_dataType")) log.info("NetflixId: %s", f"{session.cookies.get('NetflixId', 'N/A')[:60]}...") log.info("SecureNetflixId: %s", f"{session.cookies.get('SecureNetflixId', 'N/A')[:60]}...") try: params = [ ("ab_ui_ver", "darwin"), ("application_name", "htmltvui"), ("application_v", APP_VERSION), ("dh", "720"), ("dw", "1280"), ("falcor_server", "0.1.0"), ("materialize", "true"), ("mdxlib_version", "2025.2.3.0"), ("nrdapp_version", "2025.2.3.0"), ("nrdlib_version", "2025.2.3.0"), ("nrdp", "true"), ("revision", "latest"), ("sdk_version", "2025.2.3.0"), ("sw_version", ANDROID_BUILD_FINGERPRINT), ("tag", "latest"), ("ui_sem_ver", "44798.0.0"), ("webapiConfigAppName", "htmltvui"), ("withSize", "true"), ("availableLocales", "zh,ta,ml,ko,te,gu,zh,kn,ur,ja"), ("deviceLocale", "en-CA"), ("deviceModel", DEVICE_MODEL), ("deviceName", DEVICE_NAME), ("deviceTypeOverride", DEVICE_TYPE), ("esn", ESN), ("hasGooglePlayServiceOnTenfoot", "true"), ("isSuspendedMode", "false"), ("netflixClientPlatform", "tenfootMDS"), ("nglVersion", "NGL_3"), ("resolution", "720p"), ("secureVLV", "true"), ("suppScripts", "Hant,Tibt,Thai,Taml,Sinh,Orya,Mlym,Laoo,Armn,Geor,Kore,Telu,Beng,*,Hebr,Cyrl,Gujr,Hans,Deva,Guru,Cans,Ethi,Cher,Mymr,Knda,Grek,Latn,Arab,Jpan"), ("swVersion", "UI-release-20260408_44798-gibbon-sapphire-darwinql"), ("ui_trace_tag", "aui-ql"), ("inAppSwVersion", APP_VERSION), ] params.extend([ ("path", '["aui","unsupportedLanguageImage"]'), ("path", '["aui","countryProps","cross-platform-ui",["cancelBundleUponPartnerPause","preTaxDisclaimerOnPrice","show_kr_footer_disclaimer","show_paid_button_label_when_not_free","signup_tou_checkbox"]]'), ("path", '["aui","countryProps","tvui",["shouldReorderName","showPrivacyStatementText"]]'), ("json", "true"), ("method", "get"), ("seed", "0.6825750436070701"), ]) session.headers.clear() session.headers.update( { "User-Agent": HEADERS["User-Agent"], "Accept": "*/*", "Accept-Encoding": "deflate,gzip", "x-netflix.context.sdk-version": "2025.2.3.0", "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "X-Netflix.request.expiry.timeout": "20000", "X-Netflix.Client.Request.Name": "ui/falcorUnclassified", "X-Netflix.Request.Routing": '{"control_tag":"auinqtv","path":"/nq/aui/endpoint/%5E1.0.0-tv/pathEvaluator"}', "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "Referer": aui_referer, } ) url = "https://api-global.netflix.com/aui/pathEvaluator/tv/latest?" + urlencode(params, doseq=True) response = session.get(url, timeout=30) if response.status_code != 200: raise RuntimeError(f"pathEvaluator failed: HTTP {response.status_code} {response.text[:500]}") log.info("post-mint pathEvaluator OK") except Exception as exc: log.warning("post-mint pathEvaluator failed: %s", exc) try: msl.session.headers.clear() config_headers = MSL_TV.build_request_headers( request_name="config", user_agent=HEADERS["User-Agent"], referer=aui_referer, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) msl.send_message( endpoint=PBO_CONFIG_ENDPOINT, params={}, application_data={"method": "config", "params": {}}, headers=config_headers, ) log.info("pbo_config OK") except Exception as exc: log.warning("pbo_config failed: %s", exc) for request_name, route, referer_to_use in [ ("getPartnerToken", "/getPartnerToken", aui_referer), ("ping", "/ping", runtime_referer), ("getPartnerToken", "/getPartnerToken", runtime_referer), ]: try: if not (msl.keys and msl.keys.mastertoken and msl.keys.encryption and msl.keys.sign): cached_keys = MSL_TV.load_cache_data(MSL_CACHE_PATH) if cached_keys is None: raise RuntimeError("MSL cache is empty or expired and the active MSL instance is unusable") msl = MSL_TV( session=msl.session, keys=cached_keys, message_id=random.randint(0, 2**52), sender=ESN, user_auth=None, drm="widevine", ) msl.session.headers.clear() optional_headers = MSL_TV.build_request_headers( request_name=request_name, user_agent=HEADERS["User-Agent"], referer=referer_to_use, esn=ESN, expiry_timeout=12750, extra_headers={ "Accept-Encoding": "deflate,gzip", "Content-Encoding": "msl_v1", "X-Gibbon-Cache-Control": "no-cache", "X-AllowCompression": "true", "X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)), "X-DeviceModel": quote(DEVICE_MODEL, safe=""), "x-netflix.esn": ESN, }, ) header, payload = msl.send_message( endpoint=MSL_TV_ENDPOINT, params={}, application_data={ "version": 2, "common": dict(PBO_COMMON), "url": route, "languages": ["en-CA"], "params": {}, }, headers=optional_headers, ) payload_type = type(payload).__name__ parsed_payload = None text_payload = None if isinstance(payload, dict): payload_type = "msl_payload" parsed_payload = payload elif isinstance(payload, list): payload_type = "json_array" parsed_payload = {"items": payload} elif isinstance(payload, str): cleaned = payload.rstrip("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10") try: maybe_json = json.loads(cleaned) except Exception: maybe_json = cleaned text_payload = cleaned if isinstance(maybe_json, dict): parsed_payload = maybe_json payload_type = "text" else: payload_type = "text" key_id = "" if msl.keys.mastertoken: token_data = json.loads(base64.b64decode(msl.keys.mastertoken["tokendata"]).decode("utf-8")) sequence_number = str(token_data.get("sequencenumber", "")) key_id = sequence_number.encode("utf-8").hex() event = { "_type": "decrypt", "_mslId": msl.message_id, "_timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), "_keyId": key_id, "_iv": None, "_ciphertextLen": None, "_plaintextLen": len(text_payload.encode("utf-8")) if isinstance(text_payload, str) else None, "_dataType": payload_type, } if text_payload is not None: event["_text"] = text_payload if parsed_payload is not None: event["_payload"] = parsed_payload useridtoken = None servicetokens = [] stack = [parsed_payload if parsed_payload is not None else payload] while stack: current = stack.pop() if isinstance(current, dict): if set(current.keys()) >= {"tokendata", "signature"}: if useridtoken is None: useridtoken = current else: servicetokens.append(current) for child in current.values(): stack.append(child) elif isinstance(current, list): for item in current: stack.append(item) if useridtoken: USER_ID_TOKEN_PATH.write_text(json.dumps(useridtoken, indent=2), encoding="utf-8") log.info("useridtoken saved to %s", USER_ID_TOKEN_PATH) MSL_TRACE.append(event) log.info("PBO route %s completed, payload type=%s", route, event.get("_dataType")) except Exception as exc: log.warning("Optional PBO route %s failed: %s", route, exc) except Exception as exc: log.warning("MSL setup or mintCookies failed: %s", exc) log.warning("Continuing without guaranteed MSL cookies") log.info("Initiating CLCS login session") trace_uuid = str(uuid.uuid4()) session.headers.clear() session.headers.update( { "Language": "en-CA,en-US,en", "User-Agent": HEADERS["User-Agent"], "Accept": "*/*", "Accept-Language": "en-CA,en-US,en", "Accept-Encoding": "deflate,gzip", "Content-Type": "application/json", "Connection": "Keep-Alive", "x-netflix.context.sdk-version": "2025.2.3.0", "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "x-netflix.request.expiry.timeout": "20000", "x-Netflix.context.app-version": "44798.0.0", "x-Netflix.context.cloud-games-enabled": "false", "X-Netflix.context.device-height": "720", "x-Netflix.context.device-image-capability": "scalingFactor=1.0;supportedFormats=jpg,png,astc", "x-Netflix.context.dt": "", "x-Netflix.context.hawkins-version": "5.13.0", "X-Netflix.context.locales": '["en-CA","en-US","en"]', "X-Netflix.context.ui-flavor": "photon", "X-Netflix.request.device-model": quote(DEVICE_MODEL, safe=""), "X-Netflix.request.is-suspended": "false", "x-netflix.request.clcs.bucket": "high", "X-Netflix.request.toplevel.uuid": trace_uuid, "X-Netflix.tracing.cl.userActionId": trace_uuid, "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "X-Netflix.context.operation-name": "clcsLegacyMoneyballInitiateSession", "Referer": aui_referer, } ) cookie_values: List[str] = [] graphql_url = ( "https://nrdp.prod.cloud.netflix.com/graphql" f"?device_type={DEVICE_TYPE}" f"&esn={quote(ESN, safe='')}" f"&o=clcsLegacyMoneyballInitiateSession" ) body = { "extensions": {"persistedQuery": QUERY_IDS["clcsLegacyMoneyballInitiateSession"]}, "operationName": "clcsLegacyMoneyballInitiateSession", "variables": { "action": "", "flow": "tenfootSignUp", "hasGooglePlayService": False, "imageFormat": "ASTC", "inputFields": [], "legacyRequestArguments": REQUEST_ARGS, "mode": "none", "resolutionMode": "TV_720P", "supportedVideoFormat": "mp4", }, } cookie_header_map: Dict[str, str] = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) headers = dict(session.headers) if cookie_header: headers["Cookie"] = cookie_header response = session.post(graphql_url, json=body, headers=headers, timeout=30) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) preferred: Dict[str, Any] = {} for cookie in session.cookies: if cookie.name not in IMPORTANT_COOKIE_NAMES: continue current = preferred.get(cookie.name) score = (cookie.domain == ".netflix.com", cookie.path == "/", bool(cookie.value)) if current is None or score >= current[0]: preferred[cookie.name] = (score, cookie) for cookie in list(session.cookies): winner = preferred.get(cookie.name) if not winner: continue winner_cookie = winner[1] if (cookie.domain, cookie.path, cookie.value) != (winner_cookie.domain, winner_cookie.path, winner_cookie.value): try: session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) except Exception: pass response.raise_for_status() init_data = response.json() if "errors" in init_data: raise RuntimeError(json.dumps(init_data["errors"], indent=2)) flow: Dict[str, str] = {} data = init_data.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow.get("clcsSessionId"): flow["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow.get("flowSessionId"): flow["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow["mode"] = payload.get("mode", "") if payload.get("flow"): flow["flow"] = payload.get("flow", "") if value.get("membershipStatus"): flow["membershipStatus"] = value.get("membershipStatus", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) flow_session_id = flow.get("flowSessionId", "") clcs_session_id = flow.get("clcsSessionId", "") rendition_id = flow.get("renditionId", "") log.info("Flow session: %s", flow_session_id) log.info("CLCS session: %s", clcs_session_id) log.info("Mode: %s | Status: %s", flow.get("mode"), flow.get("membershipStatus")) if not flow_session_id or not clcs_session_id: raise RuntimeError(f"Failed to extract flow/session IDs from response: {json.dumps(init_data)[:800]}") log.info("Navigating to sign-in screen") trace_uuid = str(uuid.uuid4()) session.headers.clear() session.headers.update( { "Language": "en-CA,en-US,en", "User-Agent": HEADERS["User-Agent"], "Accept": "*/*", "Accept-Language": "en-CA,en-US,en", "Accept-Encoding": "deflate,gzip", "Content-Type": "application/json", "Connection": "Keep-Alive", "x-netflix.context.sdk-version": "2025.2.3.0", "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "x-netflix.request.expiry.timeout": "20000", "x-Netflix.context.app-version": "44798.0.0", "x-Netflix.context.cloud-games-enabled": "false", "X-Netflix.context.device-height": "720", "x-Netflix.context.device-image-capability": "scalingFactor=1.0;supportedFormats=jpg,png,astc", "x-Netflix.context.dt": "", "x-Netflix.context.hawkins-version": "5.13.0", "X-Netflix.context.locales": '["en-CA","en-US","en"]', "X-Netflix.context.ui-flavor": "photon", "X-Netflix.request.device-model": quote(DEVICE_MODEL, safe=""), "X-Netflix.request.is-suspended": "false", "x-netflix.request.clcs.bucket": "high", "X-Netflix.request.toplevel.uuid": trace_uuid, "X-Netflix.tracing.cl.userActionId": trace_uuid, "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "X-Netflix.context.operation-name": "clcsLegacyMoneyballSubmit", "Referer": aui_referer, } ) request_args_dict: Dict[str, Any] = {} for arg in REQUEST_ARGS: value = arg["value"] if "stringValue" in value: request_args_dict[arg["name"]] = value["stringValue"] elif "booleanValue" in value: request_args_dict[arg["name"]] = value["booleanValue"] server_state = json.dumps( { "realm": "moneyball", "flow": "tenfootSignUp", "mode": "welcomeContentLanding", "flowSessionId": flow_session_id, "requestArguments": request_args_dict, "clcsSessionId": clcs_session_id, }, separators=(",", ":"), ) cookie_values = [] graphql_url = ( "https://nrdp.prod.cloud.netflix.com/graphql" f"?device_type={DEVICE_TYPE}" f"&esn={quote(ESN, safe='')}" f"&o=clcsLegacyMoneyballSubmit" ) body = { "extensions": {"persistedQuery": QUERY_IDS["clcsLegacyMoneyballSubmit"]}, "operationName": "clcsLegacyMoneyballSubmit", "variables": { "action": "signInAction", "flow": "tenfootSignUp", "flwssn": flow_session_id, "imageFormat": "ASTC", "inputFields": [], "mode": "welcomeContentLanding", "requestArguments": REQUEST_ARGS, "resolutionMode": "TV_720P", "serverState": server_state, }, } cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) headers = dict(session.headers) if cookie_header: headers["Cookie"] = cookie_header response = session.post(graphql_url, json=body, headers=headers, timeout=30) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) preferred = {} for cookie in session.cookies: if cookie.name not in IMPORTANT_COOKIE_NAMES: continue current = preferred.get(cookie.name) score = (cookie.domain == ".netflix.com", cookie.path == "/", bool(cookie.value)) if current is None or score >= current[0]: preferred[cookie.name] = (score, cookie) for cookie in list(session.cookies): winner = preferred.get(cookie.name) if not winner: continue winner_cookie = winner[1] if (cookie.domain, cookie.path, cookie.value) != (winner_cookie.domain, winner_cookie.path, winner_cookie.value): try: session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) except Exception: pass response.raise_for_status() submit_data = response.json() flow2: Dict[str, str] = {} data = submit_data.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow2.get("clcsSessionId"): flow2["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow2["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow2.get("flowSessionId"): flow2["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow2["mode"] = payload.get("mode", "") if payload.get("flow"): flow2["flow"] = payload.get("flow", "") if value.get("membershipStatus"): flow2["membershipStatus"] = value.get("membershipStatus", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) rendition_id = flow2.get("renditionId", rendition_id) log.info("Rendition: %s", rendition_id) nonce = session.cookies.get("netflix-mfa-nonce") if nonce: log.info("netflix-mfa-nonce: %s", f"{nonce[:80]}...") else: log.warning("netflix-mfa-nonce was not present after signInAction") log.info("Using phone / TV code sign-in") text = json.dumps(submit_data, ensure_ascii=False) tvcode_info: Dict[str, str] = {} match = re.search(r'"previousRendezvousCode":"(\d+)"', text) if match: tvcode_info["code"] = match.group(1) else: match = re.search(r'(?= current[0]: preferred[cookie.name] = (score, cookie) for cookie in list(session.cookies): winner = preferred.get(cookie.name) if not winner: continue winner_cookie = winner[1] if (cookie.domain, cookie.path, cookie.value) != (winner_cookie.domain, winner_cookie.path, winner_cookie.value): try: session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) except Exception: pass response.raise_for_status() poll_data = response.json() last_poll_data = poll_data continue_action = None stack = [poll_data] while stack: value = stack.pop() if isinstance(value, dict): server_screen_update = value.get("serverScreenUpdate") if isinstance(server_screen_update, str) and '"action":"continueAction"' in server_screen_update: try: continue_action = json.loads(server_screen_update) break except Exception: pass for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) if continue_action: log.info("Activation detected") break text = json.dumps(poll_data, ensure_ascii=False) updated = "" match = re.search(r'"previousRendezvousCode":"(\d+)"', text) if match: updated = match.group(1) else: match = re.search(r'(?= current[0]: preferred[cookie.name] = (score, cookie) for cookie in list(session.cookies): winner = preferred.get(cookie.name) if not winner: continue winner_cookie = winner[1] if (cookie.domain, cookie.path, cookie.value) != (winner_cookie.domain, winner_cookie.path, winner_cookie.value): try: session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) except Exception: pass response.raise_for_status() login_data = response.json() flow_result: Dict[str, str] = {} data = login_data.get("data", {}) operation_key = next(iter(data.keys()), "") inner = data.get(operation_key, {}) screen = inner.get("screen", inner) if isinstance(inner, dict) else {} stack = [screen] while stack: value = stack.pop() if isinstance(value, dict): tracking_info = value.get("trackingInfo") if isinstance(tracking_info, str) and tracking_info: try: tracking = json.loads(tracking_info) except Exception: tracking = {} if tracking.get("clcsSessionId") and not flow_result.get("clcsSessionId"): flow_result["clcsSessionId"] = tracking.get("clcsSessionId", "") if tracking.get("clcsRenditionId"): flow_result["renditionId"] = tracking.get("clcsRenditionId", "") payload_json = value.get("payloadJson") if isinstance(payload_json, str) and payload_json: try: payload = json.loads(payload_json) except Exception: payload = {} if payload.get("flwssn") and not flow_result.get("flowSessionId"): flow_result["flowSessionId"] = payload.get("flwssn", "") if payload.get("mode"): flow_result["mode"] = payload.get("mode", "") if payload.get("flow"): flow_result["flow"] = payload.get("flow", "") if value.get("membershipStatus"): flow_result["membershipStatus"] = value.get("membershipStatus", "") for child in value.values(): stack.append(child) elif isinstance(value, list): for item in value: stack.append(item) membership = flow_result.get("membershipStatus", "") log.info("Membership: %s", membership) if membership != "CURRENT_MEMBER": log.warning("Expected CURRENT_MEMBER, got: %s", membership) errors = login_data.get("errors") if errors: log.warning("Structured errors: %s", json.dumps(errors, ensure_ascii=False)) log.warning("Response: %s", json.dumps(login_data)[:1500]) if membership == "CURRENT_MEMBER" and session.cookies.get("NetflixId") and not session.cookies.get("gsid"): log.info("Fetching post-login gsid cookie") trace_uuid = str(uuid.uuid4()) session.headers.clear() session.headers.update( { "Language": "en-CA,en-US,en", "User-Agent": HEADERS["User-Agent"], "Accept": "*/*", "Accept-Language": "en-CA,en-US,en", "Accept-Encoding": "deflate,gzip", "Content-Type": "application/json", "Connection": "Keep-Alive", "x-netflix.context.sdk-version": "2025.2.3.0", "X-Netflix.request.id": "".join(random.choice("0123456789ABCDEF") for _ in range(32)), "X-Netflix.Request.Client.Context": '{"canvas":"OTHER","feature":"OTHER","appView":"appLoading","appstate":"foreground","reason":"unknown"}', "X-Gibbon-Cache-Control": "no-cache", "x-netflix.request.expiry.timeout": "20000", "x-Netflix.context.app-version": "44798.0.0", "x-Netflix.context.cloud-games-enabled": "false", "X-Netflix.context.device-height": "720", "x-Netflix.context.device-image-capability": "scalingFactor=1.0;supportedFormats=jpg,png,astc", "x-Netflix.context.dt": "", "x-Netflix.context.hawkins-version": "5.13.0", "X-Netflix.context.locales": '["en-CA","en-US","en"]', "X-Netflix.context.ui-flavor": "photon", "X-Netflix.request.device-model": quote(DEVICE_MODEL, safe=""), "X-Netflix.request.is-suspended": "false", "x-netflix.request.clcs.bucket": "high", "X-Netflix.request.toplevel.uuid": trace_uuid, "X-Netflix.tracing.cl.userActionId": trace_uuid, "x-netflix.client.last-interacted-days": "0", "X-Netflix.Request.NonJson.Headers": "true", "x-netflix.client.netjs.version": "3.0.5", "X-Netflix.request.attempt": "1", "X-Netflix.context.operation-name": "useNavItemsQuery", "Referer": runtime_referer, } ) cookie_values = [] graphql_url = "https://nrdp.prod.cloud.netflix.com/graphql?o=useNavItemsQuery" body = { "extensions": {"persistedQuery": QUERY_IDS["useNavItemsQuery"]}, "operationName": "useNavItemsQuery", "query": None, "variables": { "artworkCapability": { "artworkResolution": "TVUI_720P", "deviceResolution": "TVUI_720P", "disablePersonalization": False, "supportsAstcFormat": True, "useWebPForAllImages": True, "useWebPForLargeImages": True, } }, } cookie_header_map = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookie_header_map: cookie_header_map[cookie.name] = cookie.value cookie_header = "; ".join(f"{name}={cookie_header_map[name]}" for name in IMPORTANT_COOKIE_NAMES if name in cookie_header_map) headers = dict(session.headers) if cookie_header: headers["Cookie"] = cookie_header try: response = session.post(graphql_url, json=body, headers=headers, timeout=30) raw_headers = getattr(response.raw, "headers", None) if raw_headers is not None and hasattr(raw_headers, "get_all"): cookie_values.extend(raw_headers.get_all("Set-Cookie") or []) header_value = response.headers.get("Set-Cookie") if header_value and header_value not in cookie_values: cookie_values.append(header_value) for raw_cookie in cookie_values: jar = SimpleCookie() try: jar.load(raw_cookie) except Exception: continue for morsel in jar.values(): cookie_domain = morsel["domain"] or None cookie_path = morsel["path"] or "/" if morsel.value == "": try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass continue try: session.cookies.clear(domain=cookie_domain, path=cookie_path, name=morsel.key) except Exception: pass session.cookies.set(morsel.key, morsel.value, domain=cookie_domain, path=cookie_path, secure=bool(morsel["secure"])) if response.ok and session.cookies.get("gsid"): log.info("gsid: %s", f"{session.cookies.get('gsid', 'N/A')[:80]}...") else: log.warning("Post-login nav bootstrap did not produce gsid") except Exception as exc: log.warning("Post-login gsid fetch failed: %s", exc) preferred = {} for cookie in session.cookies: if cookie.name not in IMPORTANT_COOKIE_NAMES: continue current = preferred.get(cookie.name) score = (cookie.domain == ".netflix.com", cookie.path == "/", bool(cookie.value)) if current is None or score >= current[0]: preferred[cookie.name] = (score, cookie) for cookie in list(session.cookies): winner = preferred.get(cookie.name) if not winner: continue winner_cookie = winner[1] if (cookie.domain, cookie.path, cookie.value) != (winner_cookie.domain, winner_cookie.path, winner_cookie.value): try: session.cookies.clear(domain=cookie.domain, path=cookie.path, name=cookie.name) except Exception: pass cookies: Dict[str, str] = {} for cookie in session.cookies: if cookie.name in IMPORTANT_COOKIE_NAMES and cookie.value and cookie.name not in cookies: cookies[cookie.name] = cookie.value if membership == "CURRENT_MEMBER" and "NetflixId" in cookies: log.info("LOGIN SUCCESSFUL") log.info("NetflixId: %s", f"{cookies['NetflixId'][:80]}...") log.info("SecureNetflixId: %s", f"{cookies.get('SecureNetflixId', 'N/A')[:80]}...") log.info("nfvdid: %s", f"{cookies.get('nfvdid', 'N/A')[:80]}...") log.info("gsid: %s", f"{cookies.get('gsid', 'N/A')[:80]}...") log.info("netflix-mfa-nonce: %s", f"{cookies.get('netflix-mfa-nonce', 'N/A')[:80]}...") else: log.error("LOGIN FAILED") if membership != "CURRENT_MEMBER": log.error("Reason: membership status is '%s' (expected CURRENT_MEMBER)", membership) if "NetflixId" not in cookies: log.error("No NetflixId cookie received") log.error("Cookies present: %s", [cookie.name for cookie in session.cookies]) NETFLIX_COOKIES_PATH.write_text(json.dumps(cookies, indent=2), encoding="utf-8") result = { "code": code, "cookies": cookies, "session": session, "flow_session_id": flow_session_id, "clcs_session_id": clcs_session_id, "response": login_data, "poll_response": last_poll_data, "useridtoken_path": str(USER_ID_TOKEN_PATH) if USER_ID_TOKEN_PATH.exists() else None, } log.info("Cookies saved to %s", NETFLIX_COOKIES_PATH.name) if result["useridtoken_path"]: log.info("useridtoken saved to %s", Path(result["useridtoken_path"]).name) else: log.info("useridtoken was not observed during this run") # ====================================================================== # WEB # ====================================================================== def run_web(new_msl: bool = False, no_verify: bool = False, recaptcha_token: str = ''): log = logging.getLogger('netflix_web_login') from typing import Any, Dict, Optional BASE_DIR = Path(__file__).resolve().parent OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache_web.json" PRELOGIN_COOKIES_PATH = OUTPUT_DIR / "netflix_prelogin_cookies.json" AUTH_COOKIES_PATH = OUTPUT_DIR / "netflix_auth_cookies.json" NETFLIX_HOME_URL = "https://www.netflix.com/" NETFLIX_CANONICAL_URL = "https://netflix.com/" GRAPHQL_URL = "https://web.prod.cloud.netflix.com/graphql" LOGIN_URL = "https://www.netflix.com/login" BROWSE_URL = "https://www.netflix.com/browse" MSL_ALE_ENDPOINT = "https://www.netflix.com/nq/msl_v1/nrdjs/pbo_tokens/%5E1.0.0/router" USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/146.0.0.0 Safari/537.36" ) CLIENT_VERSION = "6.135.459.031" APP_VERSION = "ve300d66c" HAWKINS_VERSION = "5.16.0" UI_FLAVOR = "akira" MSL_ESN = f"NFCDCH-02-{''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(32))}" REQUEST_CLIENT_CONTEXT = '{"appstate":"foreground"}' RECAPTCHA_SITE_KEY = "6Lf8hrcUAAAAAIpQAFW2VFjtiYnThOjZOA5xvLyR" QUERY_IDS = { "MembershipStatus": {"id": "3f50f3b3-fff8-48c0-bbd3-5fa2cb04b3c1", "version": 102}, "CLCSScreenUpdate": {"id": "1c276cdf-caef-49cf-b38e-384972c2b47e", "version": 102}, "CLCSSendFeedback": {"id": "079b2271-196b-4edd-b65c-e9439b22e305", "version": 102}, "CLCSInterstitialProfileGate": {"id": "b6e10c7d-0e6f-4921-83b5-177995a80d97", "version": 102}, } recaptcha_token = "" verify_tls = True restore_prelogin_cookies = True restore_auth_cookies = False session = requests.Session() session.verify = verify_tls session.headers.update({ "User-Agent": USER_AGENT, "Accept": "*/*", }) if restore_auth_cookies: MSL_WEB.load_cookiejar(session, AUTH_COOKIES_PATH) elif restore_prelogin_cookies: MSL_WEB.load_cookiejar(session, PRELOGIN_COOKIES_PATH) log.info("Bootstrapping anonymous browser session") response = session.get(NETFLIX_CANONICAL_URL, timeout=30, allow_redirects=True) response.raise_for_status() home_response = session.get(NETFLIX_HOME_URL, timeout=30) home_response.raise_for_status() log.info("Sending MembershipStatus probe") operation_name = "MembershipStatus" variables = {} referer = NETFLIX_HOME_URL originating_url = NETFLIX_HOME_URL headers = { "Host": "web.prod.cloud.netflix.com", "Connection": "keep-alive", "x-netflix.request.id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.operation-name": operation_name, "x-netflix.request.originating.url": originating_url, "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.locales": "en-us", "x-netflix.context.ui-flavor": UI_FLAVOR, "x-netflix.request.toplevel.uuid": str(uuid.uuid4()), "x-netflix.request.attempt": "1", "x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT, "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept": "application/json", "Content-Type": "application/json", "Origin": "https://www.netflix.com", "Referer": referer, "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en;q=0.9", "x-netflix.request.client.version": CLIENT_VERSION, "x-netflix.request.client.id": "ui/akiraWeb", } body = { "operationName": operation_name, "variables": variables, "extensions": {"persistedQuery": QUERY_IDS[operation_name]}, } response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30) response.raise_for_status() membership_status_response = response.json() if "errors" in membership_status_response: raise RuntimeError(json.dumps(membership_status_response["errors"], indent=2)) log.info("Fetching login page and extracting screen context") response = session.get(LOGIN_URL, timeout=30) response.raise_for_status() login_html = response.text clcs_session_id = None clcs_patterns = [ r'"clcsSessionId"\s*:\s*"([0-9a-f\-]{36})"', r'\\"clcsSessionId\\"\s*:\s*\\"([0-9a-f\-]{36})\\"', r'"serverState"\s*:\s*"[^\"]*clcsSessionId\\":\\"([0-9a-f\-]{36})', r'"trackingInfo"\s*:\s*"[^\"]*clcsSessionId\\":\\"([0-9a-f\-]{36})', r'"sessionId"\s*:\s*"([0-9a-f\-]{36})"', ] for pattern in clcs_patterns: match = re.search(pattern, login_html) if match: clcs_session_id = match.group(1) break if not clcs_session_id: raise RuntimeError("Could not extract clcsSessionId from the login page HTML") rendition_id = None rendition_patterns = [ r'"renditionId"\s*:\s*"([0-9a-f\-]{36})"', r'\\"renditionId\\"\s*:\s*\\"([0-9a-f\-]{36})\\"', ] for pattern in rendition_patterns: match = re.search(pattern, login_html) if match: rendition_id = match.group(1) break if not rendition_id: raise RuntimeError("Could not extract the initial renditionId from the login page HTML") screen_name = "IDENTIFICATION" screen_name = "PASSWORD_LOGIN" log.info("Submitting password step directly to PASSWORD_LOGIN") session_context: Dict[str, Any] = { "session-breadcrumbs": {"funnel_name": "loginWeb"}, } session_context.update({ "login.navigationSettings": {"hideOtpToggle": True}, }) full_server_state = { "realm": "growth", "name": "PASSWORD_LOGIN", "clcsSessionId": clcs_session_id, "sessionContext": session_context, } full_screen_update = { "realm": "custom", "name": "growthLoginByPassword", "metadata": {"recaptchaSiteKey": RECAPTCHA_SITE_KEY}, "loggingAction": "Submitted", "loggingCommand": "SubmitCommand", "referrerRenditionId": rendition_id, } full_variables = { "format": "HTML", "imageFormat": "PNG", "locale": "en-US", "serverState": json.dumps(full_server_state, separators=(",", ":")), "serverScreenUpdate": json.dumps(full_screen_update, separators=(",", ":")), "inputFields": [ {"name": "password", "value": {"stringValue": PASSWORD}}, {"name": "userLoginId", "value": {"stringValue": EMAIL}}, {"name": "countryCode", "value": {"stringValue": "1"}}, {"name": "countryIsoCode", "value": {"stringValue": "US"}}, {"name": "recaptchaResponseTime", "value": {"intValue": 445}}, {"name": "recaptchaResponseToken", "value": {"stringValue": recaptcha_token}}, ], } try: operation_name = "CLCSScreenUpdate" referer = NETFLIX_HOME_URL originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(full_server_state, separators=(',', ':')))}" headers = { "Host": "web.prod.cloud.netflix.com", "Connection": "keep-alive", "x-netflix.request.id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.operation-name": operation_name, "x-netflix.request.originating.url": originating_url, "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.locales": "en-us", "x-netflix.context.ui-flavor": UI_FLAVOR, "x-netflix.request.toplevel.uuid": str(uuid.uuid4()), "x-netflix.request.attempt": "1", "x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT, "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept": "application/json", "Content-Type": "application/json", "Origin": "https://www.netflix.com", "Referer": referer, "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en;q=0.9", "x-netflix.request.client.version": CLIENT_VERSION, "x-netflix.request.client.id": "ui/akiraWeb", } body = { "operationName": operation_name, "variables": full_variables, "extensions": {"persistedQuery": QUERY_IDS[operation_name]}, } response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30) response.raise_for_status() login_response = response.json() if "errors" in login_response: raise RuntimeError(json.dumps(login_response["errors"], indent=2)) except Exception as exc: log.warning("Full PASSWORD_LOGIN submit failed, retrying with minimal payload: %s", exc) session_context = { "session-breadcrumbs": {"funnel_name": "loginWeb"}, } minimal_server_state = { "realm": "growth", "name": "PASSWORD_LOGIN", "clcsSessionId": clcs_session_id, "sessionContext": session_context, } minimal_screen_update = { "realm": "custom", "name": "growthLoginByPassword", } minimal_variables = { "format": "HTML", "imageFormat": "PNG", "locale": "en-US", "serverState": json.dumps(minimal_server_state, separators=(",", ":")), "serverScreenUpdate": json.dumps(minimal_screen_update, separators=(",", ":")), "inputFields": [ {"name": "userLoginId", "value": {"stringValue": EMAIL}}, {"name": "password", "value": {"stringValue": PASSWORD}}, ], } operation_name = "CLCSScreenUpdate" referer = NETFLIX_HOME_URL originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(minimal_server_state, separators=(',', ':')))}" headers = { "Host": "web.prod.cloud.netflix.com", "Connection": "keep-alive", "x-netflix.request.id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.operation-name": operation_name, "x-netflix.request.originating.url": originating_url, "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.locales": "en-us", "x-netflix.context.ui-flavor": UI_FLAVOR, "x-netflix.request.toplevel.uuid": str(uuid.uuid4()), "x-netflix.request.attempt": "1", "x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT, "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept": "application/json", "Content-Type": "application/json", "Origin": "https://www.netflix.com", "Referer": referer, "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en;q=0.9", "x-netflix.request.client.version": CLIENT_VERSION, "x-netflix.request.client.id": "ui/akiraWeb", } body = { "operationName": operation_name, "variables": minimal_variables, "extensions": {"persistedQuery": QUERY_IDS[operation_name]}, } response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30) response.raise_for_status() login_response = response.json() if "errors" in login_response: raise RuntimeError(json.dumps(login_response["errors"], indent=2)) response_text = json.dumps(login_response, ensure_ascii=False) screen_names = re.findall(r'"name":"([A-Z_]+)"', response_text) rendition_ids = re.findall(r'"renditionId":"([0-9a-f\-]{36})"', response_text) clcs_match = re.search(r'"clcsSessionId":"([0-9a-f\-]{36})"', response_text) next_clcs_session_id = clcs_match.group(1) if clcs_match else clcs_session_id next_screen_name = screen_names[-1] if screen_names else "PASSWORD_LOGIN" next_rendition_id = rendition_ids[-1] if rendition_ids else rendition_id feedback_payload = None effect = login_response.get("data", {}).get("result", {}).get("effect", {}) nodes = effect.get("nodes", []) if isinstance(effect, dict) else [] for node in nodes: if node.get("__typename") == "CLCSSendFeedback" and node.get("serverFeedback"): feedback_payload = json.loads(node["serverFeedback"]) break if feedback_payload: log.info("Sending CLCSSendFeedback after successful login") session_context = { "session-breadcrumbs": {"funnel_name": "loginWeb"}, } session_context.update({ "login.navigationSettings": {"hideOtpToggle": True}, }) feedback_server_state = { "realm": "growth", "name": "PASSWORD_LOGIN", "clcsSessionId": next_clcs_session_id, "sessionContext": session_context, } operation_name = "CLCSSendFeedback" feedback_variables = { "inputFields": [], "serverFeedback": json.dumps(feedback_payload, separators=(",", ":")), "serverState": json.dumps(feedback_server_state, separators=(",", ":")), } referer = NETFLIX_HOME_URL originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(feedback_server_state, separators=(',', ':')))}" headers = { "Host": "web.prod.cloud.netflix.com", "Connection": "keep-alive", "x-netflix.request.id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.operation-name": operation_name, "x-netflix.request.originating.url": originating_url, "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.locales": "en-us", "x-netflix.context.ui-flavor": UI_FLAVOR, "x-netflix.request.toplevel.uuid": str(uuid.uuid4()), "x-netflix.request.attempt": "1", "x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT, "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept": "application/json", "Content-Type": "application/json", "Origin": "https://www.netflix.com", "Referer": referer, "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en;q=0.9", "x-netflix.request.client.version": CLIENT_VERSION, "x-netflix.request.client.id": "ui/akiraWeb", } body = { "operationName": operation_name, "variables": feedback_variables, "extensions": {"persistedQuery": QUERY_IDS[operation_name]}, } response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30) response.raise_for_status() feedback_response = response.json() if "errors" in feedback_response: raise RuntimeError(json.dumps(feedback_response["errors"], indent=2)) else: log.info("No post-login feedback payload was found") log.info("Opening /browse to finalize the authenticated web session") session_context = { "session-breadcrumbs": {"funnel_name": "loginWeb"}, } session_context.update({ "login.navigationSettings": {"hideOtpToggle": True}, }) browse_server_state = { "realm": "growth", "name": "PASSWORD_LOGIN", "clcsSessionId": next_clcs_session_id, "sessionContext": session_context, } browse_originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(browse_server_state, separators=(',', ':')))}" response = session.get( BROWSE_URL, headers={ "Referer": browse_originating_url, "Accept-Language": "en-US,en;q=0.9", }, timeout=30, ) response.raise_for_status() log.info("Probing the post-login profile gate") profile_gate_response = None try: operation_name = "CLCSInterstitialProfileGate" variables = {"format": "HTML", "resolutionMode": "WEB_1X"} referer = NETFLIX_HOME_URL originating_url = BROWSE_URL headers = { "Host": "web.prod.cloud.netflix.com", "Connection": "keep-alive", "x-netflix.request.id": "".join(random.choice("0123456789abcdef") for _ in range(32)), "x-netflix.context.operation-name": operation_name, "x-netflix.request.originating.url": originating_url, "x-netflix.context.app-version": APP_VERSION, "x-netflix.context.hawkins-version": HAWKINS_VERSION, "x-netflix.context.locales": "en-us", "x-netflix.context.ui-flavor": UI_FLAVOR, "x-netflix.request.toplevel.uuid": str(uuid.uuid4()), "x-netflix.request.attempt": "1", "x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT, "x-netflix.request.clcs.bucket": "high", "User-Agent": USER_AGENT, "Accept": "application/json", "Content-Type": "application/json", "Origin": "https://www.netflix.com", "Referer": referer, "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en;q=0.9", "x-netflix.request.client.version": CLIENT_VERSION, "x-netflix.request.client.id": "ui/akiraWeb", } body = { "operationName": operation_name, "variables": variables, "extensions": {"persistedQuery": QUERY_IDS[operation_name]}, } response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30) response.raise_for_status() profile_gate_response = response.json() if "errors" in profile_gate_response: raise RuntimeError(json.dumps(profile_gate_response["errors"], indent=2)) except Exception as exc: log.warning("Profile gate probe failed: %s", exc) log.info("Attempting post-login ALE provision") ale_response = None try: final_cookies = MSL_WEB.cookiejar_to_ordered_dict(session.cookies) req_id = "".join(random.choice("0123456789abcdef") for _ in range(32)) endpoint = ( f"{MSL_ALE_ENDPOINT}?reqAttempt=1&reqName=aleProvision&reqId={req_id}" f"&clienttype={UI_FLAVOR}&uiversion={APP_VERSION}&browsername=chrome" f"&browserversion=146.0.0.0&osname=windows&osversion=10.0" ) headers = MSL_WEB.build_request_headers( request_name="aleProvision", user_agent=USER_AGENT, referer=BROWSE_URL, esn=MSL_ESN, extra_headers={ "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en;q=0.9", }, ) ale_response = MSL_WEB.handshake( msl_keys_path=MSL_CACHE_PATH, session=session, sender=MSL_ESN, new_msl=False, cookies=final_cookies, endpoint=endpoint, headers=headers, ) except Exception as exc: log.error("ALE provision failed: %s", exc) auth_cookies = MSL_WEB.cookiejar_to_ordered_dict(session.cookies) AUTH_COOKIES_PATH.write_text(json.dumps(auth_cookies, indent=2), encoding="utf-8") result = { "auth_cookies": auth_cookies, } print(json.dumps(result, indent=2)) # ====================================================================== # MGK (Model Group Key) # ====================================================================== def run_mgk(kpekph_path: Optional[Path], esnid: str, new_msl: bool = False): log = logging.getLogger('netflix_mgk_login') BASE_DIR = Path(__file__).resolve().parent OUTPUT_DIR = BASE_DIR / "output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache_mgk.json" AUTH_COOKIES_PATH = OUTPUT_DIR / "netflix_auth_cookies_mgk.json" DEVICE_TYPE = "NFANDROID2-PRV-NVIDIASHIELDANDROIDTV2019" ESN = esnid session = requests.Session() session.verify = True session.headers.update({ "User-Agent": MSL_MGK.DEFAULT_USER_AGENT, "Accept": "*/*", }) log.info("Starting MSL MGK handshake with ESN: %s", ESN) handshake_headers = MSL_MGK.build_request_headers( request_name="mintCookies", esn=ESN, expiry_timeout=12750, ) msl_client = MSL_MGK.handshake( session=session, sender=ESN, kpekph_path=str(kpekph_path) if kpekph_path else None, msl_keys_path=str(MSL_CACHE_PATH), cookies=None, headers=handshake_headers, new_msl=new_msl, ) log.info("MGK handshake completed successfully") user_auth = UserAuthentication.EmailPassword(EMAIL, PASSWORD).__dict__ manifest_endpoint, manifest_params = MSL_MGK.manifest_request_defaults() manifest_headers = MSL_MGK.build_request_headers( request_name="licensedManifest", esn=ESN, expiry_timeout=12750, ) log.info("Sending authenticated MSL request with EMAIL_PASSWORD user auth") try: header, payload = msl_client.send_message( endpoint=manifest_endpoint, params=manifest_params, application_data={}, userauthdata=user_auth, headers=manifest_headers, ) except Exception: log.exception("MGK authenticated request failed") sys.exit(1) auth_cookies = {} for cookie in session.cookies: auth_cookies[cookie.name] = cookie.value try: AUTH_COOKIES_PATH.write_text(json.dumps(auth_cookies, indent=2), encoding="utf-8") log.info("Authentication cookies saved to: %s", AUTH_COOKIES_PATH) except Exception: log.exception("Failed to save cookies") sys.exit(1) result = { "auth_cookies": auth_cookies, "header": header, "payload": payload, } log.info("MGK login succeeded") print(json.dumps(result, indent=2, default=str)) # =========================================================================== # ENTRY POINT # =========================================================================== def main(): parser = argparse.ArgumentParser(description="Netflix MSL multi-platform login") parser.add_argument("--platform", required=True, choices=["android", "ios", "tv", "tv_otp", "web", "mgk"], help="Target platform") parser.add_argument("--wvd", type=Path, help="Path to Widevine .wvd device file") parser.add_argument("--kpekph", type=Path, default=None, help="Path to KpeKph file (mgk platform); auto-discovered if omitted") parser.add_argument("--esnid", type=str, help="ESN identity string (mgk platform)") parser.add_argument("--new-msl", action="store_true", help="Force new MSL key exchange") parser.add_argument("--no-verify", action="store_true", help="Skip TLS verification") args = parser.parse_args() if args.platform == "android": if not args.wvd: parser.error("--wvd is required for android platform") run_android(wvd_path=args.wvd, new_msl=args.new_msl, no_verify=args.no_verify) elif args.platform == "ios": if not args.wvd: parser.error("--wvd is required for ios platform") run_ios(wvd_path=args.wvd, new_msl=args.new_msl, no_verify=args.no_verify) elif args.platform == "tv": if not args.wvd: parser.error("--wvd is required for tv platform") run_tv(wvd_path=args.wvd, new_msl=args.new_msl, no_verify=args.no_verify) elif args.platform == "tv_otp": if not args.wvd: parser.error("--wvd is required for tv_otp platform") run_tv_otp(wvd_path=args.wvd, new_msl=args.new_msl, no_verify=args.no_verify) elif args.platform == "web": run_web(new_msl=args.new_msl, no_verify=args.no_verify) elif args.platform == "mgk": if not args.esnid: parser.error("--esnid is required for mgk platform") run_mgk(kpekph_path=args.kpekph, esnid=args.esnid, new_msl=args.new_msl) if __name__ == "__main__": if os.name == "nt": os.system('cls') else: os.system('clear') main()