Files
NF-MSL/main.py
T
2026-05-29 17:53:25 +08:00

4493 lines
188 KiB
Python

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"
"&reg=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"
"&reg=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'(?<!\d)(\d{8})(?!\d)', text)
if match:
tvcode_info["code"] = match.group(1)
for key in ("tvLoginRendezvousId", "moneyballSessionUuid"):
found = re.search(rf'"{key}".*?"value":"([^"]+)"', text)
if found:
tvcode_info[key] = found.group(1)
code = tvcode_info.get("code", "")
if not code:
raise RuntimeError("Could not find the TV code in the sign-in response")
log.info("Go to: https://www.netflix.com/tv2")
log.info("Enter code: %s", code)
log.info("Polling for activation every 5 seconds")
continue_action = None
last_poll_data: Dict[str, Any] = {}
while True:
time.sleep(5)
log.info("Polling activation status...")
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": "clcsScreenUpdate",
"Referer": aui_referer,
}
)
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 = []
graphql_url = (
"https://nrdp.prod.cloud.netflix.com/graphql"
f"?device_type={DEVICE_TYPE}"
f"&esn={quote(ESN, safe='')}"
f"&o=clcsScreenUpdate"
)
body = {
"extensions": {"persistedQuery": QUERY_IDS["clcsScreenUpdate"]},
"operationName": "clcsScreenUpdate",
"variables": {
"imageFormat": "PNG",
"resolutionMode": "TV_720P",
"serverScreenUpdate": json.dumps(
{
"realm": "custom",
"metadata": {"pollInterval": 5000, "previousRendezvousCode": code},
"name": "tenfootSignUp.loginRendezvous.polling",
"referrerRenditionId": rendition_id,
},
separators=(",", ":"),
),
"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()
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'(?<!\d)(\d{8})(?!\d)', text)
if match:
updated = match.group(1)
if updated and updated != code:
code = updated
log.info("Code updated: %s", code)
log.info("Completing OTP sign-in")
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": "clcsScreenUpdate",
"Referer": aui_referer,
}
)
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 = []
graphql_url = (
"https://nrdp.prod.cloud.netflix.com/graphql"
f"?device_type={DEVICE_TYPE}"
f"&esn={quote(ESN, safe='')}"
f"&o=clcsScreenUpdate"
)
body = {
"extensions": {"persistedQuery": QUERY_IDS["clcsScreenUpdate"]},
"operationName": "clcsScreenUpdate",
"variables": {
"imageFormat": "PNG",
"inputFields": [],
"resolutionMode": "TV_720P",
"serverScreenUpdate": json.dumps(continue_action, separators=(",", ":")) if continue_action else json.dumps(
{
"realm": "moneyball",
"action": "continueAction",
"loggingAction": "Submitted",
"loggingCommand": "SubmitCommand",
"referrerRenditionId": rendition_id,
},
separators=(",", ":"),
),
"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()
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()