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()