From c9f062206c0874c5b82d5c3e319b3b78e5e507a8 Mon Sep 17 00:00:00 2001 From: xocko Date: Sat, 7 Nov 2026 12:13:31 +0100 Subject: [PATCH] 1.0.0+kodimov.4: password-less first login from the Movistar network On a new box the home-network login now also finds the account number (accountInfo) and registers the box as a device, so no email, password or account number is needed. The new device id is saved straight away, so a retry reuses it instead of adding another one. Error pages from the new servers (including HTML 401s) are retried. Tested live on 2026-11-07 with a fresh profile; the test devices were deleted afterwards. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- addon.xml | 5 +- .../lib/__pycache__/kodimov.cpython-315.pyc | Bin 13742 -> 17008 bytes .../lib/__pycache__/plugin.cpython-315.pyc | Bin 57263 -> 56823 bytes resources/lib/kodimov.py | 112 ++++++++++++------ resources/lib/plugin.py | 5 +- 6 files changed, 80 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 6b4c979..60da82b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Fork of [Paco8's Movistar+ add-on](https://github.com/Paco8/plugin.video.movista `https://git.anosanube.ovh/xocko/kodi-repo/raw/branch/main/repository.xocko.zip` What it adds: -- **Login from the Movistar home network**, no password. The home-network login gives a token for Movistar's new servers, which is traded once for a classic token; the classic servers renew it every 10 days. It needs the account number (taken from an existing login, or set in the settings) and an already registered device. +- **Login from the Movistar home network**, no email, password or account number. On first open, on a box connected to the Movistar home network, KodiMov+ gets a home-network token, finds the account, registers the box as one device (like a password login does) and trades the token for a classic login, which the classic servers renew every 10 days. Outside the Movistar network, log in once with the email and password (Accounts > Cuenta 1 > Login with username). - **Takes over the login of plugin.video.movistarplus** on first run (same registered device, so no new device is added to the account). - **Hides channels it can't play**: Movistar marks football and premium sports with `QMWatermark`; LALIGA and Liga de Campeones refuse their manifest, while M+ Deportes, DAZN and DAZN Baloncesto play and are kept (`resources/lib/kodimov.py`). - **Extra free channels** in the TV guide list: TVG2 (10) and TeleVigo (15). diff --git a/addon.xml b/addon.xml index 6e49e2a..6e9982c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ @@ -21,7 +21,8 @@ Movistar+ para Kodi, con inicio de sesión automático desde la red de Movistar Fork of Paco8's Movistar+ add-on (plugin.video.movistarplus). Logs in by itself from the Movistar home network, keeps the channel list and numbers up to date, hides the channels that need stronger DRM than Kodi has, adds free Galician channels (TVG2, TeleVigo) and writes a programme guide for every channel. Requires a Movistar+ subscription. Fork del addon Movistar+ de Paco8 (plugin.video.movistarplus). Inicia sesión solo desde la red de Movistar, mantiene la lista y los números de los canales al día, oculta los canales que necesitan una protección que Kodi no tiene añade canales gallegos gratuitos (TVG2, TeleVigo) y genera la guía de programación de todos los canales. Se necesita estar abonado a Movistar+. - 1.0.0+kodimov.3: one-tap TV setup: "Create settings for IPTV Simple" also turns on Movistar channel numbers, daily list reload, and restarts IPTV Simple; fixes invalid XML in that file. + 1.0.0+kodimov.4: first login on a new box needs no password from the Movistar home network: the account is found and the box registered automatically. +1.0.0+kodimov.3: one-tap TV setup: "Create settings for IPTV Simple" also turns on Movistar channel numbers, daily list reload, and restarts IPTV Simple; fixes invalid XML in that file. 1.0.0+kodimov.2: programme guide for every channel (Movistar + TVG2/TeleVigo), TeleVigo on 15, silent background refresh. 1.0.0+kodimov.1: first KodiMov+ release, based on Movistar+ 1.0.0. This addon is not officially commissioned/supported by Movistar+. All product names, logos, and trademarks mentioned in this project are property of their respective owners. diff --git a/resources/lib/__pycache__/kodimov.cpython-315.pyc b/resources/lib/__pycache__/kodimov.cpython-315.pyc index 475e7de6f024d96c1fba0bd2b9c2882754da3a6c..be9b927ec1280cdbea8f3f14be179617b10abeb5 100644 GIT binary patch delta 6118 zcmbtYe@q*BcAxQ&@z}-&8w|!jU<`p^!tX#52qbX`SqKSc0!}WmDCgJ%H~|ltu|pu2 zwR3w?Liet)npF?=PCE8>+oU~dlZus^c5k(NTV;X_K&t|q)Me1PCaF> zYTtLp04dv>j+Ey^D5OuuC{!MmwQyrEBO4v0e^6l_F=ODd@4& zE}#y&3aE2fPJ;z*_oes=1Tyr28WMh%&KxBeFJk3l7g7ZC5 zF2*c;2aRhDF;~JthCeKoqw}*&Pkw5xJT=sFEyrH36@IGx9p16FqH5IOhu1#SPvLmy zTC-tQfe)=E3;$GspAja__Xjm#xDvxu&q<&OsFY)47>)NUUc*_16Z2tS5*okuc*XsC zwb5%3zAUxxSJDcfl2-15PQOte;Wf$J%hHGYi{+ZXM0mKnBHos()2e#4+=XFnxWv=y z4u!^Onop%koU`u#Ps;M2AXhW7R%2n3Pv1Khj$+1R9G?nqjg{UYdZR zHq=^OxF5y^uVR=D^AR<}h-+dMzoSVWr zm3C|jcNZP#lXeS7(`ElnGHB6AE*{3O{i;&89#s%1*PB;Rv)(#CRqo zgevNAvw&Cb5e6#{9?9zYXdoKn$AfH$$r2GJI?V>NBohhobK&R|cM#5m>jdJ~a{W+b zwRvl}Q&1Pisj6;X3tVk2B|RMaK(o5>{L~G&h+W%*~k_GZ~^RMU<^5?=uh2CW*2%;Y+wS492B%i|3Zc7ROe)R=KpHK2f)! zCT@=17)zEmudAE?rvF#^;^qFd-kH(wO6hlH^o=QfW3uVBjK2SozW+%OO8dyn-z(p8^z^`qlv3GPR2J#?UH&?y<}K4WXSRqS-w*DiTdGOk}OY?XA_k^-XQuKum=sM!u-nzRA4^k74z;mjis{7+(Lfiuv7g1)NnOZICx9U*=r66Z7Y%c^ia; z%`%feGAA6^2@i&8F~`^BjbXB!VX{@>!YVL7C9g+d!ER(VtKVi*!A>@f*Tk?>>$Tu3l5J&hdK*IZf?H9E!2Q|v?(TwWl;&oLZ@ zoCNSx|LCcc6vJ^Wr<)B-GE^PU&M?vN48v0as=cMvE&9j^3vMU?ekVgTdW$aflGmlvQ(q$RxvsfR{9ELEIGT0?jVR7(SX+$7W|iHgdvQbtpD7%X2-5 zCqj(mw5RzfYy{^(Q_-SXZQoV22@oVWbnUoS)TkCiNLB-z9EirF95*SoPBYA`yFha8 z1f1Cv8{!bA;ULIkS=9wLG@m7N*7Bm(+zjf` zxIcm3d3gERxEKh2M_|gT4MLZ2Ju#IoU;Sa_a%E!j6<=&ik+wAH02genPk0iMl*+PU zJ__8_jlo})yH;!O#M0#*P-j!B(oKa%eF*vLn#G!>#>K{rt}>;oTqSN7yi@S5@t*Rb zW}~y`QRl#WqqpPde|r9&YQ3i8VRTc8>8a-!uJ_@Iz?V{_$rzj|gL7r_PB>|BrVR%Y zp$&s+$-n4-b1Y-1Nf~NZgBe5pBSZa0VbRj{#p^4fWMNgBtO8laJzFHECX-}GhU`v} z-S$)>UPm1XI3L=sE&2(Yd&+Ufe+^39}L2rngy4E5r*OmzXqtcaj;<9I* zfYCN^y~CteBl&EN=(C4R+J06E5hOn{2*4WPq*9*Qi%&G57C(X!;`1%#Ho105F1TwH zxol^>=p*~ulSi%)kz}zG;AcT7x_aW{!)T=+nI+X!Z7PYo0%l)6Q;xz`0m8FVE3Fc) z7unceTWBPZRbf&j0TgQBE?kMN>A<&{U+EY&Gs zKT+_5jr2qKRMS=aLCOT63a~C8<9r%f^D%1Q9ANem)DLi2x1%okh=gLzh=x^)$Extw zprztqr@W&O&34Nw(fS=_Dd~dkyCmx&mmnd3uxIjeSrx5@9Wg>4RtQ=aK|W=o&2%wc zLRZ6sOkE61mK8CcH@w5cVvc0N9DBQm72TvmbwEFm9VP=XMcw^g{?;IJWp z!s>nce0Lw(IiFq<;j+mh{Bq`oq5YlMvwc*V9pZQH`uN-B{qnX$^e+*zRztIV8jc-z z@Rid5sI?y{U*h!X=nA^BQ6XypnJT(`@+nb1oxJ8Nr{sD$p57%Qi9F!)TV>t+d8Fe< zfkS*+Re&@QghSU@M-w~gI+E8l4q!`R2dE_@2!JegvM3M)pAA43a@*%)*im3UXWXWC zHIfX+LiUiZT=^2Iz&gkv6xAUAZW0S1tqNEaZjh+GhSk{tYYpy`m2=kw-WNWw9V?m) zUlBoxoWcqF?3HmA01aeh3_k^F+f|6}FMm@|(gcKsa;;P(Hgf^sK>($9&@5#{RG8O6 z(BqiNFc07XU{oC&ky66k09a(3lm==#dh?BK;Yc`oED#M) zQ7#Y?^_7G)%2_3(ox+U0T94KYP6v2C929Mo)&Wwv!YaA{sASCfUS^P)g1tDKvAT77|93&W?+!cWt*m z#F){~8~{j7O-)@x&(L)p5;@ETX^bpI2J5w|dfe07>-7zd z4h$Xlj{8oZ6p<9yg{FzwZ7<@EA#xNE5iFUcnaBFh4D|Mmpjla^XPSusf(u+>#wP$2 zO3>;Jn36S1qX&itMvr+$J>nv0>$65r6rkA!_)*Y@pt110vNkT^E`uoN5;HH#lJS=@ zcL3wk7slO23QZ#-f-yqM)w%B@*^sNx!9NA=2dMj&*dqN<%c^HNc9zd!F$squD&^(E zKTw61`!IYNUjBXXSvRofMVQjKAv~vA;+qPKvgN71{C4?j`4gjc#guV$CLNu3O77bJ z()Ke)#@(56cczV93FfK6{-n&g5?HmY*Y(_wrOWyj3vLmc3Z?c4{={DSldeDOS`DS` zd+rQ8w(q-l`LVt8e)D7diCazoMPSA@ZyDQ`Nq}-l|O-Z*W?T(*I`oEpLG`(In zgQRVkEI+ipWfL43Q+3Kzz1o{L)j_M?v|?M=SHH*K+4t_Xdl%MsAJ{OJCaql=t0!gk zd{UdX4kS$jN!`FkiS3s5OGxfDiyA4hPulA546Kv;o{;)vL3^g)V5;EY{ee#|d~_;Z zaAKV-8iaoZ%a_vyyApkyI!td9x~dvFw+PH^UBOrOtyg#4i=|DyHyRSPV5!pl1YqI$ zKR&;rN*kORgDYilC968phJA_SU}fW8;SW`ZEJaDPJ7aE1nOp9arp*VE`U6ST0pU<} z*WMn?WdCvhdlx@&Ckwj)&cV)!hO}g?`%~8a_ldOiaME=6k?t_yH|YFo%PK5a*T+jR zz2)slQd@)mHJAaKzHuD=YB0ax$KX(Zq4n4XN$k^Idrp*Ke_!a?2bBkeM+e1c`)dZf zv41LQ8dNJkSK!d{xe73QqGgEuJLrTY$uIn}#e;t+SX)oxp9)NC?-6vd;95*U!Tl0QR?#<_CFTMV z_{Wu1vau-lbJQiKB5Ki>W%2Q>f|=$Xq4pmL|JK@~{&!#>uQ}WF3gce@>sAasLw_;c z6+%hubO!$7g z)q}i{=y^DF!8o)-9I~E6dT_{84#i*TB9h%RY$!I(9OC{P+7K?}|5^BT`+J2O)@w=3 vvZ=r<5?_KX%`9 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 G3Vs3XQBDm2 delta 1442 zcma)5ZA@EL7`}(zTUJ^M+Tk(LQqC1fatNsNOfE|WzDPW;nt9fJ#&!7ZK^hfQ?& zLpM~$^c~f0WLXCcv$0XRBUMoeZi^WmlY&X1gWwznbS65t#2Ae6oZC_N)06b=InVpN zU(Y%B&k@Dos6zXqHZg%AtA+io;n}WFwFf%lhT@>JV?$CElSLh5)wMH>0djjYAn=JE z22ZIHW1iXvJ>~7HEOMhY>h1~%4L}C26)w~Ggc~} zL{TU`T7``fjL>;Fi(j(~J#JXV7{-ZgvBa9n_|@R;8F15M1eIP>U}h|;h_#@Qme z?P5#QUIBYF_s@9^E~n^GyPf+4(Ip=o+4~s=texg<$@nEw2k)llWoqoiL{yE=>RNl9 zqfx+^bc(AenBWrJI|R4eA=XFMy7B9FQE=~f;(hi;XT3wjJD{y|*8v})A0yhhejp)l zF>`!cm|q{_*H1oLFwI-r@)lI7y{JFl7aum|hfMkYys)Ws%2ayI{9Vq~oN$>fRA!sp z=?IrKOqDgv>CL@a;mqPtX7Oj%>CDn;{pPmv1@*dPg>$Sb%<4j{Zc?9h>do)j!bMIQ z=F&o(#2uMLTt8`|Uy6LN8=+o!8nz}35a2xR>N)t;TL7c*JL&=dQ6q6r9L+&z;JP;r z4N2OrLp0(eZ=vBViExo0W5VeVvWI7oZ8 zAn-r2PP%;h=Qwl;F7#QHmxv2rhWWl!?kd^dlK$%JU{Q~B@T?UnDT0IW&A9*yf@feO zngVGcms=q49cghONrA4wy}=f4kpPL3=ob}85A)JjXc*C3PZtZ&O)v~^AjECM zIeUI2#K>Bqwb_Z?cA>f6?snQ+n;P+5qKid|muS7))O<^Yx<%OEFCY|r$MS!xk3rkD!-oHA z#)@DQ?c)kOHnEdq$>nX7+9$?UNj(HDFUKq7kMy+yd&5O3@_ffBqQFW*#XdL}UP3DX zGleJ~#%DIA&?lGr@OPvbO+(R*_tm46;h@|^xPIf6 z&2j;GBKcj)+Q}-)0*+C({8r>KWTE9dk!PWmu?|}2(veB>&hbd8L0ixtMi`vm`Y&4X BxC8(I diff --git a/resources/lib/kodimov.py b/resources/lib/kodimov.py index 997db50..8b722d2 100644 --- a/resources/lib/kodimov.py +++ b/resources/lib/kodimov.py @@ -46,6 +46,8 @@ EXTRA_EPG_URL = 'https://raw.githubusercontent.com/davidmuma/EPG_dobleM/master/g AUTOLOGIN_URL = 'https://soter-pf.svp.video.telefonicaservices.com/service/login/autologin?deviceId={device}&deviceType=webplayer&_={ts}' SOTER_INITDATA_URL = 'https://soter-pf.sve.video.telefonicaservices.com/service/login/initData?legacy=true' +SOTER_ACCOUNTINFO_URL = 'https://soter-pf.sve.video.telefonicaservices.com/service/login/webplayer/accountInfo?legacy=true' +SOTER_DEVICES_URL = 'https://soter-pf.sve.video.telefonicaservices.com/service/login/devices?deviceType=webplayer&accountId={account}&legacy=true' def is_playable(channel): @@ -93,60 +95,96 @@ def find_account_number(m, setting=''): return (setting or '').strip() -def network_login(m, account_number): - """Password-less login from the Movistar home network. - 1. autologin (only reachable inside Movistar's network) gives a 5-minute - token for the new servers; - 2. their initData trades it for a legacyAccessToken, which the classic - servers this add-on uses accept (and renew for 10 days at a time). - Returns (success, reason).""" +def _autologin(m, device_id): + """5-minute token for the new servers, bound to device_id. Only works from + the Movistar home network. Returns the token or raises.""" try: from urllib.parse import urlsplit except ImportError: from urlparse import urlsplit - from .signature import get_auth - - device_id = (m.cache.load_file('device_id.conf') or '').strip().strip('"') - if not account_number: return False, 'no account number' - if not device_id: return False, 'no registered device' - headers = m.net.headers.copy() headers['Content-Type'] = 'application/json' url = AUTOLOGIN_URL.format(device=device_id, ts=int(time.time() * 1000)) + r = m.net.session.get(url, headers=headers, allow_redirects=False, timeout=(4, 10)) + location = r.headers.get('Location', '') + if r.status_code in (301, 302, 303, 307, 308) and location: + if not urlsplit(location).netloc.endswith('.telefonicaservices.com'): + raise ValueError('unexpected redirect') + h2 = headers.copy() + h2['Origin'] = 'null' + r = m.net.session.get(location, headers=h2, allow_redirects=False, timeout=(4, 10)) + token = r.json().get('access_token') + if not token: raise ValueError('autologin refused') + return token + + +def _soter(m, method, url, token, body=None, attempts=6): + """Signed call to the new servers. They often answer with an HTML error + page (sometimes a 401) for no reason, so retry those.""" + from .signature import get_auth + r = None + for attempt in range(attempts): + h = m.net.headers.copy() + h['Content-Type'] = 'application/json' + h['Accept'] = 'application/json, text/javascript, */*; q=0.01' + h['Authorization'] = get_auth(method, url, token) + try: + r = m.net.session.request(method.upper(), url, headers=h, timeout=(4, 15), + data=json.dumps(body) if body is not None else None) + text = r.content.decode('utf-8', errors='replace') + if not text.lstrip().startswith('<'): + return r.status_code, text + except Exception as e: + LOG('soter {} failed: {}'.format(url.split('?')[0], e)) + LOG('soter {}: attempt {} got an error page ({})'.format(url.split('?')[0], attempt + 1, getattr(r, 'status_code', '?'))) + time.sleep(5) + return getattr(r, 'status_code', 0), '' + + +def network_login(m, account_number=''): + """Password-less login from the Movistar home network. + 1. autologin gives a 5-minute token for the new servers; + 2. on a fresh box: accountInfo gives the account number and the device is + registered (one new device on the account, like a password login); + 3. initData trades a device-bound token for a legacyAccessToken, which the + classic servers this add-on uses accept and renew for 10 days at a time. + Returns (success, reason).""" + import uuid + device_id = (m.cache.load_file('device_id.conf') or '').strip().strip('"') try: - r = m.net.session.get(url, headers=headers, allow_redirects=False, timeout=(4, 10)) - location = r.headers.get('Location', '') - if r.status_code in (301, 302, 303, 307, 308) and location: - if not urlsplit(location).netloc.endswith('.telefonicaservices.com'): - return False, 'unexpected redirect' - h2 = headers.copy() - h2['Origin'] = 'null' - r = m.net.session.get(location, headers=h2, allow_redirects=False, timeout=(4, 10)) - token = r.json().get('access_token') + if not account_number or not device_id: + token = _autologin(m, device_id or uuid.uuid4().hex) + if not account_number: + status, text = _soter(m, 'get', SOTER_ACCOUNTINFO_URL, token) + try: + account_number = json.loads(text)['ofertas'][0]['accountNumber'] + except (ValueError, KeyError, IndexError, TypeError): + return False, 'account not found ({})'.format(status) + if not device_id: + status, text = _soter(m, 'post', SOTER_DEVICES_URL.format(account=account_number), token, attempts=1) + new_id = text.strip().strip('"') + if status not in (200, 201) or len(new_id) != 32: + return False, 'device registration refused ({})'.format(status) + device_id = new_id + # Saved straight away, so a retry reuses it instead of adding another. + m.cache.save_file('device_id.conf', device_id) + LOG('network_login: registered device {}...'.format(device_id[:5])) + token = _autologin(m, device_id) except Exception as e: LOG('network_login: autologin failed: {}'.format(e)) return False, 'not on the Movistar network' - if not token: return False, 'autologin refused' body = {'accountNumber': account_number, 'sessionUserProfile': 0, 'isKidProfile': False, 'deviceId': device_id, 'streamMiscellanea': 'HTTPS', 'deviceManufacturerProduct': 'Firefox', 'streamDRM': 'Widevine', 'streamFormat': 'DASH'} - data = None - for attempt in range(4): - h = headers.copy() - h['Authorization'] = get_auth('post', SOTER_INITDATA_URL, token) - try: - r = m.net.session.post(SOTER_INITDATA_URL, data=json.dumps(body), headers=h, timeout=(4, 15)) - data = r.json() - break - except Exception: - # The new servers sometimes answer with an HTML error page; retry. - LOG('network_login: initData attempt {} failed ({})'.format(attempt + 1, getattr(r, 'status_code', '?'))) - time.sleep(3) - legacy = data.get('legacyAccessToken') if isinstance(data, dict) else None + status, text = _soter(m, 'post', SOTER_INITDATA_URL, token, body) + try: + legacy = json.loads(text).get('legacyAccessToken') + except (ValueError, AttributeError): + legacy = None props = m.get_token_properties(legacy) if legacy else None if not props or props.get('iss') != CLASSIC_ISSUER or props.get('exp', 0) < time.time(): - return False, 'initData refused' + return False, 'initData refused ({})'.format(status) m.save_key_file({'access_token': legacy, 'token_type': 'bearer'}) m.cache.save_file('access_token.conf', legacy) LOG('network_login: OK') diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index d8ecce6..1dc5932 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -663,11 +663,8 @@ def login(): show_notification(addon.getLocalizedString(30166)) # Failed def network_login(): + # The account number and the device are found/registered automatically. account_number = addon.getSetting('account_number') - if not account_number and not m.cache.load_file('account.json') and not m.cache.load_file('tokens.json'): - account_number = input_window(addon.getLocalizedString(30502)) # Account number - if not account_number: return - addon.setSetting('account_number', account_number) # Keeps device_id.conf: clearing the session would register a new device. if m.try_network_login(account_number, force=True): show_notification(addon.getLocalizedString(30503), xbmcgui.NOTIFICATION_INFO) # Logged in