diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b89893f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,26 @@
+VERSION = $(shell ./get_version.py)
+OUTPUT_DIR = plugin.video.movistarplus-$(VERSION)
+OUTPUT_FILE = $(OUTPUT_DIR).zip
+
+install:
+ -install -d $(OUTPUT_DIR)/resources/lib/
+ install -m 644 default.py service.py $(OUTPUT_DIR)/
+ install -m 644 resources/lib/*.py $(OUTPUT_DIR)/resources/lib/
+ install -m 644 resources/*.py $(OUTPUT_DIR)/resources/
+ install -m 644 addon.xml $(OUTPUT_DIR)/
+ install -m 644 *.png $(OUTPUT_DIR)/
+
+ install -m 644 resources/settings.xml $(OUTPUT_DIR)/resources/
+ -install -d $(OUTPUT_DIR)/resources/language/resource.language.en_gb/
+ -install -d $(OUTPUT_DIR)/resources/language/resource.language.es_es/
+ install -m 644 resources/language/resource.language.en_gb/strings.po $(OUTPUT_DIR)/resources/language/resource.language.en_gb/strings.po
+ install -m 644 resources/language/resource.language.es_es/strings.po $(OUTPUT_DIR)/resources/language/resource.language.es_es/strings.po
+
+ zip -9 -r $(OUTPUT_FILE) $(OUTPUT_DIR)/
+
+clean:
+ -rm -rf $(OUTPUT_DIR)/
+ -rm $(OUTPUT_FILE)
+ -rm *.pyo *.pyc
+ -rm resources/lib/*.pyo resources/lib/*.pyc
+
diff --git a/addon.xml b/addon.xml
new file mode 100644
index 0000000..a69ea8d
--- /dev/null
+++ b/addon.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+ video
+
+
+
+
+ Movistar+
+ Movistar+
+ Watch live channels, recordings and video on demand content from Movistar+ in Kodi. Requires a subscription.
+ Vea canales en directo, grabaciones, U7D y TV a la carta de Movistar+ en Kodi. Se necesita estar abonado.
+ 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.
+ Este addon no ha sido realizado ni apoyado por Movistar+. Todos los nombres de productos, logotipos y marcas registradas mencionados en este proyecto son propiedad de sus respectivos dueños.
+ all
+ LGPL-2.1-or-later
+
+
+
+ icon.png
+
+
+
+
+
+
diff --git a/default.py b/default.py
new file mode 100644
index 0000000..7c320f6
--- /dev/null
+++ b/default.py
@@ -0,0 +1,9 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from resources.lib.plugin import run
+
+run()
+
+
diff --git a/get_version.py b/get_version.py
new file mode 100755
index 0000000..aeb9155
--- /dev/null
+++ b/get_version.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+import io
+import re
+
+with io.open('addon.xml', 'r', encoding='utf-8') as handle:
+ text = handle.read()
+ m = re.search(r'movistar.*?version="(.*?)"', text, re.DOTALL)
+ if m:
+ print(m.group(1))
+
+
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..673db83
Binary files /dev/null and b/icon.png differ
diff --git a/resources/__init__.py b/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
new file mode 100644
index 0000000..86e9601
--- /dev/null
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -0,0 +1,258 @@
+# XBMC Media Center language file
+# Addon Name: Orange
+# Addon id: plugin.video.orange
+# Addon version: 0.0.2
+# Addon Provider: Paco8
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: XBMC-Addons\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-12-28 16:47+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgctxt "#30001"
+msgid "Main"
+msgstr ""
+
+msgctxt "#30002"
+msgid "Proxy"
+msgstr ""
+
+msgctxt "#30003"
+msgid "Experimental"
+msgstr ""
+
+msgctxt "#30010"
+msgid "DRM type"
+msgstr ""
+
+msgctxt "#30011"
+msgid "Manifest alteration"
+msgstr ""
+
+msgctxt "#30012"
+msgid "Use proxy for license"
+msgstr ""
+
+msgctxt "#30013"
+msgid "Fix freezes in VOD content"
+msgstr ""
+
+msgctxt "#30016"
+msgid "Proxy timeout (ms)"
+msgstr ""
+
+msgctxt "#30017"
+msgid "Show only subscribed content"
+msgstr ""
+
+msgctxt "#30018"
+msgid "Try to play HD in SD devices"
+msgstr ""
+
+msgctxt "#30019"
+msgid "Configure InputStream Adaptive"
+msgstr ""
+
+msgctxt "#30020"
+msgid "Add EPG info to TV channels"
+msgstr ""
+
+msgctxt "#30021"
+msgid "Download extra info (like posters, actors...)"
+msgstr ""
+
+msgctxt "#30022"
+msgid "Report sessions to servers"
+msgstr ""
+
+msgctxt "#30023"
+msgid "Reuse devices"
+msgstr ""
+
+msgctxt "#30024"
+msgid "Try to fix error 4027"
+msgstr ""
+
+msgctxt "#30101"
+msgid "Menu"
+msgstr ""
+
+msgctxt "#30102"
+msgid "My list"
+msgstr ""
+
+msgctxt "#30103"
+msgid "Recordings"
+msgstr ""
+
+msgctxt "#30104"
+msgid "Live TV"
+msgstr ""
+
+msgctxt "#30105"
+msgid "Movies"
+msgstr ""
+
+msgctxt "#30106"
+msgid "TV shows"
+msgstr ""
+
+msgctxt "#30107"
+msgid "EPG"
+msgstr ""
+
+msgctxt "#30108"
+msgid "Manage devices"
+msgstr ""
+
+msgctxt "#30109"
+msgid "Next page"
+msgstr ""
+
+msgctxt "#30110"
+msgid "Previous page"
+msgstr ""
+
+msgctxt "#30111"
+msgid "VOD"
+msgstr ""
+
+msgctxt "#30112"
+msgid "Search"
+msgstr ""
+
+msgctxt "#30113"
+msgid "New search"
+msgstr ""
+
+msgctxt "#30114"
+msgid "Delete search"
+msgstr ""
+
+msgctxt "#30115"
+msgid "Delete all searchs"
+msgstr ""
+
+msgctxt "#30116"
+msgid "Search term"
+msgstr ""
+
+msgctxt "#30117"
+msgid "Search results"
+msgstr ""
+
+msgctxt "#30120"
+msgid "Documentaries"
+msgstr ""
+
+msgctxt "#30121"
+msgid "Kids"
+msgstr ""
+
+msgctxt "#30150"
+msgid "Close session"
+msgstr ""
+
+msgctxt "#30151"
+msgid "Delete device"
+msgstr ""
+
+msgctxt "#30152"
+msgid "Select device"
+msgstr ""
+
+msgctxt "#30153"
+msgid "New device"
+msgstr ""
+
+msgctxt "#30154"
+msgid "Device name"
+msgstr ""
+
+msgctxt "#30160"
+msgid "Accounts"
+msgstr ""
+
+msgctxt "#30161"
+msgid "Add account"
+msgstr ""
+
+msgctxt "#30162"
+msgid "Remove account"
+msgstr ""
+
+msgctxt "#30163"
+msgid "Username"
+msgstr ""
+
+msgctxt "#30164"
+msgid "Password"
+msgstr ""
+
+msgctxt "#30165"
+msgid "Select account"
+msgstr ""
+
+msgctxt "#30166"
+msgid "Login failed"
+msgstr ""
+
+msgctxt "#30167"
+msgid "Edit account"
+msgstr ""
+
+msgctxt "#30170"
+msgid "Play from the beginning"
+msgstr ""
+
+msgctxt "#30171"
+msgid "Record program"
+msgstr ""
+
+msgctxt "#30172"
+msgid "Recording scheduled"
+msgstr ""
+
+msgctxt "#30173"
+msgid "Delete recording"
+msgstr ""
+
+msgctxt "#30174"
+msgid "Do you want to delete {}?"
+msgstr ""
+
+msgctxt "#30180"
+msgid "Profiles"
+msgstr ""
+
+msgctxt "#30181"
+msgid "Login with key file"
+msgstr ""
+
+msgctxt "#30182"
+msgid "Select the key file"
+msgstr ""
+
+msgctxt "#30200"
+msgid "Error"
+msgstr ""
+
+msgctxt "#30201"
+msgid "Information"
+msgstr ""
+
+msgctxt "#30202"
+msgid "Inputstream.adaptive is not available"
+msgstr ""
+
+msgctxt "#30203"
+msgid "Format not supported"
+msgstr ""
diff --git a/resources/language/resource.language.es_es/strings.po b/resources/language/resource.language.es_es/strings.po
new file mode 100644
index 0000000..428124d
--- /dev/null
+++ b/resources/language/resource.language.es_es/strings.po
@@ -0,0 +1,258 @@
+# XBMC Media Center language file
+# Addon Name: Orange
+# Addon id: plugin.video.orange
+# Addon version: 0.0.2
+# Addon Provider: Paco8
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: XBMC-Addons\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-12-28 16:47+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgctxt "#30001"
+msgid "Main"
+msgstr "Principal"
+
+msgctxt "#30002"
+msgid "Proxy"
+msgstr "Proxy"
+
+msgctxt "#30003"
+msgid "Experimental"
+msgstr "Experimental"
+
+msgctxt "#30010"
+msgid "DRM type"
+msgstr "Tipo de DRM"
+
+msgctxt "#30011"
+msgid "Manifest alteration"
+msgstr "Modificar manifiesto"
+
+msgctxt "#30012"
+msgid "Use proxy for license"
+msgstr "Usar proxy para la licencia"
+
+msgctxt "#30013"
+msgid "Fix freezes in VOD content"
+msgstr "Solucionar parones en TV a la carta"
+
+msgctxt "#30016"
+msgid "Proxy timeout (ms)"
+msgstr "Tiempo de espera (ms)"
+
+msgctxt "#30017"
+msgid "Show only subscribed content"
+msgstr "Solo mostrar el contenido incluido en la suscripción"
+
+msgctxt "#30018"
+msgid "Try to play HD in SD devices"
+msgstr "Intentar reproducir en HD en dispositivos SD"
+
+msgctxt "#30019"
+msgid "Configure InputStream Adaptive"
+msgstr "Configurar InputStream Adaptive"
+
+msgctxt "#30020"
+msgid "Add EPG info to TV channels"
+msgstr "Mostrar el programa en emisión en los canales de TV"
+
+msgctxt "#30021"
+msgid "Download extra info (like posters, actors...)"
+msgstr "Descargar información extra (como posters, actores...)"
+
+msgctxt "#30022"
+msgid "Report sessions to servers"
+msgstr "Informar de las sesiones a los servidores"
+
+msgctxt "#30023"
+msgid "Reuse devices"
+msgstr "Reutilizar dispositivos"
+
+msgctxt "#30024"
+msgid "Try to fix error 4027"
+msgstr "Intentar solucionar el error 4027"
+
+msgctxt "#30101"
+msgid "Menu"
+msgstr "Menú"
+
+msgctxt "#30102"
+msgid "My list"
+msgstr "Mi lista"
+
+msgctxt "#30103"
+msgid "Recordings"
+msgstr "Grabaciones"
+
+msgctxt "#30104"
+msgid "Live TV"
+msgstr "TV en directo"
+
+msgctxt "#30105"
+msgid "Movies"
+msgstr "Películas"
+
+msgctxt "#30106"
+msgid "TV shows"
+msgstr "Series"
+
+msgctxt "#30107"
+msgid "EPG"
+msgstr "Guía de programación"
+
+msgctxt "#30108"
+msgid "Manage devices"
+msgstr "Gestionar dispositivos"
+
+msgctxt "#30109"
+msgid "Next page"
+msgstr "Página siguiente"
+
+msgctxt "#30110"
+msgid "Previous page"
+msgstr "Página anterior"
+
+msgctxt "#30111"
+msgid "VOD"
+msgstr "TV a la carta"
+
+msgctxt "#30112"
+msgid "Search"
+msgstr "Búsqueda"
+
+msgctxt "#30113"
+msgid "New search"
+msgstr "Nueva búsqueda"
+
+msgctxt "#30114"
+msgid "Delete search"
+msgstr "Borrar búsqueda"
+
+msgctxt "#30115"
+msgid "Delete all searchs"
+msgstr "Borrar todas las búsquedas"
+
+msgctxt "#30116"
+msgid "Search term"
+msgstr "Término a buscar"
+
+msgctxt "#30117"
+msgid "Search results"
+msgstr "Resultados de la búsqueda"
+
+msgctxt "#30120"
+msgid "Documentaries"
+msgstr "Documentales"
+
+msgctxt "#30121"
+msgid "Kids"
+msgstr "Infantiles"
+
+msgctxt "#30150"
+msgid "Close session"
+msgstr "Cerrar sesión"
+
+msgctxt "#30151"
+msgid "Delete device"
+msgstr "Eliminar dispositivo"
+
+msgctxt "#30152"
+msgid "Select device"
+msgstr "Seleccionar dispositivo"
+
+msgctxt "#30153"
+msgid "New device"
+msgstr "Nuevo dispositivo"
+
+msgctxt "#30154"
+msgid "Device name"
+msgstr "Nombre para el dispositivo"
+
+msgctxt "#30160"
+msgid "Accounts"
+msgstr "Cuentas"
+
+msgctxt "#30161"
+msgid "Add account"
+msgstr "Añadir cuenta"
+
+msgctxt "#30162"
+msgid "Remove account"
+msgstr "Borrar cuenta"
+
+msgctxt "#30163"
+msgid "Username"
+msgstr "Usuario"
+
+msgctxt "#30164"
+msgid "Password"
+msgstr "Clave"
+
+msgctxt "#30165"
+msgid "Select account"
+msgstr "Seleccionar cuenta"
+
+msgctxt "#30166"
+msgid "Login failed"
+msgstr "Error al iniciar sesión"
+
+msgctxt "#30167"
+msgid "Edit account"
+msgstr "Editar cuenta"
+
+msgctxt "#30170"
+msgid "Play from the beginning"
+msgstr "Ver desde el principio"
+
+msgctxt "#30171"
+msgid "Record program"
+msgstr "Grabar programa"
+
+msgctxt "#30172"
+msgid "Recording scheduled"
+msgstr "Grabación programada"
+
+msgctxt "#30173"
+msgid "Delete recording"
+msgstr "Borrar grabación"
+
+msgctxt "#30174"
+msgid "Do you want to delete {}?"
+msgstr "¿Quieres borrar {}?"
+
+msgctxt "#30180"
+msgid "Profiles"
+msgstr "Perfiles"
+
+msgctxt "#30181"
+msgid "Login with key file"
+msgstr "Iniciar sesión con fichero key"
+
+msgctxt "#30182"
+msgid "Select the key file"
+msgstr "Selecciona el fichero key"
+
+msgctxt "#30200"
+msgid "Error"
+msgstr "Error"
+
+msgctxt "#30201"
+msgid "Information"
+msgstr "Información"
+
+msgctxt "#30202"
+msgid "Inputstream.adaptive is not available"
+msgstr "Inputstream.adaptive no está disponible"
+
+msgctxt "#30203"
+msgid "Format not supported"
+msgstr "Formato no soportado"
diff --git a/resources/lib/__init__.py b/resources/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/lib/addon.py b/resources/lib/addon.py
new file mode 100644
index 0000000..fdd7fa6
--- /dev/null
+++ b/resources/lib/addon.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+try: # Kodi >= 19
+ from xbmcvfs import translatePath
+except ImportError: # Kodi 18
+ from xbmc import translatePath
+
+import os
+import xbmcaddon
+from .log import LOG
+
+addon = xbmcaddon.Addon()
+profile_dir = translatePath(addon.getAddonInfo('profile'))
+
+LOG("Profile directory: {}".format(profile_dir))
+if not os.path.exists(profile_dir):
+ LOG('Creating {}'.format(profile_dir))
+ os.makedirs(profile_dir)
diff --git a/resources/lib/cache.py b/resources/lib/cache.py
new file mode 100644
index 0000000..ad9b7b4
--- /dev/null
+++ b/resources/lib/cache.py
@@ -0,0 +1,50 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+import io
+import os
+import time
+import glob
+import sys
+
+class Cache(object):
+ config_directory = ''
+
+ def __init__(self, config_directory):
+ self.config_directory = config_directory
+
+ def load_file(self, filename):
+ filename = self.config_directory + filename
+ if not os.path.exists(filename): return None
+ with io.open(filename, 'r', encoding='utf-8') as handle:
+ return handle.read()
+
+ def save_file(self, filename, data):
+ if sys.version_info[0] < 3:
+ if not isinstance(data, unicode):
+ data = unicode(data, 'utf-8')
+ with io.open(self.config_directory + filename, 'w', encoding='utf-8', newline='') as handle:
+ handle.write(data)
+
+ def load(self, filename, cache_minutes = 24*60):
+ filename = self.config_directory + filename
+ if os.path.exists(filename) and (time.time() - os.path.getmtime(filename) < cache_minutes*60):
+ with io.open(filename, 'r', encoding='utf-8') as handle:
+ return handle.read()
+ return None
+
+ def remove_file(self, filename):
+ filename = self.config_directory + filename
+ if os.path.exists(filename):
+ os.remove(filename)
+
+ def clear_config(self):
+ types = ('*.conf', '*.json')
+ files = []
+ for ext in types:
+ files.extend(glob.glob(self.config_directory + ext))
+ for filename in files:
+ os.remove(filename)
diff --git a/resources/lib/gui.py b/resources/lib/gui.py
new file mode 100644
index 0000000..3532a6e
--- /dev/null
+++ b/resources/lib/gui.py
@@ -0,0 +1,55 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import sys
+import xbmc
+import xbmcgui
+import xbmcplugin
+from .log import LOG
+from .addon import addon
+
+def handle():
+ try:
+ return int(sys.argv[1])
+ except:
+ return -1
+
+def input_window(heading, text = '', hidden = False):
+ res = None
+ keyboard = xbmc.Keyboard(text)
+ keyboard.setHeading(heading)
+ keyboard.setHiddenInput(hidden)
+ keyboard.doModal()
+ if (keyboard.isConfirmed()):
+ res = keyboard.getText()
+ del keyboard
+ return res
+
+def open_folder(name, content_type = 'videos'):
+ LOG('handle: {}'.format(handle()))
+ xbmcplugin.setPluginCategory(handle(), name)
+ xbmcplugin.setContent(handle(), content_type)
+
+def close_folder(updateListing=False, cacheToDisc=True):
+ xbmcplugin.endOfDirectory(handle(), updateListing=updateListing, cacheToDisc=cacheToDisc)
+
+def add_menu_option(title, url, context_menu = None, info = None, art = None):
+ list_item = xbmcgui.ListItem(label=title)
+ if not info:
+ info = {'title': title, 'plot': '[B][/B]'}
+ if not art:
+ art = {'icon': addon.getAddonInfo('icon'), 'poster': addon.getAddonInfo('icon')}
+ list_item.setInfo('video', info)
+ list_item.setArt(art)
+ if context_menu:
+ list_item.addContextMenuItems(context_menu)
+ xbmcplugin.addDirectoryItem(handle(), url, list_item, True)
+
+def show_notification(message, icon = xbmcgui.NOTIFICATION_ERROR):
+ if icon == xbmcgui.NOTIFICATION_ERROR:
+ heading = addon.getLocalizedString(30200)
+ else:
+ heading = addon.getLocalizedString(30201)
+ icon = addon.getAddonInfo('icon')
+ xbmcgui.Dialog().notification(heading, message, icon, 5000)
diff --git a/resources/lib/log.py b/resources/lib/log.py
new file mode 100644
index 0000000..bcf9fa6
--- /dev/null
+++ b/resources/lib/log.py
@@ -0,0 +1,16 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import json
+
+def LOG(message):
+ try:
+ import xbmc
+ import xbmcaddon
+ xbmc.log('[{}] {}'.format(xbmcaddon.Addon().getAddonInfo('id'), message), xbmc.LOGDEBUG)
+ except:
+ print(message)
+
+def print_json(data):
+ LOG(json.dumps(data, indent=4))
diff --git a/resources/lib/movistar.py b/resources/lib/movistar.py
new file mode 100644
index 0000000..1d19b81
--- /dev/null
+++ b/resources/lib/movistar.py
@@ -0,0 +1,743 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+import sys
+import base64
+import json
+import requests
+import io
+import os
+import time
+import re
+
+from .log import LOG, print_json
+from .network import Network
+from .cache import Cache
+from .timeconv import *
+
+class Movistar(object):
+ account = {'username': '', 'password': '',
+ 'device_id': '',
+ 'id': None,
+ 'pid': None,
+ 'encoded_user': '',
+ 'profile_id': '0',
+ 'platform': '',
+ 'access_token': '',
+ 'session_token': '',
+ 'ssp_token': '',
+ 'demarcation': 0}
+
+ def __init__(self, config_directory, reuse_devices=False):
+ self.logged = False
+
+ # Network
+ headers = {
+ 'Accept': 'application/json, text/javascript, */*; q=0.01',
+ 'Accept-Language': 'es-ES,es;q=0.9',
+ 'Origin': 'https://ver.movistarplus.es',
+ 'Referer': 'https://ver.movistarplus.es/',
+ 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
+ }
+ self.net = Network()
+ self.net.headers = headers
+
+ # Cache
+ self.cache = Cache(config_directory)
+
+ # Access token
+ self.load_key_file() # Default access_token
+ content = self.cache.load_file('access_token.conf')
+ if content:
+ self.account['access_token'] = content
+ #LOG('access_token: {}'.format(self.account['access_token']))
+ if not self.account['access_token']: return
+
+ # Endpoints
+ self.endpoints = self.get_endpoints()
+
+ # Account
+ data = None
+ content = self.cache.load('account.json')
+ if content:
+ data = json.loads(content)
+ if not data or 'ofertas' not in data:
+ data = self.authenticate()
+ if not data or 'ofertas' not in data: return
+ self.cache.save_file('account.json', json.dumps(data, ensure_ascii=False))
+ self.account['id'] = data['ofertas'][0]['accountNumber']
+ self.account['pid'] = data['ofertas'][0]['cod_persona']
+ self.account['encoded_user'] = data['cod_usuario_cifrado']
+ self.account['platform'] = data['ofertas'][0]['@id_perfil']
+
+ # Profile ID
+ content = self.cache.load_file('profile_id.conf')
+ if content:
+ self.account['profile_id'] = content
+
+ # Device ID
+ self.account['device_id'] = self.cache.load_file('device_id.conf')
+ if not reuse_devices:
+ LOG('not reusing devices')
+ if not self.account['device_id']:
+ self.account['device_id'] = self.new_device_id()
+ self.cache.save_file('device_id.conf', self.account['device_id'])
+
+ # Check if device is registered
+ device_list = self.get_devices()
+ found_device = False
+ for device in device_list:
+ if device['id'] == self.account['device_id']:
+ found_device = True
+ break
+ if not found_device:
+ LOG('device not found, trying to register device')
+ self.register_device()
+ device_list = self.get_devices(use_cache=False) # To store the updated list in cache
+ else:
+ LOG('reusing devices')
+ # Check if device is registered
+ device_list = self.get_devices()
+ found_device = False
+ wp_device = None
+ for device in device_list:
+ if not wp_device and device['type_code'] == 'WP':
+ wp_device = device['id']
+ if device['id'] == self.account['device_id']:
+ found_device = True
+ break
+ if not found_device:
+ if wp_device:
+ LOG('device not found, using {}'.format(wp_device))
+ self.account['device_id'] = wp_device
+ else:
+ LOG('device not found, registering new device')
+ self.account['device_id'] = self.new_device_id()
+ self.register_device()
+ device_list = self.get_devices(use_cache=False) # To store the updated list in cache
+ self.cache.save_file('device_id.conf', self.account['device_id'])
+ LOG('device_id: {}'. format(self.account['device_id']))
+
+ # Tokens
+ content = self.cache.load('tokens.json', 3)
+ if content:
+ data = json.loads(content)
+ else:
+ LOG('Loading tokens')
+ data = self.get_token()
+ #print_json(data)
+ if 'accessToken' in data:
+ self.cache.save_file('tokens.json', json.dumps(data, ensure_ascii=False))
+ self.cache.save_file('access_token.conf', data['accessToken'])
+
+ self.account['session_token'] = ''
+ self.account['ssp_token'] = ''
+
+ self.entitlements = {}
+
+ if 'accessToken' in data:
+ self.account['access_token'] = data['accessToken']
+ self.account['session_token'] = data['token']
+ self.account['ssp_token'] = data['sspToken']
+ self.account['demarcation'] = data['demarcation']
+
+ self.entitlements['activePurchases'] = data['activePurchases']
+ self.entitlements['partners'] = data['partners']
+ self.entitlements['activePackages'] = data['activePackages']
+ self.entitlements['vodSubscription'] = data['vodSubscription'].split(',')
+ self.entitlements['linearSubscription'] = data['linearSubscription'].split(',')
+ self.entitlements['suscripcion'] = data['suscripcion']
+ #print_json(self.entitlements)
+ self.logged = True
+
+ def install_key_file(self, filename):
+ import shutil
+ if sys.version_info[0] > 2:
+ filename = bytes(filename, 'utf-8')
+ shutil.copyfile(filename, self.cache.config_directory + 'auth.key')
+
+ def load_key_file(self):
+ content = self.cache.load_file('auth.key')
+ if content:
+ data = json.loads(content)
+ data = json.loads(data['data'])
+ self.account['access_token'] = data['response']['access_token']
+
+ def get_token(self):
+ data = {"accountNumber": self.account['id'],
+ "userProfile": self.account['profile_id'],
+ "streamMiscellanea":"HTTPS",
+ "deviceType":"WP_OTT",
+ "deviceManufacturerProduct":"Firefox",
+ "streamDRM":"Widevine",
+ "streamFormat":"DASH",
+ }
+
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['X-Movistarplus-Ui'] = '2.36.30'
+ headers['X-Movistarplus-Os'] = 'Linux88'
+ headers['X-Movistarplus-Deviceid'] = self.account['device_id']
+ headers['Authorization'] = 'Bearer ' + self.account['access_token']
+ #print_json(data)
+ #print_json(headers)
+
+ url = self.endpoints['initdata'].format(deviceType='webplayer', DEVICEID=self.account['device_id'])
+ response = self.net.session.post(url, data=json.dumps(data), headers=headers)
+ content = response.content.decode('utf-8')
+ #LOG('get_token response: {}'.format(content))
+ try:
+ d = json.loads(content)
+ except:
+ d = {'error': content}
+ return d
+
+ def clear_session(self):
+ headers = self.net.headers.copy()
+ headers['Access-Control-Request-Method'] = 'POST'
+ headers['Access-Control-Request-Headers'] = 'content-type,x-hzid'
+ url = self.endpoints['setUpStream'].format(PID=self.account['pid'], deviceCode='WP_OTT', PLAYREADYID=self.account['device_id'])
+ response = self.net.session.options(url, headers=headers)
+ content = response.content.decode('utf-8')
+ return content
+
+ def open_session(self, data, session_token, session_id = None):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['X-Hzid'] = session_token
+ url = self.endpoints['setUpStream'].format(PID=self.account['id'], deviceCode='WP_OTT', PLAYREADYID=self.account['device_id'])
+ if session_id != None:
+ url += '/' + session_id
+ #LOG('open_session: url: {}'.format(url))
+ #LOG('open_session: data: {}'.format(data))
+ response = self.net.session.post(url, data=data, headers=headers)
+ content = response.content.decode('utf-8')
+ try:
+ d = json.loads(content)
+ except:
+ d = None
+ #LOG("**** open_session response: {}".format(d))
+ return d
+
+ def login(self):
+ headers = self.net.headers.copy()
+ headers['x-movistarplus-deviceid'] = self.account['device_id']
+ headers['x-movistarplus-ui'] = '2.36.30'
+ headers['x-movistarplus-os'] = 'Linux88'
+
+ if sys.version_info[0] < 3:
+ p = base64.b64decode(bytes(self.account['password']))
+ else:
+ p = base64.b64decode(bytes(self.account['password'], encoding='ascii')).decode('ascii')
+
+ data = {
+ 'grant_type': 'password',
+ 'deviceClass': 'webplayer',
+ 'username': self.account['username'],
+ 'password': p,
+ 'captchaResult': '',
+ }
+
+ url = self.endpoints['token']
+ response = self.net.session.post(url, headers=headers, data=data)
+ print(response.content)
+
+ def authenticate(self):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ #headers['x-movistarplus-deviceid'] = self.account['device_id']
+ headers['x-movistarplus-ui'] = '2.36.30'
+ headers['x-movistarplus-os'] = 'Linux88'
+ headers['Authorization'] = 'Bearer ' + self.account['access_token']
+ url = self.endpoints['autenticacion_tk'] + '?_=' + str(int(time.time()*1000))
+ #LOG('authenticate: headers: {}'.format(headers))
+ data = self.net.load_data(url, headers)
+ #LOG('authenticate: data: {}'.format(data))
+ return data
+
+ def load_service_directory(self):
+ url = 'https://homeservicedirectory.dof6.com/VoD/vod.svc/webplayer/environments/prod/ws-directory?format=json'
+ return self.net.load_data(url)
+
+ def get_endpoints(self):
+ endpoints = {}
+ content = self.cache.load('endpoints.json')
+ if content:
+ data = json.loads(content)
+ else:
+ data = self.load_service_directory()
+ self.cache.save_file('endpoints.json', json.dumps(data, ensure_ascii=False))
+
+ for services in data['services']['service']:
+ if isinstance(services['endpoint'], list):
+ for endpoint in services['endpoint']:
+ endpoints[endpoint['@name']] = endpoint['@address']
+ else:
+ endpoint = services['endpoint']
+ endpoints[endpoint['@name']] = endpoint['@address']
+ return endpoints
+
+ def change_device(self, id):
+ self.account['device_id'] = id
+ self.cache.save_file('device_id.conf', self.account['device_id'])
+ self.cache.remove_file('tokens.json')
+
+ def get_devices(self, use_cache=True):
+ content = self.cache.load('devices.json', 3)
+ if use_cache and content:
+ data = json.loads(content)
+ else:
+ headers = self.net.headers.copy()
+ headers['Authorization'] = 'Bearer ' + self.account['access_token']
+ url = self.endpoints['obtenerdipositivos'].format(ACCOUNTNUMBER=self.account['id'])
+ data = self.net.load_data(url, headers)
+ self.cache.save_file('devices.json', json.dumps(data, ensure_ascii=False))
+ #print_json(data)
+ res = []
+ for d in data:
+ if d['Id'] != '-':
+ dev = {}
+ dev['id'] = d['Id']
+ dev['name'] = d['Name']
+ dev['type'] = d['DeviceType']
+ dev['type_code'] = d['DeviceTypeCode']
+ dev['playing'] = d['ContentPlaying']
+ dev['reg_date'] = isodate2str(d['RegistrationDate'])
+ dev['in_ssp'] = d['IsInSsp']
+ res.append(dev)
+ return res
+
+ def register_device(self):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['x-movistarplus-deviceid'] = self.account['device_id']
+ headers['x-movistarplus-ui'] = '2.36.30'
+ headers['x-movistarplus-os'] = 'Linux88'
+ headers['Authorization'] = 'Bearer ' + self.account['access_token']
+ url = self.endpoints['activacion_dispositivo_cuenta_tk'].format(ACCOUNTNUMBER=self.account['id'], DEVICEID=self.account['device_id'])
+ response = self.net.session.post(url, headers=headers)
+ content = response.content.decode('utf-8')
+ return content
+
+ def unregister_device(self):
+ return self.delete_device(self.account['device_id'])
+
+ def new_device_id(self):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['x-movistarplus-ui'] = '2.36.30'
+ headers['x-movistarplus-os'] = 'Linux88'
+ headers['Authorization'] = 'Bearer ' + self.account['access_token']
+ url = 'https://auth.dof6.com/movistarplus/webplayer/accounts/'+ self.account['id'] + '/devices/?qspVersion=ssp'
+ response = self.net.session.post(url, headers=headers)
+ content = response.content.decode('utf-8')
+ return content.replace('"', '')
+
+ def delete_device(self, device_id):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['Authorization'] = 'bearer ' + self.account['access_token']
+ url = self.endpoints['eliminardipositivo'].format(ACCOUNTNUMBER=self.account['id'], DEVICEID=device_id)
+ response = self.net.session.delete(url, headers=headers)
+ content = response.content.decode('utf-8')
+ return content
+
+ def rename_device(self, device_id, name):
+ headers = self.net.headers.copy()
+ headers['Authorization'] = 'bearer ' + self.account['access_token']
+ url = self.endpoints['nombrardispositivo'].format(ACCOUNTNUMBER=self.account['id'], DEVICEID=device_id)
+ response = self.net.session.put(url, headers=headers, json=name)
+ content = response.content.decode('utf-8')
+ return content
+
+ def delete_session_id(self, session_token, id = '0'):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'text/plain;charset=UTF-8'
+ data = '{"X-HZId":"' + session_token +'","X-Content-Type":"application/json","X-Operation":"DELETE"}'
+ url = self.endpoints['tearDownStream'].format(PID=self.account['pid'], deviceCode='WP_OTT', PLAYREADYID=self.account['device_id'], SessionID=id)
+ response = self.net.session.post(url, headers=headers, data=data)
+ content = response.content.decode('utf-8')
+ return content
+
+ """
+ def delete_last_session(self):
+ token = self.cache.load_file('session_token.conf')
+ if token:
+ self.delete_session0(token)
+ os.remove(self.cache.config_directory + 'session_token.conf')
+ """
+
+ def delete_session(self, device_id = None):
+ if device_id == None: device_id = self.account['device_id']
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['Authorization'] = 'bearer ' + self.account['access_token']
+ url = self.endpoints['cerrarsesiondispositivo'].format(ACCOUNTNUMBER=self.account['id'], DEVICEID=device_id)
+ response = self.net.session.delete(url, headers=headers)
+ content = response.content.decode('utf-8')
+ return content
+
+ def get_profiles(self):
+ headers = self.net.headers.copy()
+ headers['X-HZId'] = self.account['session_token']
+ url = self.endpoints['listaperfiles']
+ data = self.net.load_data(url, headers=headers)
+ res = []
+ for d in data['items']:
+ p = {}
+ p['id'] = d['id']
+ p['name'] = d['name']
+ p['for_kids'] = d['isForKids']
+ p['type'] = d['typeID']
+ p['image_id'] = d['imageID']
+ res.append(p)
+ return res
+
+ def change_profile(self, id):
+ self.account['profile_id'] = id
+ self.cache.save_file('profile_id.conf', self.account['profile_id'])
+ self.cache.remove_file('tokens.json')
+
+ def get_epg(self):
+ content = self.cache.load('epg2.json', 6*60)
+ if content:
+ data = json.loads(content)
+ else:
+ today = datetime.today()
+ str_now = today.strftime('%Y-%m-%dT%H:00:00')
+ url = self.endpoints['rejilla'].format(deviceType='webplayer', profile=self.account['platform'], UTCDATETIME=str_now, DURATION=2, CHANNELS='', NETWORK='movistarplus', mdrm='true', demarcation=self.account['demarcation'])
+ data = self.net.load_data(url)
+ self.cache.save_file('epg2.json', json.dumps(data, ensure_ascii=False))
+
+ epg = {}
+ for ch in data:
+ id = ch[0]['Canal']['CodCadenaTv']
+ if not id in epg: epg[id] = []
+ for p in ch:
+ pr = {}
+ pr['start'] = int(p['FechaHoraInicio'])
+ pr['end'] = int(p['FechaHoraFin'])
+ pr['start_str'] = timestamp2str(pr['start'])
+ pr['end_str'] = timestamp2str(pr['end'])
+ pr['desc1'] = p['Titulo']
+ pr['desc2'] = ''
+ if 'TituloHorLinea2' in p:
+ pr['desc2'] = p['TituloHorLinea2']
+ epg[id].append(pr)
+
+ return epg
+
+ def find_program_epg(self, epg, id, timestamp):
+ found = False
+ c = 0
+ programs = []
+ for p in epg[id]:
+ #print(p)
+ if (p['start'] <= timestamp) and (timestamp <= p['end']):
+ found = True
+ if found:
+ programs.append(p)
+ c += 1
+ if (c > 5): break;
+ return programs
+
+ def colorize_title(self, title):
+ s = title['info']['title']
+
+ stype = title.get('stream_type')
+ if stype == 'u7d': s += ' (U7D)'
+ elif stype == 'rec': s += ' (REC)'
+
+ if title['subscribed'] == True:
+ color1 = 'yellow'
+ color2 = 'red'
+ else:
+ color1 = 'gray'
+ color2 = 'gray'
+ s = '[COLOR gray]' + s +'[/COLOR]'
+ if title.get('desc1', '') != '':
+ s += ' - [COLOR {}]{}[/COLOR]'.format(color1, title['desc1'])
+ if title.get('desc2', '') != '':
+ s += ' - [COLOR {}]{}[/COLOR]'.format(color2, title['desc2'])
+ return s
+
+ def add_epg_info(self, channels, epg, timestamp):
+ for ch in channels:
+ programs = self.find_program_epg(epg, ch['id'], timestamp)
+ ch['info']['plot'] = ''
+ for i in range(len(programs)):
+ desc1 = programs[i]['desc1']
+ desc2 = programs[i]['desc2']
+ if i == 0:
+ ch['desc1'] = desc1
+ ch['desc2'] = desc2
+ ch['info']['plot'] += "[B]{}[/B] {} {}\n".format(programs[i]['start_str'], desc1, desc2)
+
+ def is_subscribed_channel(self, products):
+ for e in self.entitlements['activePackages']:
+ if e['name'] in products:
+ return True
+ for e in self.entitlements['activePurchases']:
+ if e['name'] in products:
+ return True
+ return False
+
+ def is_subscribed_vod(self, products):
+ for p in products:
+ if p['Nombre'] in self.entitlements['vodSubscription']:
+ return True
+ return False
+
+ def get_channels(self, add_epg_info = False):
+ if add_epg_info:
+ url = self.endpoints['guiaTV'].format(deviceType='webplayer', profile=self.account['platform'], preOffset=0, postOffset=0, mdrm='true', demarcation=self.account['demarcation'])
+ data = self.net.load_data(url)
+ else:
+ content = self.cache.load('channels2.json')
+ if content:
+ data = json.loads(content)
+ else:
+ url = self.endpoints['canales'].format(deviceType='webplayer', profile=self.account['platform'], mdrm='true', demarcation=self.account['demarcation'])
+ data = self.net.load_data(url)
+ self.cache.save_file('channels2.json', json.dumps(data, ensure_ascii=False))
+
+ res = []
+ for c in data:
+ t = {}
+ t['info'] = {}
+ t['art'] = {}
+ t['type'] = 'movie'
+ t['stream_type'] = 'tv'
+ t['info']['mediatype'] = 'movie'
+ t['info']['title'] = str(c['Dial']) +'. ' + c['Nombre']
+ t['id'] = c['CodCadenaTv']
+ if add_epg_info:
+ t['desc1'] = c['Nombre']
+ t['desc2'] = ''
+ t['dial'] = c['Dial']
+ t['url'] = c['PuntoReproduccion']
+ t['art']['icon'] = c['Logo']
+ t['art']['thumb'] = c['Logo']
+ t['art']['poster'] = c['Logo']
+ t['session_request'] = '{"contentID":"'+ t['id'] +'", "streamType":"CHN"}'
+ t['subscribed'] = self.is_subscribed_channel(c['Productos'])
+ t['info']['playcount'] = 1 # Set as watched
+ # epg
+ if add_epg_info:
+ program = c['Pases'][0]
+ t['desc1'] = program['Titulo']
+ if 'TituloHorLinea2' in program:
+ t['desc2'] = program['TituloHorLinea2']
+ t['start'] = int(program['FechaHoraInicio'])
+ t['end'] = int(program['FechaHoraFin'])
+ t['start_str'] = timestamp2str(t['start'])
+ t['end_str'] = timestamp2str(t['end'])
+ t['info']['plot'] = '[B]{}-{}[/B] {}\n{}'.format(t['start_str'], t['end_str'], t['desc1'], t['desc2'])
+
+ res.append(t)
+
+ return res
+
+ def get_channels_with_epg(self):
+ channels = self.get_channels()
+ epg = self.get_epg()
+ import time
+ now = int(time.time() * 1000)
+ self.add_epg_info(channels, epg, now)
+ return channels
+
+ def download_list(self, url, use_hz = False):
+ headers = self.net.headers.copy()
+ headers['Accept'] = 'application/vnd.miviewtv.v1+json'
+ headers['Content-Type'] = 'application/json'
+ if use_hz:
+ headers['Authorization'] = 'Bearer ' + self.account['access_token']
+ headers['X-Hzid'] = self.account['session_token']
+
+ return self.net.load_data(url, headers)
+
+ def get_wishlist_url(self):
+ url = self.endpoints['favoritos'].format(
+ deviceType='webplayer', DIGITALPLUSUSERIDC=self.account['encoded_user'], PROFILE=self.account['platform'],
+ ACCOUNTNUMBER=self.account['id'], idsOnly='false', start=1, end=30, mdrm='true', demarcation=self.account['demarcation'])
+ #url += '&filter=AD-SINX&topic=CN'
+ url += '&_='+ str(int(time.time()*1000))
+ return url
+
+ def get_recordings_url(self):
+ url = self.endpoints['grabaciones'].format(
+ deviceType='webplayer', DIGITALPLUSUSERIDC=self.account['encoded_user'], PROFILE=self.account['platform'],
+ idsOnly='false', start=1, end=30, mdrm='true', demarcation=self.account['demarcation'])
+ url += '&state=Completed&_='+ str(int(time.time()*1000))
+ return url
+
+ def get_title(self, data):
+ t = {}
+ t['info'] = {}
+ t['art'] = {}
+ ed = data['DatosEditoriales']
+ t['id'] = ed['Id']
+ t['info']['title'] = ed['Titulo']
+ t['art']['poster'] = ed['Imagen'].replace('ywcatalogov', 'dispficha')
+ t['art']['thumb'] = t['art']['poster']
+ t['info']['genre'] = ed['GeneroComAntena']
+ if ed['TipoContenido'] in ['Individual', 'Episodio']:
+ t['type'] = 'movie'
+ t['stream_type'] = 'vod'
+ t['info']['mediatype'] = 'movie'
+ t['info']['duration'] = ed['DuracionEnSegundos']
+ t['url'] = ''
+ t['session_request'] = ''
+ if len(data['VodItems']) > 0:
+ video = data['VodItems'][0]
+ t['subscribed'] = self.is_subscribed_vod(video['Productos'])
+ if not 'UrlVideo' in video: return None
+ t['url'] = video['UrlVideo']
+ if video['AssetType'] == 'VOD':
+ t['session_request'] = '{"contentID":' + str(t['id']) + ',"drmMediaID":"' + video['CasId'] +'", "streamType":"AST"}'
+ elif video['AssetType'] == 'U7D':
+ t['stream_type'] = 'u7d'
+ t['session_request'] = '{"contentID":' + str(t['id']) + ', "streamType":"CUTV"}'
+ elif video['AssetType'] == 'NPVR':
+ t['stream_type'] = 'rec'
+ t['session_request'] = '{"contentID":' + str(t['id']) + ', "streamType":"NPVR"}'
+ if ed['TipoContenido'] == 'Serie':
+ t['type'] = 'series'
+ t['info']['mediatype'] = 'tvshow'
+ t['subscribed'] = self.is_subscribed_vod(data['Productos'])
+ if ed['TipoContenido'] == 'Temporada':
+ t['type'] = 'season'
+ t['info']['mediatype'] = 'season'
+ t['subscribed'] = self.is_subscribed_vod(data['Productos'])
+
+ return t
+
+ def get_list(self, data):
+ res = []
+ for d in data:
+ #print_json(d)
+ t = self.get_title(d)
+ if t and t['id'] != '':
+ res.append(t)
+ return res
+
+ def get_seasons(self, id):
+ url = self.endpoints['ficha'].format(deviceType='webplayer', id=id, profile=self.account['platform'], mediatype='FOTOV', version='7.1', mode='GLOBAL', catalog='', channels='', state='', mdrm='true', demarcation=self.account['demarcation'], legacyBoxOffice='')
+ data = self.net.load_data(url)
+ #print_json(data)
+ res = []
+ c = 1
+ for d in data['Temporadas']:
+ t = {}
+ t['id'] = d['Id']
+ t['info'] = {}
+ t['art'] = {}
+ t['type'] = 'season'
+ t['info']['mediatype'] = 'season'
+ t['info']['title'] = d['Titulo']
+ t['info']['tvshowtitle'] = data['TituloSerie']
+ t['info']['plot'] = data['Descripcion']
+ t['info']['season'] = c
+ t['art']['poster'] = t['art']['thumb'] = data['Imagen']
+ t['subscribed'] = self.is_subscribed_vod(data['Productos'])
+ c += 1
+ res.append(t)
+
+ return res
+
+ def get_episodes(self, id):
+ url = self.endpoints['ficha'].format(deviceType='webplayer', id=str(id), profile=self.account['platform'], mediatype='FOTOV', version='7.1', mode='GLOBAL', catalog='', channels='', state='', mdrm='true', demarcation=self.account['demarcation'], legacyBoxOffice='')
+ #print(url)
+ data = self.net.load_data(url)
+ #print_json(data)
+ res = []
+ for d in data['Episodios']:
+ ed = d['DatosEditoriales']
+ t = {}
+ t['id'] = ed['Id']
+ t['info'] = {}
+ t['art'] = {}
+ t['type'] = 'movie'
+ t['info']['mediatype'] = 'episode'
+ t['stream_type'] = 'vod'
+ t['info']['title'] = ed['TituloEpisodio']
+ t['info']['episode'] = ed['NumeroEpisodio']
+ t['info']['duration'] = ed['DuracionEnSegundos']
+ t['art']['thumb'] = ed['Imagen']
+ for im in ed['Imagenes']:
+ if im['id'] == 'horizontal': t['art']['thumb'] = im['uri']
+ m = re.search(r'T(\d+)', ed['Temporada'])
+ if m: t['info']['season'] = m.group(1)
+ t['url'] = ''
+ t['session_request'] = ''
+ if len(d['VodItems']) > 0:
+ video = d['VodItems'][0]
+ t['subscribed'] = self.is_subscribed_vod(video['Productos'])
+ t['url'] = video['UrlVideo']
+ if video['AssetType'] == 'VOD':
+ t['session_request'] = '{"contentID":' + str(t['id']) + ',"drmMediaID":"' + video['CasId'] +'", "streamType":"AST"}'
+ elif video['AssetType'] == 'U7D':
+ t['stream_type'] = 'u7d'
+ t['session_request'] = '{"contentID":' + str(t['id']) + ', "streamType":"CUTV"}'
+
+ res.append(t)
+
+ return res
+
+ def get_u7d_url(self, url):
+ headers = self.net.headers.copy()
+ headers['Content-Type'] = 'application/json'
+ headers['X-HZId'] = self.account['session_token']
+ data = self.net.load_data(url, headers)
+ return data
+
+ def get_subtitles(self, manifest_url):
+ base_url = os.path.dirname(manifest_url)
+ content = self.net.load_url(manifest_url)
+ rx = r'.*?(.*?)<\/BaseURL>'
+ matches = re.findall(rx, content, flags=re.MULTILINE | re.DOTALL)
+ res = []
+ for m in matches:
+ sub = {}
+ sub['lang'] = m[0]
+ sub['filename'] = m[1]
+ sub['url'] = base_url +'/' + sub['filename']
+ if sub['lang'] == 'srd': sub['lang'] = 'es [CC]'
+ if sub['lang'] == 'qaa': sub['lang'] = 'en [VO]'
+ res.append(sub)
+ return res
+
+ def download_subtitles(self, sublist):
+ output_dir = self.cache.config_directory + 'subtitles'
+ #LOG('output_dir: {}'.format(output_dir))
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+ res = []
+ for s in sublist:
+ filename = output_dir + os.sep + s['lang'] + '.ttml'
+ LOG('filename: {}'.format(filename))
+ content = self.net.load_url(s['url'])
+ with io.open(filename, 'w', encoding='utf-8', newline='') as handle:
+ handle.write(content)
+ res.append(filename)
+ return res
+
+ def get_vod_list_url(self, cat='movies'):
+ #sort = 'FD'
+ sort = 'MA'
+ url = self.endpoints['consultar'].format(deviceType='webplayer', profile=self.account['platform'], sort=sort, start=1, end=50, mdrm='true', demarcation=self.account['demarcation'])
+ filter = '&filter=AC-OM,AC-MA,MA-GBLCICLO59,TD-CUP,RG-SINCAT,AC-PREPUB,' + self.entitlements['suscripcion']
+ url += '&mode=VOD' + filter
+ if cat == 'tvshows':
+ url += '&topic=SR&showSeries=series'
+ elif cat == 'movies':
+ url += '&topic=CN'
+ elif cat == 'documentaries':
+ url += '&topic=DC'
+ elif cat == 'kids':
+ url += '&topic=IN'
+ #LOG('vod url: {}'.format(url))
+ return url
diff --git a/resources/lib/network.py b/resources/lib/network.py
new file mode 100644
index 0000000..2b4ca54
--- /dev/null
+++ b/resources/lib/network.py
@@ -0,0 +1,38 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+import json
+import requests
+
+from .log import LOG, print_json
+
+class Network(object):
+ headers = {}
+ session = requests.Session()
+
+ def load_url(self, url, headers = None):
+ if headers is None: headers = self.headers
+ response = self.session.get(url, headers=headers, allow_redirects=True)
+ content = response.content.decode('utf-8')
+ return content
+
+ def load_data(self, url, headers = None):
+ content = self.load_url(url, headers)
+ try:
+ data = json.loads(content)
+ return data
+ except:
+ return {'error': content}
+
+ def post_data(self, url, data, headers = None):
+ if headers is None: headers = self.headers
+ #LOG('post_data: {}'.format(data))
+ #print_json(headers)
+ response = self.session.post(url, headers=headers, data=data)
+ content = response.content.decode('utf-8')
+ #LOG(content)
+ data = json.loads(content)
+ return data
diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py
new file mode 100644
index 0000000..33bdb62
--- /dev/null
+++ b/resources/lib/plugin.py
@@ -0,0 +1,486 @@
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+import sys
+import json
+
+import xbmc
+import xbmcgui
+import xbmcplugin
+
+if sys.version_info[0] >= 3:
+ import urllib.request as urllib2
+ from urllib.parse import urlencode, parse_qsl, quote_plus
+ unicode = str
+else:
+ import urllib2
+ from urllib import urlencode, quote_plus
+ from urlparse import parse_qsl
+
+import xbmcaddon
+import os.path
+
+from .log import LOG
+from .movistar import *
+from .addon import *
+from .gui import *
+
+# Get the plugin url in plugin:// notation.
+_url = sys.argv[0]
+# Get the plugin handle as an integer number.
+_handle = int(sys.argv[1])
+
+try: # Kodi >= 19
+ from xbmcvfs import translatePath
+except ImportError: # Kodi 18
+ from xbmc import translatePath
+
+
+def get_url(**kwargs):
+ return '{0}?{1}'.format(_url, urlencode(kwargs))
+
+def play(params):
+ LOG('play - params: {}'.format(params))
+
+ if not 'url' in params: return
+
+ url = params['url']
+ channel_id = params['id']
+ stype = params['stype']
+
+ token = m.account['ssp_token']
+ session_token = m.account['session_token']
+
+ import inputstreamhelper
+ is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
+ if not is_helper.check_inputstream():
+ show_notification(addon.getLocalizedString(30202))
+ return
+
+ """
+ if addon.getSettingBool('reregister'):
+ m.unregister_device()
+ m.register_device()
+ """
+
+ if addon.getSettingBool('open_session') or stype == 'vod':
+ d = m.open_session(params['session_request'], session_token)
+ LOG('Open session: d: {}'.format(d))
+ if d['resultCode'] != 0:
+ dialog = xbmcgui.Dialog().notification('Error', d['resultText'], xbmcgui.NOTIFICATION_ERROR, 5000)
+ return
+ if 'resultData' in d and 'cToken' in d['resultData']:
+ token = d['resultData']['cToken']
+
+ if not addon.getSettingBool('open_session'):
+ d = m.delete_session()
+ LOG('Delete session: d: {}'.format(d))
+
+ LOG("*** TEST token: {} session_token: {}".format(token, session_token))
+
+ #url = 'https://vamos-nmp-movistarplus.emisiondof6.com/hls/vamos.isml/index-02-spa.m3u8'
+
+ if stype in ['u7d', 'rec']:
+ d = m.get_u7d_url(url)
+ url = d['url']
+
+ proxy = m.cache.load_file('proxy.conf')
+ if addon.getSettingBool('manifest_modification') and proxy:
+ url = '{}/?manifest={}'.format(proxy, url)
+
+ LOG("*** TEST url: {}".format(url))
+
+ headers = 'Content-Type=application/octet-stream'
+ headers += '&User-Agent=Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
+ headers += '&Accept=*/*&Accept-Language=es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3'
+ headers += '&Referer=https://ver.movistarplus.es/'
+ headers += '&Origin=https://ver.movistarplus.es&Connection=keep-alive'
+ headers += '&Host=wv-ottlic-f3.imagenio.telefonica.net'
+
+ license_url = 'https://wv-ottlic-f3.imagenio.telefonica.net/TFAESP/wvls/contentlicenseservice/v1/licenses'
+ certificate = (
+ 'CsECCAMSEBcFuRfMEgSGiwYzOi93KowYgrSCkgUijgIwggEKAoIBAQCZ7Vs7Mn2rXiTvw7YqlbWY'
+ 'UgrVvMs3UD4GRbgU2Ha430BRBEGtjOOtsRu4jE5yWl5KngeVKR1YWEAjp+GvDjipEnk5MAhhC28V'
+ 'jIeMfiG/+/7qd+EBnh5XgeikX0YmPRTmDoBYqGB63OBPrIRXsTeo1nzN6zNwXZg6IftO7L1KEMpH'
+ 'SQykfqpdQ4IY3brxyt4zkvE9b/tkQv0x4b9AsMYE0cS6TJUgpL+X7r1gkpr87vVbuvVk4tDnbNfF'
+ 'XHOggrmWEguDWe3OJHBwgmgNb2fG2CxKxfMTRJCnTuw3r0svAQxZ6ChD4lgvC2ufXbD8Xm7fZPvT'
+ 'CLRxG88SUAGcn1oJAgMBAAE6FGxpY2Vuc2Uud2lkZXZpbmUuY29tEoADrjRzFLWoNSl/JxOI+3u4'
+ 'y1J30kmCPN3R2jC5MzlRHrPMveoEuUS5J8EhNG79verJ1BORfm7BdqEEOEYKUDvBlSubpOTOD8S/'
+ 'wgqYCKqvS/zRnB3PzfV0zKwo0bQQQWz53ogEMBy9szTK/NDUCXhCOmQuVGE98K/PlspKkknYVeQr'
+ 'OnA+8XZ/apvTbWv4K+drvwy6T95Z0qvMdv62Qke4XEMfvKUiZrYZ/DaXlUP8qcu9u/r6DhpV51Wj'
+ 'x7zmVflkb1gquc9wqgi5efhn9joLK3/bNixbxOzVVdhbyqnFk8ODyFfUnaq3fkC3hR3f0kmYgI41'
+ 'sljnXXjqwMoW9wRzBMINk+3k6P8cbxfmJD4/Paj8FwmHDsRfuoI6Jj8M76H3CTsZCZKDJjM3BQQ6'
+ 'Kb2m+bQ0LMjfVDyxoRgvfF//M/EEkPrKWyU2C3YBXpxaBquO4C8A0ujVmGEEqsxN1HX9lu6c5OMm'
+ '8huDxwWFd7OHMs3avGpr7RP7DUnTikXrh6X0')
+
+ play_item = xbmcgui.ListItem(path= url)
+ play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
+ #play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
+ if format(addon.getSetting('drm_type')) == 'Playready':
+ play_item.setProperty('inputstream.adaptive.license_type', 'com.microsoft.playready')
+ else:
+ play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
+
+ if addon.getSettingBool('use_proxy_for_license') and proxy:
+ request_id = str(int(time.time()*1000))
+ license_url = '{}/license?token={}&stype={}&session_request={}&session_token={}&request_id={}||R{{SSM}}|'.format(proxy, quote_plus(token), stype, quote_plus(params['session_request']), quote_plus(session_token), request_id)
+ LOG('license_url: {}'.format(license_url))
+ play_item.setProperty('inputstream.adaptive.license_key', license_url)
+ else:
+ play_item.setProperty('inputstream.adaptive.license_key', '{}|{}&nv-authorizations={}|R{{SSM}}|'.format(license_url, headers, token))
+ play_item.setProperty('inputstream.adaptive.stream_headers', 'User-Agent=Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0')
+ play_item.setProperty('inputstream.adaptive.server_certificate', certificate)
+ #play_item.setProperty('inputstream.adaptive.license_flags', 'persistent_storage')
+ #play_item.setProperty('inputstream.adaptive.license_flags', 'force_secure_decoder')
+
+ if sys.version_info[0] < 3:
+ play_item.setProperty('inputstreamaddon', 'inputstream.adaptive')
+ else:
+ play_item.setProperty('inputstream', 'inputstream.adaptive')
+
+ play_item.setMimeType('application/dash+xml')
+ play_item.setContentLookup(False)
+
+ # Subtitles
+ if stype == 'vod' and addon.getSettingBool('use_ttml2ssa'):
+ # Convert subtitles
+ from ttml2ssa import Ttml2SsaAddon
+ ttml = Ttml2SsaAddon()
+ #ttml.use_language_filter = False
+ subtype = ttml.subtitle_type()
+
+ subfolder = profile_dir + os.sep +'subtitles/'
+ if not os.path.exists(subfolder):
+ os.makedirs(subfolder)
+
+ sublist = m.get_subtitles(url)
+ LOG('sublist: {}'.format(sublist))
+ subpaths = []
+ for sub in sublist:
+ filename = subfolder + sub['lang']
+ LOG('Converting {}'.format(sub['filename']))
+ response = m.net.session.get(sub['url'], allow_redirects=True)
+ content = response.content
+ with io.open(filename + '.ttml', 'w', encoding='utf-8', newline='') as handle:
+ handle.write(content.decode('utf-8'))
+ ttml.parse_ttml_from_string(content)
+ if subtype != 'srt':
+ filename_ssa = filename + '.ssa'
+ ttml.write2file(filename_ssa)
+ subpaths.append(filename_ssa)
+ if subtype != 'ssa':
+ filename_srt = filename
+ if (subtype == 'both'): filename_srt += '.SRT'
+ filename_srt += '.srt'
+ ttml.write2file(filename_srt)
+ subpaths.append(filename_srt)
+ play_item.setSubtitles(subpaths)
+
+ xbmcplugin.setResolvedUrl(_handle, True, listitem=play_item)
+
+ player.running = True
+ monitor = xbmc.Monitor()
+ while not monitor.abortRequested() and player.running:
+ if monitor.waitForAbort(1):
+ break
+ LOG('Exiting play')
+
+
+def add_videos(category, ctype, videos, ref=None, url_next=None, url_prev=None):
+ #LOG("*** TEST category: {} ctype: {}".format(category, ctype))
+ xbmcplugin.setPluginCategory(_handle, category)
+ xbmcplugin.setContent(_handle, ctype)
+
+ if ctype == 'movies' or ctype == 'seasons':
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_UNSORTED)
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_LABEL)
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_TITLE)
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_GENRE)
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_LASTPLAYED)
+ if ctype == 'episodes':
+ xbmcplugin.addSortMethod(_handle, xbmcplugin.SORT_METHOD_EPISODE)
+
+ """
+ if url_prev:
+ list_item = xbmcgui.ListItem(label = addon.getLocalizedString(30110)) # Previous page
+ xbmcplugin.addDirectoryItem(_handle, get_url(action=ref, url=url_prev, name=category), list_item, True)
+ """
+
+ for t in videos:
+ #LOG("*** TEST t: {}".format(t))
+ if 'subscribed' in t:
+ if addon.getSettingBool('only_subscribed') and t['subscribed'] == False: continue
+ t['info']['title'] = m.colorize_title(t)
+ title_name = t['info']['title'].encode('utf-8')
+ if not 'type' in t: continue
+ if t['type'] == 'movie':
+ list_item = xbmcgui.ListItem(label = title_name)
+ #if t['stream_type'] == 'tv':
+ # action = get_url(action='epg', id=t['id'], name=t['name'].encode('utf-8'))
+ # LOG('action: {}'.format(action))
+ # list_item.addContextMenuItems([('EPG', "RunPlugin(" + action + ")")])
+ if t['url'] == '':
+ t['info']['title'] = '[COLOR gray]' + t['info']['title'] +'[/COLOR]'
+ list_item.setProperty('IsPlayable', 'true')
+ list_item.setInfo('video', t['info'])
+ list_item.setArt(t['art'])
+ url = get_url(action='play', id=t['id'], url=t['url'], session_request=t['session_request'], stype=t['stream_type'])
+ xbmcplugin.addDirectoryItem(_handle, url, list_item, False)
+ elif t['type'] == 'series':
+ list_item = xbmcgui.ListItem(label = title_name)
+ list_item.setInfo('video', t['info'])
+ list_item.setArt(t['art'])
+ xbmcplugin.addDirectoryItem(_handle, get_url(action='series', id=t['id'], name=title_name), list_item, True)
+ elif t['type'] == 'season':
+ list_item = xbmcgui.ListItem(label = title_name)
+ list_item.setInfo('video', t['info'])
+ list_item.setArt(t['art'])
+ xbmcplugin.addDirectoryItem(_handle, get_url(action='season', id=t['id'], name=title_name), list_item, True)
+ elif t['type'] == 'category':
+ list_item = xbmcgui.ListItem(label = title_name)
+ #list_item.setInfo('video', t['info'])
+ #list_item.setArt(t['art'])
+ xbmcplugin.addDirectoryItem(_handle, get_url(action='category', id=t['id'], name=title_name), list_item, True)
+
+ if url_next:
+ list_item = xbmcgui.ListItem(label = addon.getLocalizedString(30109)) # Next page
+ xbmcplugin.addDirectoryItem(_handle, get_url(action=ref, url=url_next, name=category), list_item, True)
+
+ xbmcplugin.endOfDirectory(_handle)
+
+def list_devices(params):
+ LOG('list_devices: params: {}'.format(params))
+
+ devices = m.get_devices(False)
+
+ if 'id' in params:
+ if params['name'] == 'select':
+ LOG('Selecting device {}'.format(params['id']))
+ m.change_device(params['id'])
+ elif params['name'] == 'close':
+ LOG('Closing session {}'.format(params['id']))
+ m.delete_session(params['id'])
+ elif params['name'] == 'delete':
+ LOG('Removing device {}'.format(params['id']))
+ m.delete_device(params['id'])
+ if params['id'] == m.account['device_id']:
+ LOG('Registering device')
+ m.register_device()
+
+ xbmc.executebuiltin("Container.Refresh")
+ return
+
+ open_folder(addon.getLocalizedString(30108)) # Devices
+
+ for d in devices:
+ name = d['name'] +' - '+ d['type'] + ' (' + d['reg_date'] + ')'
+ if d['playing']: name += ' [' + d['playing'] + ']'
+ name += ' ['+ d['id'][:5] +']'
+ if d['id'] == m.account['device_id']:
+ name = '[B][COLOR blue]' + name + '[/COLOR][/B]'
+
+ select_action = get_url(action='devices', id=d['id'], name='select')
+ close_action = get_url(action='devices', id=d['id'], name='close')
+ remove_action = get_url(action='devices', id=d['id'], name='delete')
+ cm = [(addon.getLocalizedString(30150), "RunPlugin(" + close_action + ")"),
+ (addon.getLocalizedString(30151), "RunPlugin(" + remove_action + ")")]
+ if d['type_code'] == 'WP':
+ cm.insert(0, (addon.getLocalizedString(30152), "RunPlugin(" + select_action + ")"))
+ default_action = select_action
+ else:
+ default_action = close_action
+ add_menu_option(name, default_action, cm)
+
+ close_folder(cacheToDisc=False)
+
+def list_profiles(params):
+ LOG('list_profiles: params: {}'.format(params))
+
+ profiles = m.get_profiles()
+
+ if 'id' in params:
+ if params['name'] == 'select':
+ LOG('Selecting profile {}'.format(params['id']))
+ m.change_profile(params['id'])
+ xbmc.executebuiltin("Container.Refresh")
+ return
+
+ open_folder(addon.getLocalizedString(30180)) # Profiles
+ for p in profiles:
+ name = p['name']
+ if p['id'] == m.account['profile_id']:
+ name = '[B][COLOR blue]' + name + '[/COLOR][/B]'
+ select_action = get_url(action='profiles', id=p['id'], name='select')
+ add_menu_option(name, select_action)
+ close_folder(cacheToDisc=False)
+
+def list_epg(params):
+ LOG('list_epg: {}'.format(params))
+ if 'id' in params:
+ epg = m.get_epg()
+ xbmcplugin.setPluginCategory(_handle, params['name'])
+ xbmcplugin.setContent(_handle, 'files')
+ for p in epg[params['id']]:
+ name = '[B]' + p['start_str'] + '[/B] ' + p['desc1']
+ if p['desc2']: name += ' - '+ p['desc2']
+ plot = name +"\n" + p['desc2']
+ list_item = xbmcgui.ListItem(label=name)
+ list_item.setInfo('video', {'name':name, 'plot':plot})
+ xbmcplugin.addDirectoryItem(_handle, '', list_item, False)
+ xbmcplugin.endOfDirectory(_handle)
+ else:
+ channels = m.get_channels()
+ open_folder(addon.getLocalizedString(30107)) # EPG
+ for t in channels:
+ name = t['info']['title']
+ add_menu_option(name, get_url(action='epg', id=t['id'], name=name.encode('utf-8')), art=t['art'])
+ close_folder()
+
+def listing(name, url):
+ data = m.download_list(url, use_hz=False)
+ l = m.get_list(data['Contenidos'])
+ url_next = data['next']['href'] if isinstance(data['next'], dict) and 'href' in data['next'] else None
+ url_prev = data['prev']['href'] if isinstance(data['next'], dict) and 'prev' in data['next'] else None
+ add_videos(name, 'movies', l, url_next=url_next, url_prev=url_prev, ref='listing')
+
+def listing_hz(name, url):
+ data = m.download_list(url, use_hz=True)
+ l = m.get_list(data['Contenidos'])
+ url_next = data['next']['href'] if isinstance(data['next'], dict) and 'href' in data['next'] else None
+ url_prev = data['prev']['href'] if isinstance(data['next'], dict) and 'prev' in data['next'] else None
+ add_videos(name, 'movies', l, url_next=url_next, url_prev=url_prev, ref='listing_hz')
+
+def list_vod():
+ open_folder(addon.getLocalizedString(30111)) # VOD
+ name = addon.getLocalizedString(30105).encode('utf-8')
+ add_menu_option(name, get_url(action='listing', name=name, url=m.get_vod_list_url(cat='movies'))) # Movies
+
+ name = addon.getLocalizedString(30106).encode('utf-8')
+ add_menu_option(name, get_url(action='listing', name=name, url=m.get_vod_list_url(cat='tvshows'))) # TV Shows
+
+ name = addon.getLocalizedString(30120).encode('utf-8')
+ add_menu_option(name, get_url(action='listing', name=name, url=m.get_vod_list_url(cat='documentaries'))) # Documentaries
+
+ name = addon.getLocalizedString(30121).encode('utf-8')
+ add_menu_option(name, get_url(action='listing', name=name, url=m.get_vod_list_url(cat='kids'))) # Kids
+ close_folder()
+
+def login_with_key():
+ filename = xbmcgui.Dialog().browseSingle(1, addon.getLocalizedString(30182), '', '.key')
+ if filename:
+ m.install_key_file(filename)
+ m.cache.remove_file('access_token.conf')
+ m.cache.remove_file('account.json')
+ m.cache.remove_file('device_id.conf')
+ m.cache.remove_file('devices.json')
+ m.cache.remove_file('profile_id.conf')
+ m.cache.remove_file('tokens.json')
+
+def router(paramstring):
+ """
+ Router function that calls other functions
+ depending on the provided paramstring
+ :param paramstring: URL encoded plugin paramstring
+ :type paramstring: str
+ """
+
+ params = dict(parse_qsl(paramstring))
+ LOG('params: {}'.format(params))
+ if params:
+ if params['action'] == 'play':
+ play(params)
+ elif params['action'] == 'tv':
+ if addon.getSettingBool('channels_with_epg'):
+ channels = m.get_channels_with_epg()
+ else:
+ channels = m.get_channels()
+ add_videos(addon.getLocalizedString(30104), 'movies', channels)
+ elif params['action'] == 'devices':
+ list_devices(params)
+ elif params['action'] == 'profiles':
+ list_profiles(params)
+ elif params['action'] == 'login_with_key':
+ login_with_key()
+ elif params['action'] == 'logout':
+ pass
+ elif params['action'] == 'epg':
+ list_epg(params)
+ elif params['action'] == 'wishlist':
+ # Wishlist
+ listing_hz(addon.getLocalizedString(30102), m.get_wishlist_url())
+ elif params['action'] == 'recordings':
+ # Recordings
+ listing_hz(addon.getLocalizedString(30103), m.get_recordings_url())
+ elif params['action'] == 'listing':
+ listing(params['name'], params['url'])
+ elif params['action'] == 'listing_hz':
+ listing_hz(params['name'], params['url'])
+ elif params['action'] == 'series':
+ add_videos(params['name'], 'seasons', m.get_seasons(params['id']))
+ elif params['action'] == 'season':
+ add_videos(params['name'], 'episodes', m.get_episodes(params['id']))
+ elif params['action'] == 'vod':
+ list_vod()
+ else:
+ # Main
+ open_folder(addon.getLocalizedString(30101)) # Menu
+ xbmcplugin.setContent(_handle, 'files')
+
+ if m.logged:
+ add_menu_option(addon.getLocalizedString(30104), get_url(action='tv')) # TV
+ add_menu_option(addon.getLocalizedString(30107), get_url(action='epg')) # EGP
+ add_menu_option(addon.getLocalizedString(30102), get_url(action='wishlist')) # My list
+ add_menu_option(addon.getLocalizedString(30103), get_url(action='recordings')) # Recordings
+ add_menu_option(addon.getLocalizedString(30111), get_url(action='vod')) # VOD
+ add_menu_option(addon.getLocalizedString(30180), get_url(action='profiles')) # Profiles
+ add_menu_option(addon.getLocalizedString(30108), get_url(action='devices')) # Devices
+ #add_menu_option(addon.getLocalizedString(30150), get_url(action='logout')) # Close session
+
+ add_menu_option(addon.getLocalizedString(30181), get_url(action='login_with_key')) # Login with key
+ close_folder(cacheToDisc=False)
+
+
+class Player(xbmc.Player):
+ running = False
+
+ def onAVStarted(self):
+ LOG('Playback started')
+
+ def onPlayBackPaused(self):
+ LOG('Playback paused')
+
+ def onPlayBackResumed(self):
+ LOG('Playback resumed')
+
+ def onPlayBackEnded(self):
+ LOG('Playback ended')
+ self.close_session()
+
+ def onPlayBackStopped(self):
+ LOG('Playback stopped')
+ self.close_session()
+
+ def close_session(self):
+ if self.running:
+ LOG('Closing session')
+ d = m.delete_session()
+ LOG('delete_session: {}'.format(d))
+ self.running = False
+
+def run():
+ global m
+ reuse_devices = addon.getSettingBool('reuse_devices')
+ LOG('profile_dir: {}'.format(profile_dir))
+ LOG('reuse_devices: {}'.format(reuse_devices))
+ m = Movistar(profile_dir, reuse_devices=reuse_devices)
+
+ global player
+ player = Player()
+
+ # Call the router function and pass the plugin call parameters to it.
+ # We use string slicing to trim the leading '?' from the plugin call paramstring
+ router(sys.argv[2][1:])
diff --git a/resources/lib/proxy.py b/resources/lib/proxy.py
new file mode 100644
index 0000000..dc6131b
--- /dev/null
+++ b/resources/lib/proxy.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+try: # Python 3
+ from http.server import BaseHTTPRequestHandler, HTTPServer
+except ImportError: # Python 2
+ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+
+try: # Python 3
+ from socketserver import TCPServer, ThreadingMixIn
+except ImportError: # Python 2
+ from SocketServer import TCPServer, ThreadingMixIn
+
+try: # Python 3
+ from urllib.parse import unquote, quote_plus
+except ImportError: # Python 2
+ from urllib import unquote, quote_plus
+
+try: # Python 3
+ from urllib.parse import parse_qsl
+except: # Python 2
+ from urlparse import parse_qsl
+
+"""
+try: # Python 3
+ from SocketServer import ThreadingTCPServer
+except ImportError: # Python 2
+ from socketserver import ThreadingTCPServer
+"""
+
+import os
+import re
+import json
+import requests
+import threading
+import socket
+import base64
+from contextlib import closing
+import xbmcaddon
+from .log import LOG
+from .addon import profile_dir
+
+from ttml2ssa import Ttml2SsaAddon
+ttml = Ttml2SsaAddon()
+
+session = requests.Session()
+session.headers.update({'user-agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'})
+previous_token = ''
+
+reregister_needed = False
+
+def is_ascii(s):
+ try:
+ return s.isascii()
+ except:
+ return all(ord(c) < 128 for c in s)
+
+def try_load_json(text):
+ try:
+ return json.loads(text)
+ except:
+ return None
+
+mvs_o = None
+def mvs():
+ global mvs_o
+ if not mvs_o:
+ from .movistar import Movistar
+ reuse_devices = xbmcaddon.Addon().getSettingBool('reuse_devices')
+ mvs_o = Movistar(profile_dir, reuse_devices)
+ return mvs_o
+
+
+class RequestHandler(BaseHTTPRequestHandler):
+
+ def do_GET(self):
+ """Handle http get requests, used for manifest"""
+ path = self.path # Path with parameters received from request e.g. "/manifest?id=234324"
+ print('HTTP GET Request received to {}'.format(path))
+ try:
+ #if True:
+ if 'manifest' in path:
+ pos = path.find('=')
+ url = path[pos+1:]
+ LOG('url: {}'.format(url))
+ response = session.get(url, allow_redirects=True)
+ LOG('headers: {}'.format(response.headers))
+ baseurl = os.path.dirname(response.url)
+ LOG('baseurl: {}'.format(baseurl))
+ content = response.content.decode('utf-8')
+ pos = content.find(' -1:
+ content = content[:pos] + '' + baseurl + '/' + content[pos:]
+ content = content.replace('lang="qaa"', 'lang="eng"')
+ if not xbmcaddon.Addon().getSettingBool('use_ttml2ssa'):
+ LOG(self.headers.get('Host'))
+ my_address = 'http://' + self.headers.get('Host')
+ LOG('my_address: {}'.format(my_address))
+ content = content.replace('mimeType="application/ttml+xml"', 'mimeType="text/vtt"')
+ content = re.sub(r'(.*?)\.ttml<\/BaseURL>', r'{}/?subtitle={}/\1.ttml'.format(my_address, baseurl), content)
+ #LOG('content: {}'.format(content))
+ manifest_data = content
+ self.send_response(200)
+ #self.send_header('Content-type', 'application/xml')
+ self.send_header('Content-type', 'text/plain')
+ self.end_headers()
+ self.wfile.write(manifest_data.encode('utf-8'))
+ elif 'subtitle' in path:
+ pos = path.find('=')
+ url = path[pos+1:]
+ LOG('subtitle url: {}'.format(url))
+ response = session.get(url, allow_redirects=True)
+ content = response.content
+ ttml.parse_ttml_from_string(content)
+ sub_data = ttml.generate_vtt()
+ self.send_response(200)
+ self.send_header('Content-type', 'text/plain')
+ self.end_headers()
+ self.wfile.write(sub_data.encode('utf-8'))
+ elif 'fragment' in path:
+ pos = path.find('=')
+ url = path[pos+1:]
+ LOG('url: {}'.format(url))
+ self.send_response(301)
+ self.send_header('Location', url)
+ self.end_headers()
+ else:
+ self.send_response(404)
+ self.end_headers()
+ except Exception:
+ self.send_response(500)
+ self.end_headers()
+
+ def do_POST(self):
+ """Handle http post requests, used for license"""
+ path = self.path # Path with parameters received from request e.g. "/license?id=234324"
+ print('HTTP POST Request received to {}'.format(path))
+ if '/license' not in path:
+ self.send_response(404)
+ self.end_headers()
+ return
+ try:
+ #if True:
+ pos = path.find('?')
+ path = path[pos+1:]
+ params = dict(parse_qsl(path))
+ LOG('params: {}'.format(params))
+
+ length = int(self.headers.get('content-length', 0))
+ isa_data = self.rfile.read(length)
+ LOG('isa_data length: {}'.format(length))
+ LOG('isa_data: {}'.format(base64.b64encode(isa_data)))
+
+ token = params['token']
+ LOG('token: {}'.format(token))
+
+ global previous_token, reregister_needed
+ while True:
+ LOG('reregister_needed: {}'.format(reregister_needed))
+ if reregister_needed:
+ mvs().unregister_device()
+ mvs().register_device()
+
+ if previous_token == token and params['stype'] == 'vod':
+ d = mvs().open_session(params['session_request'], params['session_token'])
+ LOG('open_session: d: {}'.format(d))
+ if 'resultData' in d and 'cToken' in d['resultData']:
+ token = d['resultData']['cToken']
+ LOG('new token: {}'.format(token))
+ previous_token = token
+
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0',
+ 'Accept': '*/*',
+ 'Accept-Language': 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3',
+ 'Referer': 'https://ver.movistarplus.es/',
+ 'nv-authorizations': quote_plus(token),
+ 'Origin': 'https://ver.movistarplus.es',
+ }
+ LOG('headers: {}'.format(headers))
+
+ url = 'https://wv-ottlic-f3.imagenio.telefonica.net/TFAESP/wvls/contentlicenseservice/v1/licenses'
+ response = session.post(url, data=isa_data, headers=headers)
+ license_data = response.content
+ LOG('license response length: {}'.format(len(license_data)))
+ if is_ascii(license_data):
+ LOG('license response: {}'.format(license_data))
+ d = try_load_json(license_data)
+ if d and 'errorCode' in d:
+ from .gui import show_notification
+ show_notification('Error {}: {}'.format(d['errorCode'], d['message']))
+ if d['errorCode'] == 4027:
+ if not reregister_needed and xbmcaddon.Addon().getSettingBool('reregister'):
+ reregister_needed = True
+ else:
+ break
+ else:
+ LOG('license response: {}'.format(base64.b64encode(license_data)))
+ break
+
+ self.send_response(response.status_code)
+ self.end_headers()
+ self.wfile.write(license_data)
+ except Exception:
+ self.send_response(500)
+ self.end_headers()
+
+
+HOST = '127.0.0.1'
+PORT = 57011
+
+class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
+ daemon_threads = True
+
+class Proxy(object):
+ started = False
+
+ def check_port(self, port=0, default=False):
+ try:
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
+ s.bind((HOST, port))
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ return s.getsockname()[1]
+ except:
+ return default
+
+ def start(self):
+ if self.started:
+ return
+
+ port = self.check_port(PORT)
+ if not port:
+ port = self.check_port(0)
+ LOG('port: {}'.format(port))
+
+ self._server = ThreadedHTTPServer((HOST, port), RequestHandler)
+ self._server.allow_reuse_address = True
+ self._httpd_thread = threading.Thread(target=self._server.serve_forever)
+ self._httpd_thread.start()
+ self.proxy_address = 'http://{}:{}'.format(HOST, port)
+ self.started = True
+ LOG("Proxy Started: {}:{}".format(HOST, port))
+
+ def stop(self):
+ if not self.started:
+ return
+
+ self._server.shutdown()
+ self._server.server_close()
+ self._server.socket.close()
+ self._httpd_thread.join()
+ self.started = False
+ LOG("Proxy: Stopped")
diff --git a/resources/lib/timeconv.py b/resources/lib/timeconv.py
new file mode 100644
index 0000000..011c84d
--- /dev/null
+++ b/resources/lib/timeconv.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# encoding: utf-8
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import unicode_literals, absolute_import, division
+
+from datetime import datetime, timedelta
+
+def timestamp2str(timestamp):
+ time = datetime.fromtimestamp(timestamp / 1000)
+ return time.strftime("%H:%M")
+
+def isodate2str(iso_str):
+ iso_str = iso_str[0:19]
+ try:
+ dt = datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%S")
+ except:
+ return iso_str
+
+ try:
+ import pytz
+ utc_tz = pytz.timezone('UTC')
+ dt = utc_tz.localize(dt)
+ dt = dt.astimezone(pytz.timezone('Europe/Madrid'))
+ except:
+ pass
+
+ dt = dt.strftime('%d/%m/%Y %H:%M:%S')
+ return dt
+
diff --git a/resources/settings.xml b/resources/settings.xml
new file mode 100644
index 0000000..087f1d9
--- /dev/null
+++ b/resources/settings.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/service.py b/service.py
new file mode 100644
index 0000000..12199a9
--- /dev/null
+++ b/service.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+import os
+import xbmc
+from resources.lib.proxy import Proxy
+from resources.lib.addon import *
+from resources.lib.log import LOG
+from resources.lib.cache import Cache
+
+if __name__ == '__main__':
+ LOG('Service started')
+ proxy = Proxy()
+ proxy.start()
+ LOG('proxy_address: {}'.format(proxy.proxy_address))
+ cache = Cache(profile_dir)
+ cache.save_file('proxy.conf', proxy.proxy_address)
+
+ monitor = xbmc.Monitor()
+ while not monitor.abortRequested():
+ if monitor.waitForAbort(1):
+ break
+
+ proxy.stop()