feat(netflix): support templated Android ESN generation

Add support for `{randomchar_N}` placeholders in Netflix Android `esn_map` values and generate those segments at runtime. Reuse a cached ESN only when it matches the derived template pattern, is Android-typed, and is not expired; otherwise regenerate and refresh the cache.

This keeps static ESN mappings working as before while enabling dynamic ESN templates (e.g., system_id `7110`) to avoid fixed identifiers and keep ESNs valid per template.
This commit is contained in:
kenzuya
2026-03-10 14:58:08 +07:00
parent 1244141df2
commit 9c7af72cad
2 changed files with 33 additions and 9 deletions

View File

@@ -708,20 +708,44 @@ class Netflix(Service):
def get_esn(self):
if self.cdm.device_type == DeviceTypes.ANDROID:
try:
# Use ESN map from config.yaml instead of generating a new one
esn = self.config["esn_map"][self.cdm.system_id]
esn_template = self.config["esn_map"][self.cdm.system_id]
except KeyError:
self.log.error(f"ESN mapping not found for system_id: {self.cdm.system_id}")
raise Exception(f"ESN mapping not found for system_id: {self.cdm.system_id}")
esn_value = {
'esn': esn,
'type': self.cdm.device_type
}
cached_esn = self.esn.data.get("esn") if isinstance(self.esn.data, dict) else self.esn.data
cached_type = self.esn.data.get("type") if isinstance(self.esn.data, dict) else None
if cached_esn != esn or cached_type != DeviceTypes.ANDROID or (hasattr(self.esn, "expired") and self.esn.expired):
self.esn.set(esn_value, expiration=1 * 60 * 60)
cache_expired = hasattr(self.esn, "expired") and self.esn.expired
randomchar_pattern = r"\{randomchar_(\d+)\}"
if re.search(randomchar_pattern, esn_template):
esn_regex = "^" + re.sub(
r"\\\{randomchar_(\d+)\\\}",
lambda match: rf"[A-Z0-9]{{{match.group(1)}}}",
re.escape(esn_template)
) + "$"
if cached_type == DeviceTypes.ANDROID and isinstance(cached_esn, str) and re.match(esn_regex, cached_esn) and not cache_expired:
esn = cached_esn
else:
self.log.info("Generating Android ESN from configured randomchar template")
esn = re.sub(
randomchar_pattern,
lambda match: "".join(random.choice("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(int(match.group(1)))),
esn_template
)
self.esn.set({
'esn': esn,
'type': self.cdm.device_type
}, expiration=1 * 60 * 60)
else:
esn = esn_template
esn_value = {
'esn': esn,
'type': self.cdm.device_type
}
if cached_esn != esn or cached_type != DeviceTypes.ANDROID or cache_expired:
self.esn.set(esn_value, expiration=1 * 60 * 60)
else:
ESN_GEN = "".join(random.choice("0123456789ABCDEF") for _ in range(30))
generated_esn = f"NFCDIE-03-{ESN_GEN}"

View File

@@ -22,7 +22,7 @@ esn_map:
8131: "HISETVK84500000000000000000000000007401422"
22590: "NFANDROID1-PXA-P-L3-XIAOMM2102J20SG-22590-020NTB086HJPGG70MDDMR0306MR0NNO5G3DJGFCKS9HJF58ER9QA21VFG4I0246JRN6TF16L9I627EPK708SH42UUMG1ASFVG20F3"
12063: "NFANDROID1-PRV-P-SHENZHENKTC-49B1U-12063-2PAENERYJWY35H7F24163TMUCBBA4VRHQ2XZX4OBU4MUTKYFW50BMFBVGTUMN6IM0"
7110: "NFANDROID1-PRV-P-MSD6886602GUHDANDROIDTV-HISENHISMARTTV-A4-7110-5EAE417AE3DB234B5FFC4EFC289A1B11D4475CC5949186C83F4C3D20FF203972"
7110: "NFANDROID1-PRV-P-MSD6886602GUHDANDROIDTV-HISENHISMARTTV-A4-7110-{randomchar_64}"
16401: "NFANDROID1-PRV-P-MSD6886602GUHDANDROIDTV-HISENHISMARTTV-A4-16401-FA2CF15C2E3A00BDDC3B6811C210893F0CD2C062471A62C2A0DD8C28BAE8DF42"
endpoints:
website: "https://www.netflix.com/nq/website/memberapi/{build_id}/pathEvaluator"