diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
index 4bc8822a6..0c86e2e01 100644
--- a/resources/language/resource.language.en_gb/strings.po
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -1225,3 +1225,11 @@ msgstr ""
msgctxt "#30735"
msgid "About Netflix add-on"
msgstr ""
+
+msgctxt "#30736"
+msgid "Automatically generates new ESNs (workaround for 540p limit)"
+msgstr ""
+
+msgctxt "#30737"
+msgid "WARNING: Do not use the original device ESN of the official app."
+msgstr ""
diff --git a/resources/lib/kodi/ui/xmldialog_esnwidevine.py b/resources/lib/kodi/ui/xmldialog_esnwidevine.py
index 83e68ff31..8d5942bb4 100644
--- a/resources/lib/kodi/ui/xmldialog_esnwidevine.py
+++ b/resources/lib/kodi/ui/xmldialog_esnwidevine.py
@@ -7,6 +7,8 @@
SPDX-License-Identifier: MIT
See LICENSES/MIT.md for more information.
"""
+import time
+
import xbmc
import xbmcgui
import xbmcvfs
@@ -55,6 +57,8 @@ def onInit(self):
self.getControl(40002).setLabel(common.get_local_string(30605).format(WidevineForceSecLev.L3_4445))
# Set the current ESN to Label
self.getControl(30000).setLabel(self.esn)
+ # Set [Auto generate ESN] radio button value
+ self.getControl(40100).setSelected(G.LOCAL_DB.get_value('esn_auto_generate', True))
# Set the current Widevine security level to the radio buttons
self.getControl(self.WV_SECLEV_MAP_BTN[self.wv_force_sec_lev]).setSelected(True)
# Hide force L3 on non-android systems (L1 is currently supported only to android)
@@ -88,6 +92,11 @@ def onClick(self, controlId):
G.LOCAL_DB.set_value('widevine_force_seclev',
self.wv_sec_lev_new or self.wv_force_sec_lev,
TABLE_SESSION)
+ # Reset ESN timestamp to prevent to replace the stored ESN immediately
+ G.LOCAL_DB.set_value('esn_timestamp', int(time.time()))
+ # Update value for auto generate ESN
+ is_checked = self.getControl(40100).isSelected()
+ G.LOCAL_DB.set_value('esn_auto_generate', is_checked)
# Delete manifests cache, to prevent possible problems in relation to previous ESN used
from resources.lib.common.cache_utils import CACHE_MANIFESTS
G.CACHE.clear([CACHE_MANIFESTS])
@@ -105,14 +114,13 @@ def onAction(self, action):
self._revert_changes()
self.close()
- def _esn_checks(self):
- """Sanity checks for custom ESN"""
- esn = self.esn_new or self.esn
+ def _esn_checks(self, esn):
+ """Sanity checks for ESN"""
if self.is_android:
- if not esn.startswith(('NFANDROID1-PRV-', 'NFANDROID2-PRV-')) or len(esn.split('-')) < 5:
+ if not esn.startswith(('NFANDROID1-PRV-', 'NFANDROID2-PRV-')) or esn.count('-') < 5:
return False
else:
- if len(esn.split('-')) != 3 or len(esn) != 40:
+ if esn.count('-') != 3 or len(esn) != 40:
return False
return True
@@ -138,7 +146,7 @@ def _update_esn_label(self):
def _change_esn(self):
esn_custom = ui.ask_for_input(common.get_local_string(30602), self.esn_new or self.esn)
if esn_custom:
- if not self._esn_checks():
+ if not self._esn_checks(esn_custom):
# Wrong custom ESN format type
ui.show_ok_dialog(common.get_local_string(30600), common.get_local_string(30608))
else:
diff --git a/resources/lib/services/nfsession/msl/msl_handler.py b/resources/lib/services/nfsession/msl/msl_handler.py
index 5906d9921..1350dc466 100644
--- a/resources/lib/services/nfsession/msl/msl_handler.py
+++ b/resources/lib/services/nfsession/msl/msl_handler.py
@@ -19,7 +19,7 @@
from resources.lib.common.exceptions import MSLError
from resources.lib.database.db_utils import TABLE_SESSION
from resources.lib.globals import G
-from resources.lib.utils.esn import get_esn, set_esn
+from resources.lib.utils.esn import get_esn, set_esn, regen_esn
from resources.lib.utils.logging import LOG, measure_exec_time_decorator
from .converter import convert_to_dash
from .events_handler import EventsHandler
@@ -94,6 +94,8 @@ def get_manifest(self, viewable_id, challenge, sid):
# When the add-on is installed from scratch or you logout the account the ESN will be empty
if not esn:
esn = set_esn()
+ else:
+ esn = regen_esn(esn)
manifest = self._get_manifest(viewable_id, esn, challenge, sid)
except MSLError as exc:
if 'Email or password is incorrect' in str(exc):
diff --git a/resources/lib/utils/esn.py b/resources/lib/utils/esn.py
index 11ed10c07..dd0acb5fc 100644
--- a/resources/lib/utils/esn.py
+++ b/resources/lib/utils/esn.py
@@ -7,7 +7,8 @@
SPDX-License-Identifier: MIT
See LICENSES/MIT.md for more information.
"""
-from re import sub
+import time
+import re
from resources.lib.database.db_utils import TABLE_SESSION
from resources.lib.globals import G
@@ -51,6 +52,41 @@ def set_website_esn(esn):
G.LOCAL_DB.set_value('website_esn', esn, TABLE_SESSION)
+def regen_esn(esn):
+ """
+ Regenerate the ESN on the basis of the existing one,
+ to preserve possible user customizations,
+ this method will only be executed every 20 hours.
+ """
+ # From the beginning of December 2022 if you are using an ESN for more than about 20 hours
+ # Netflix limits the resolution to 540p. The reasons behind this are unknown, there are no changes on website
+ # or Android apps. Moreover, if you set the full-length ESN of android app on the add-on, also the original app
+ # will be downgraded to 540p without any kind of message.
+ if not G.LOCAL_DB.get_value('esn_auto_generate', True):
+ return esn
+ from resources.lib.common.device_utils import get_system_platform
+ ts_now = int(time.time())
+ ts_esn = G.LOCAL_DB.get_value('esn_timestamp', default_value=0)
+ # When an ESN has been used for more than 20 hours ago, generate a new ESN
+ if ts_esn == 0 or ts_now - ts_esn > 72000:
+ if get_system_platform() == 'android':
+ if esn[-1] == '-':
+ # We have a partial ESN without last 64 chars, so generate and add the 64 chars
+ esn += _create_id64chars()
+ elif re.search(r'-[0-9]+-[A-Z0-9]{64}', esn):
+ # Replace last 64 chars with the new generated one
+ esn = esn[:-64] + _create_id64chars()
+ else:
+ LOG.warn('ESN format not recognized, will be reset with a new ESN')
+ esn = generate_android_esn()
+ else:
+ esn = generate_esn(esn[:-30])
+ set_esn(esn)
+ G.LOCAL_DB.set_value('esn_timestamp', ts_now)
+ LOG.debug('The ESN has been regenerated (540p workaround).')
+ return esn
+
+
def generate_android_esn(wv_force_sec_lev=None):
"""Generate an ESN if on android or return the one from user_data"""
from resources.lib.common.device_utils import get_system_platform
@@ -63,15 +99,25 @@ def generate_android_esn(wv_force_sec_lev=None):
return None
-def generate_esn(prefix=''):
- """Generate a random ESN"""
- # For possibles prefixes see website, are based on browser user agent
- import random
- esn = prefix
+def generate_esn(init_part=None):
+ """
+ Generate a random ESN
+ :param init_part: Specify the initial part to be used e.g. "NFCDCH-02-",
+ if not set will be obtained from the last retrieved from the website
+ :return: The generated ESN
+ """
+ # The initial part of the ESN e.g. "NFCDCH-02-" depends on the web browser used and then the user agent,
+ # refer to website to know all types available.
+ if not init_part:
+ esn_w_split = get_website_esn().split('-', 2)
+ if len(esn_w_split) != 3:
+ raise Exception('Cannot generate ESN due to unexpected website ESN')
+ init_part = '-'.join(esn_w_split[:2]) + '-'
+ esn = init_part
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+ from secrets import choice
for _ in range(0, 30):
- esn += random.choice(possible)
- LOG.debug('Generated random ESN: {}', esn)
+ esn += choice(possible)
return esn
@@ -110,8 +156,9 @@ def _generate_esn_android(props, wv_force_sec_lev):
model = model[:45].strip()
prod = manufacturer + model
- prod = sub(r'[^A-Za-z0-9=-]', '=', prod)
- return 'NFANDROID1-PRV-' + device_category + sec_lev + prod + '-' + system_id + '-'
+ prod = re.sub(r'[^A-Za-z0-9=-]', '=', prod)
+
+ return 'NFANDROID1-PRV-' + device_category + sec_lev + prod + '-' + system_id + '-' + _create_id64chars()
def _generate_esn_android_tv(props, wv_force_sec_lev):
@@ -134,7 +181,7 @@ def _generate_esn_android_tv(props, wv_force_sec_lev):
if not model_group:
model_group = '0'
- model_group = sub(r'[^A-Za-z0-9=-]', '=', model_group)
+ model_group = re.sub(r'[^A-Za-z0-9=-]', '=', model_group)
if len(manufacturer) < 5:
manufacturer += ' '
@@ -142,10 +189,11 @@ def _generate_esn_android_tv(props, wv_force_sec_lev):
model = model[:45].strip()
prod = manufacturer + model
- prod = sub(r'[^A-Za-z0-9=-]', '=', prod)
+ prod = re.sub(r'[^A-Za-z0-9=-]', '=', prod)
_, system_id = _get_drm_info(wv_force_sec_lev)
- return 'NFANDROID2-PRV-' + model_group + '-' + prod + '-' + system_id + '-'
+
+ return 'NFANDROID2-PRV-' + model_group + '-' + prod + '-' + system_id + '-' + _create_id64chars()
def _get_drm_info(wv_force_sec_lev):
@@ -192,3 +240,11 @@ def _get_android_system_props():
except OSError:
LOG.error('Cannot get "getprop" data due to system error.')
return {}
+
+def _create_id64chars():
+ # The Android full length ESN include to the end a hashed ID of 64 chars,
+ # this value is created from the android app by using the Widevine "deviceUniqueId" property value
+ # hashed in various ways, not knowing the correct formula, we create a random value.
+ # Starting from 12/2022 this value is mandatory to obtain HD resolutions
+ from secrets import token_hex
+ return re.sub(r'[^A-Za-z0-9=-]', '=', token_hex(32).upper())
diff --git a/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml b/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml
index 937df892d..5b02fa2f0 100644
--- a/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml
+++ b/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml
@@ -21,7 +21,7 @@
1520
- 490
+ 6000
@@ -75,7 +75,7 @@
10801200
- 400
+ 510buttons/dialogbutton-nofo.pngstretch
@@ -85,7 +85,7 @@
001200
- 400
+ 55030010true10090
@@ -118,18 +118,38 @@
trueFFFFFFFF
+ lefttopfalse
font14
false
-
-
- CHANGE ESN button
+
+
+ Warning to avoid use the original device ESN2006520065
+ 1000
+ 90
+ true
+ false
+
+ red
+ left
+ top
+ false
+ font10
+ false
+
+
+
+ CHANGE ESN button
+ 240
+ 65
+ 240
+ 65300100
@@ -142,15 +162,15 @@
centercenterno
- 40000
+ 4010030011Reset button
- 200
+ 240365
- 200
+ 240365300100
@@ -164,16 +184,16 @@
centercenterno
- 40000
+ 401003001030012Save system info button
- 200
+ 240665
- 200
+ 240665300100
@@ -187,16 +207,45 @@
centercenterno
- 40000
+ 401003001140020
+
+ Radiobutton to auto generate ESN's
+ radiobutton
+ 65
+ 310
+ 920
+ 110
+ true
+ myfocustexture.png
+ mynormaltexture.png
+ buttons/radio-button-on.png
+ buttons/radio-button-on.png
+ buttons/radio-button-off.png
+ buttons/radio-button-off.png
+
+ font12
+ FFFFFFFF
+ red
+ 80FFFFFF
+ left
+ center
+ 4
+ 5
+ false
+ 30010
+ 40000
+ 40020
+
+
Widevine security level
- 310
+ 43065
- 310
+ 430651000110
@@ -214,17 +263,16 @@
Widevine force sec.lev. DISABLEDradiobutton65
- 350
+ 470300110true
- redmyfocustexture.pngmynormaltexture.pngbuttons/radio-button-on-sm.png
- buttons/radio-button-on-sm.png
+ buttons/radio-button-on-sm.pngbuttons/radio-button-off-sm.png
- buttons/radio-button-off-sm.png
+ buttons/radio-button-off-sm.png
font12
FFFFFFFF
@@ -235,24 +283,23 @@
45false
- 30010
+ 4010040001Widevine force sec.lev. L3radiobutton375
- 350
+ 470300110true
- redmyfocustexture.pngmynormaltexture.pngbuttons/radio-button-on-sm.png
- buttons/radio-button-on-sm.png
+ buttons/radio-button-on-sm.pngbuttons/radio-button-off-sm.png
- buttons/radio-button-off-sm.png
+ buttons/radio-button-off-sm.png
font12
FFFFFFFF
@@ -263,7 +310,7 @@
45false
- 30010
+ 401004000040002
@@ -271,17 +318,16 @@
Widevine force sec.lev. L3 (ID 4445)radiobutton685
- 350
+ 470300110true
- redmyfocustexture.pngmynormaltexture.pngbuttons/radio-button-on-sm.png
- buttons/radio-button-on-sm.png
+ buttons/radio-button-on-sm.pngbuttons/radio-button-off-sm.png
- buttons/radio-button-off-sm.png
+ buttons/radio-button-off-sm.png
font12
FFFFFFFF
@@ -292,7 +338,7 @@
45false
- 30010
+ 401004000140020