# encoding: utf-8 # # SPDX-License-Identifier: LGPL-2.1-or-later from __future__ import unicode_literals, absolute_import, division import sys import json import requests import io import os import time import re from datetime import datetime, timedelta from .endpoints import endpoints from .log import LOG, print_json from .network import Network from .cache import Cache from .timeconv import * from .useragent import useragent 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} add_extra_info = True #dplayer = 'webplayer' dplayer = 'android.tv' def __init__(self, config_directory, reuse_devices=False): self.logged = False self.expired_access_token = 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': useragent } self.net = Network() self.net.headers = headers self.quality = 'HD' # or UHD # Cache self.cache = Cache(config_directory) if not os.path.exists(config_directory + 'cache'): os.makedirs(config_directory + 'cache') # Endpoints #self.endpoints = self.get_endpoints() self.endpoints = endpoints # Access token self.load_key_file() # Default access_token if self.account['access_token']: exp = self.get_token_expire_date(self.account['access_token']) #LOG('auth.key: expiring date: {}'.format(exp)) if exp < time.time(): self.expired_access_token = True self.account['access_token'] = None content = self.cache.load_file('access_token.conf') if content: exp = self.get_token_expire_date(content) #LOG('access_token.conf: expiring date: {}'.format(exp)) if exp < time.time(): self.expired_access_token = True else: self.account['access_token'] = content #LOG('access_token: {}'.format(self.account['access_token'])) LOG('expired_access_token: {}'.format(self.expired_access_token)) if not self.account['access_token']: return # 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'] #self.account['platform'] = 'OTT' # 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 self.account['device_id']: self.account['device_id'] = self.account['device_id'].strip('"') else: # Create new device 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']) 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.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', 5) if content: data = json.loads(content) else: LOG('Loading tokens') data = self.get_token() if 'error' in data and self.account['device_id']: # Try registering the device self.register_device() 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.account['pid'] = data['pid'] self.entitlements['activePurchases'] = data['activePurchases'] self.entitlements['partners'] = data['partners'] self.entitlements['activePackages'] = data['activePackages'] self.entitlements['vodSubscription'] = data['vodSubscription'].split(',') if data['linearSubscription'] else [] self.entitlements['linearSubscription'] = data['linearSubscription'].split(',') if data['linearSubscription'] else [] self.entitlements['suscripcion'] = data['suscripcion'] self.entitlements['tvRights'] = data['tvRights'] self.entitlements['distilledTvRights'] = data['distilledTvRights'] #print_json(self.entitlements) self.logged = True # Search data = self.cache.load_file('searchs.json') self.search_list = json.loads(data) if data else [] 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=None, session_id=None): if not session_token: session_token = self.account['session_token'] headers = self.net.headers.copy() headers['Content-Type'] = 'application/json' headers['X-Hzid'] = session_token url = self.endpoints['setUpStream'].format(PID=self.account['pid'], 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, username, password): headers = self.net.headers.copy() if self.account['device_id']: headers['x-movistarplus-deviceid'] = self.account['device_id'] headers['x-movistarplus-ui'] = '2.36.30' headers['x-movistarplus-os'] = 'Linux88' data = { 'grant_type': 'password', 'deviceClass': 'webplayer', 'username': username, 'password': password, } url = self.endpoints['token'] response = self.net.session.post(url, headers=headers, data=data) content = response.content.decode('utf-8') LOG(content) success = False try: d = json.loads(content) if 'access_token' in d: success = True self.save_key_file(d) except: pass return success, 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): 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) #print_json(data) if not isinstance(data, list): return [] res = [] for d in data: if d.get('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.strip('"') #import random #s = '' #for _ in range(0, 32): s += random.choice('abcdef0123456789') #return s 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_cdntoken(self): headers = self.net.headers.copy() headers['Authorization'] = 'Bearer ' + self.account['access_token'] url = self.endpoints['renovacion_cdntoken2'].format(ACCOUNTNUMBER=self.account['id']) data = self.net.post_data(url, None, headers) if isinstance(data, dict): return data.get('access_token') return '' def get_session_token(self): data = {"accountNumber": self.account['id'], "sessionUserProfile": 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['Authorization'] = 'Bearer ' + self.account['access_token'] url = self.endpoints['renovacion_hztoken'].format(deviceType='webplayer', DEVICEID=self.account['device_id']) data = self.net.post_data(url, json.dumps(data), headers) return data.get('homeZoneID') def get_ssp_token(self): headers = self.net.headers.copy() headers['Content-Type'] = 'application/json' headers['Authorization'] = 'Bearer ' + self.account['access_token'] url = self.endpoints['renovacion_ssptoken'].format(deviceType='webplayer', ACCOUNTNUMBER=self.account['id'], DEVICEID=self.account['device_id']) data = self.net.post_data(url, '', headers) return data.get('access_token') def update_session_token(self): new_hz_token = self.get_session_token() if new_hz_token: self.account['session_token'] = new_hz_token return 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.get('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 load_epg_data(self, date_str, duration=2, channels=''): demarcation = self.account['demarcation'] url = self.endpoints['rejilla'].format(deviceType='webplayer', profile=self.account['platform'], UTCDATETIME=date_str, DURATION=duration, CHANNELS=channels, NETWORK='movistarplus', mdrm='true', demarcation=demarcation) if self.quality == 'UHD': url += '&filterQuality=UHD' #LOG(url) data = self.net.load_data(url) return data def get_epg(self, date=None, duration=2, channels=None): if channels or date: if not date: today = datetime.today() date = today.strftime('%Y-%m-%dT00:00:00') epg_data = self.load_epg_data(date, duration, channels if channels else '') if not epg_data or 'error' in epg_data: return {} if channels: data = [] data.append(epg_data) else: data = epg_data else: cache_filename = 'epg_{}.json'.format(self.quality) content = self.cache.load(cache_filename, 6*60) if content: data = json.loads(content) else: today = datetime.today() str_now = today.strftime('%Y-%m-%dT00:00:00') data = self.load_epg_data(str_now) if not 'error' in data: self.cache.save_file(cache_filename, json.dumps(data, ensure_ascii=False)) epg = {} if 'error' in data: return 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['date_str'] = timestamp2str(pr['start'], '%a %d %H:%M') pr['desc1'] = p['Titulo'] pr['desc2'] = '' if 'TituloHorLinea2' in p: pr['desc2'] = p['TituloHorLinea2'] pr['show_id'] = p['ShowId'] if 'SerialId' in p: pr['serie_id'] = p['SerialId'] pr['id'] = p['Id'] #if 'links' in p: pr['links'] = p['links'] epg[id].append(pr) return epg def find_program_epg(self, epg, id, timestamp): found = False c = 0 programs = [] if not id in epg: return 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.get('video_format') == '4K': s += " (4K)" color1 = 'yellow' color2 = 'red' available = True if 'subscribed' in title and title['subscribed'] == False: available = False if 'url' in title and title['url'] == '': available = False if not available: color1 = 'gray' color2 = 'gray' s = '[COLOR gray]' + s +'[/COLOR]' elif 'start' in title: aired = (title['start'] <= (time.time() * 1000)) if not aired: color1 = 'blue' color2 = 'blue' s = '[COLOR blue]' + 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) plot = '' for i in range(len(programs)): desc1 = programs[i]['desc1'] desc2 = programs[i]['desc2'] if i == 0: ch['desc1'] = desc1 ch['desc2'] = desc2 plot += "[B]{}[/B] {} {}\n".format(programs[i]['start_str'], desc1, desc2) if self.add_extra_info: ch['art']['poster'] = ch['art']['fanart'] = None ch['show_id'] = programs[i]['show_id'] self.add_video_extra_info(ch) if 'plot' in ch['info']: plot = plot + ch['info']['plot'] break ch['info']['plot'] = plot 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 for e in self.entitlements['tvRights']: if e 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 return self.is_subscribed_channel(products) def get_channels(self): demarcation = self.account['demarcation'] profile = self.account['platform'] cache_filename = 'channels_{}.json'.format(self.quality) content = self.cache.load(cache_filename) if content: data = json.loads(content) else: url = self.endpoints['canales'].format(deviceType='webplayer', profile=profile, mdrm='true', demarcation=demarcation) if self.quality == 'UHD': url += '&filterQuality=UHD' #LOG(url) data = self.net.load_data(url) if not 'error' in data: self.cache.save_file(cache_filename, json.dumps(data, ensure_ascii=False)) res = [] if 'error' in data: return res for c in data: t = {} t['info'] = {} t['art'] = {} t['type'] = 'movie' t['stream_type'] = 'tv' t['info']['mediatype'] = 'movie' t['channel_name'] = c['Nombre'].strip() t['info']['title'] = str(c.get('Dial', '0')) +'. ' + t['channel_name'] t['id'] = c['CodCadenaTv'] #if add_epg_info: # t['desc1'] = c['Nombre'] # t['desc2'] = '' t['dial'] = c.get('Dial', '0') t['url'] = c['PuntoReproduccion'] if 'Logo' in c: t['art']['icon'] = t['art']['thumb'] = t['art']['poster'] = c['Logo'] elif 'Logos' in c: t['art']['icon'] = t['art']['thumb'] = t['art']['poster'] = c['Logos'][0]['uri'] t['session_request'] = '{"contentID":"'+ t['id'] +'", "streamType":"CHN"}' t['subscribed'] = self.is_subscribed_channel(c.get('tvProducts', [])) 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 add_to_wishlist(self, id, stype='vod'): LOG('add_to_wishlist: {} {}'.format(id, stype)) url = self.endpoints['marcadofavoritos2'].format(family=stype) headers = self.net.headers.copy() headers['Accept'] = 'application/vnd.miviewtv.v1+json' headers['Content-Type'] = 'application/json' headers['X-Hzid'] = self.account['session_token'] post_data = {'objectID': id} response = self.net.session.post(url, data=json.dumps(post_data), headers=headers) content = response.content.decode('utf-8') if response.status_code != 201: data = json.loads(content) if 'resultCode' in data: return data['resultCode'], data['resultText'] return response.status_code, '' def delete_from_wishlist(self, id, stype='vod'): url = self.endpoints['borradofavoritos'].format(family=stype, contentId=id) headers = self.net.headers.copy() headers['Accept'] = 'application/vnd.miviewtv.v1+json' headers['Content-Type'] = 'application/json' headers['X-Hzid'] = self.account['session_token'] response = self.net.session.delete(url, headers=headers) content = response.content.decode('utf-8') if response.status_code != 204: data = json.loads(content) if 'resultCode' in data: return data['resultCode'], data['resultText'] return response.status_code, '' def get_wishlist_url(self): url = self.endpoints['favoritos'].format( deviceType=self.dplayer, 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' if self.quality == 'UHD': url += '&filterQuality=UHD' 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)) url += '&_='+ str(int(time.time()*1000)) return url def get_viewings_url(self): url = self.endpoints['ultimasreproducciones'].format( deviceType=self.dplayer, 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' url += '&_='+ str(int(time.time()*1000)) return url def get_search_url(self, search_term): url = self.endpoints['buscar_best'].format( ACCOUNTNUMBER=self.account['id'], profile=self.account['platform'], texto=search_term, distilledTvRights=','.join(self.entitlements['distilledTvRights']), mdrm='true', demarcation=self.account['demarcation']) url = url.replace('webplayer', self.dplayer) if self.quality == 'UHD': url += '&filterQuality=UHD' return url def add_search(self, search_term): self.search_list.append(search_term) self.cache.save_file('searchs.json', json.dumps(self.search_list, ensure_ascii=False)) def delete_search(self, search_term): self.search_list = [s for s in self.search_list if s != search_term] self.cache.save_file('searchs.json', json.dumps(self.search_list, ensure_ascii=False)) def get_favorite_data(self, links): res = {} for link in links: name = link['rel'] if 'favorites' in name: d = {} d['id'] = link['id'] d['family'] = link.get('href', '').split("/")[-2] res[name] = d return res def get_ficha_url(self, id, mode='GLOBAL', catalog=''): url = self.endpoints['ficha'].format(deviceType='webplayer', id=id, profile=self.account['platform'], mediatype='FOTOV', version='7.1', mode=mode, catalog=catalog, channels='', state='', mdrm='true', demarcation=self.account['demarcation'], legacyBoxOffice='') url = url.replace('state=&', '') if self.quality == 'UHD': url += '&filterQuality=UHD' #print(url) 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.get('Imagen', '').replace('ywcatalogov', 'dispficha') t['art']['thumb'] = t['art']['poster'] t['info']['genre'] = ed['GeneroComAntena'] if ed.get('TipoComercial') == 'Impulsivo': return None # Alquiler if 'Seguible' in ed: t['seguible'] = ed['Seguible'] if 'links' in data: #t['links'] = data['links'] t['favorite_data'] = self.get_favorite_data(data['links']) 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.get('tvProducts', [])) 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 'ShowId' in video: t['show_id'] = video['ShowId'] if self.add_extra_info: self.add_video_extra_info(t) if 'Recording' in data: t['stream_type'] = 'rec' t['rec'] = {'id': data['Recording']['id'], 'name': data['Recording']['name'], 'start': data['Recording']['beginTime'], 'end': data['Recording']['endTime']} if t['url'] == '': t['info']['title'] += ' (' + isodate2str(t['rec']['start']) + ')' if t['url'] == '' and t['stream_type'] == 'vod': t['subscribed'] = False if ed['TipoContenido'] == 'Serie': t['type'] = 'series' t['info']['mediatype'] = 'tvshow' t['subscribed'] = self.is_subscribed_vod(data.get('tvProducts', [])) if ed['TipoContenido'] == 'Temporada': t['type'] = 'season' t['info']['mediatype'] = 'season' t['subscribed'] = self.is_subscribed_vod(data.get('tvProducts', [])) if 'DatosAccesoAnonimo' in data: da = data['DatosAccesoAnonimo'] t['video_format'] = da.get('FormatoVideo') if 'HoraInicio' in da and da['HoraInicio'] != None: t['start'] = int(da['HoraInicio']) t['start_str'] = timestamp2str(t['start']) t['date_str'] = timestamp2str(t['start'], '%a %d %H:%M') if t['url'] == '': t['info']['title'] += ' (' + t['date_str'] +')' 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 add_video_extra_info(self, t): #LOG('add_video_extra_info: t: {}'.format(t)) try: if 'show_id' in t: catalog = 'events' id = t['show_id'] elif 'id' in t: catalog = '' id = t['id'] else: return #LOG('id: {} catalog: {}'.format(id, catalog)) prefix = 'event' if catalog=='events' else 'info' cache_filename = 'cache/{}_{}.json'.format(prefix, id) content = self.cache.load(cache_filename, 30*24*60) if content: data = json.loads(content) else: url = self.get_ficha_url(id=id, catalog=catalog) #print(url) data = self.net.load_data(url) self.cache.save_file(cache_filename, json.dumps(data, ensure_ascii=False)) if not 'info' in t: t['info'] = {} if not 'art' in t: t['art'] = {} if not t['info'].get('title'): if 'TituloEpisodio' in data: t['info']['title'] = data['TituloEpisodio'] else: t['info']['title'] = data['Titulo'] if 'Serie' in data: if not t['info'].get('episode'): t['info']['episode'] = data['NumeroEpisodio'] if not t['info'].get('tvshowtitle'): t['info']['tvshowtitle'] = data['Serie']['TituloSerie'] if not t['info'].get('season') and data['Serie'].get('Temporada'): m = re.search(r'T(\d+)', data['Serie']['Temporada']) if m: t['info']['season'] = m.group(1) if not 'duration' in t['info']: t['info']['duration'] = data.get('Duracion', 0) * 60 if data.get('Sinopsis'): t['info']['plot'] = data['Sinopsis'] if data.get('Actores'): t['info']['cast'] = data['Actores'].split(', ') if data.get('Directores'): t['info']['director'] = data['Directores'].split(', ') if data.get('Anno'): t['info']['year'] = data['Anno'] if data.get('Nacionalidad'): t['info']['country'] = data['Nacionalidad'] im_thumb = im_default = im_season = im_fanart = None for im in data['Imagenes']: if im['id'] == 'horizontal': im_thumb = im['uri'] elif im['id'] == 'default': im_default = im['uri'] elif im['id'] == 'watch2tgr-end': im_fanart = im['uri'] elif im['id'] == 'temporada': im_season = im['uri'] if im_thumb and not t['art'].get('thumb'): t['art']['thumb'] = im_thumb if im_season: if not t['art'].get('poster'): t['art']['poster'] = im_season if im_fanart: if not t['art'].get('fanart'): t['art']['fanart'] = im_fanart if im_default: if not t['art'].get('poster'): t['art']['poster'] = im_default if not t['art'].get('icon'): t['art']['icon'] = im_default if not t['art'].get('fanart'): t['art']['fanart'] = im_default except: pass def get_seasons(self, id): url = self.get_ficha_url(id) #print(url) 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.get('tvProducts', [])) if 'Seguible' in data: t['seguible'] = data['Seguible'] #t['video_format'] = data.get('FormatoVideo') c += 1 res.append(t) return res def get_episodes(self, id): url = self.get_ficha_url(id=str(id)) #print(url) data = self.net.load_data(url) #print_json(data) #self.cache.save_file('episodes.json', json.dumps(data, ensure_ascii=False)) 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['info']['tvshowtitle'] = data.get('TituloSerie', '') t['art']['poster'] = data['Imagen'] for im in ed['Imagenes']: if im['id'] == 'horizontal': t['art']['thumb'] = im['uri'] t['info']['season'] = '0' if ed.get('Temporada'): 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] #if video.get('AssetType') == 'SOON': continue if not 'UrlVideo' in video: continue t['subscribed'] = self.is_subscribed_vod(video.get('tvProducts', [])) 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"}' if 'ShowId' in video: t['show_id'] = video['ShowId'] if 'DatosAccesoAnonimo' in d: da = d['DatosAccesoAnonimo'] t['video_format'] = da.get('FormatoVideo') if 'HoraInicio' in da and da['HoraInicio'] != None: t['start'] = int(da['HoraInicio']) t['start_str'] = timestamp2str(t['start']) t['date_str'] = timestamp2str(t['start'], '%a %d %H:%M') if t['url'] == '': t['info']['title'] += ' (' + t['date_str'] +')' if self.add_extra_info: self.add_video_extra_info(t) 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 order_recording(self, program_id): headers = self.net.headers.copy() headers['Content-Type'] = 'application/json' headers['X-Hzid'] = self.account['session_token'] url = self.endpoints['grabarprograma'] data = '{"tvProgramID":"' + str(program_id) +'"}' response = self.net.session.post(url, data=data, headers=headers) content = response.content.decode('utf-8') try: data = json.loads(content) return data except: return None def order_recording_season(self, season_id): headers = self.net.headers.copy() headers['Content-Type'] = 'application/json' headers['X-Hzid'] = self.account['session_token'] url = self.endpoints['grabartemporada'] data = '{"tvSeasonID":"' + str(season_id) +'"}' response = self.net.session.post(url, data=data, headers=headers) content = response.content.decode('utf-8') try: data = json.loads(content) return data except: return None def delete_recording(self, program_id): headers = self.net.headers.copy() headers['Content-Type'] = 'application/json' headers['X-Hzid'] = self.account['session_token'] url = self.endpoints['borrargrabacionindividual'].format(showId=program_id) response = self.net.session.delete(url, headers=headers) content = response.content.decode('utf-8') try: data = json.loads(content) return data except: return None def epg_to_movies(self, channel_id, date=None, duration=2): LOG('epg_to_movies: {} {}'.format(channel_id, date)) epg = self.get_epg(channels=channel_id, date=date, duration=duration) res = [] if not channel_id in epg: return res for p in epg[channel_id]: if sys.version_info[0] < 3: p['date_str'] = unicode(p['date_str'], 'utf-8') name = '[B]' + p['date_str'] + '[/B] ' + p['desc1'] if p['desc2']: name += ' - '+ p['desc2'] plot = name +"\n" + p['desc2'] t = {'info': {}, 'art': {}} t['info']['title'] = name t['info']['plot'] = plot t['type'] = 'movie' t['stream_type'] = 'u7d' t['info']['mediatype'] = 'movie' t['url'] = '' t['id'] = p['id'] t['show_id'] = p['show_id'] if 'serie_id' in p: t['serie_id'] = p['serie_id'] t['session_request'] = '{"contentID":' + str(t['id']) +',"streamType":"CUTV"}' t['info']['playcount'] = 1 # Set as watched t['start'] = p['start'] t['end'] = p['end'] t['aired'] = (p['end'] <= (time.time() * 1000)) t['subscribed'] = True # Fix me """ if 'links' in p: for link in p['links']: if link['rel'] == 'start-over': t['url'] = link['href'] """ t['url'] = 'https://grmovistar.imagenio.telefonica.net/asfe/rest/tvMediaURLs?tvProgram.id='+ str(t['show_id']) +'&svc=startover' if self.add_extra_info: self.add_video_extra_info(t) res.append(t) return res 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 = 'FE' sort = 'MA' #sort = 'AZ' 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'] #filter = '&filter=AC-OM,AC-MA,MA-GBLCICLO59,TD-CUP,RG-SINCAT,AC-PREPUB' 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' if self.quality == 'UHD': url += '&filterQuality=UHD' #LOG('vod url: {}'.format(url)) return url def get_vod_sections(self): from collections import OrderedDict profile = self.account['platform'] #profile = 'OTT' #profile = 'LITE' content = self.cache.load('vod_sections.json') if content: data = json.loads(content) else: url = 'https://apps.dof6.com/feed/conf.php?id=justconfig&id_perfil={}&dispositivo=cell'.format(profile) data = self.net.load_data(url) self.cache.save_file('vod_sections.json', json.dumps(data, ensure_ascii=False)) submenu = data.get('YOMVIANDROID', {}).get('Menu', {}).get('Submenu', []) res = OrderedDict() for o in submenu: #print_json(o) menu = {} menu['visible'] = (o.get('@visible') != 'false') menu['id'] = o.get('@id') if 'Modulo' in o: section_name = o['@nombre'] if '@nombre' in o else o['@P'] modulo = o['Modulo'] section = [] if isinstance(modulo, dict): modulo = [modulo] if isinstance(modulo, list): for m in modulo: if 'consulta' in m and m['consulta'].get('@endpoint_ref', '') == 'consultar': if not '@nombre' in m: continue #print (menu['id'], m['@nombre']) c = {} c['name'] = m['@nombre'] pars = '' sort = '' parametros = m['consulta']['parametro'] if isinstance(parametros, dict): parametros = [parametros] for par in parametros: #print_json(par) if par.get('@incluir', '') == 'true': pars += '&{}={}'.format(par['@id'], par['@value']) if par['@id'] == 'sort': sort = par['@value'] pars = pars.replace('{suscripcion}', self.entitlements['suscripcion']) c['url'] = self.endpoints['consultar'].format(deviceType=self.dplayer, profile=profile, sort=sort, start=1, end=50, mdrm='true', demarcation=self.account['demarcation']) c['url'] += pars if self.quality == 'UHD': c['url'] += '&filterQuality=UHD' section.append(c) if len(section) > 0: menu['name'] = section_name menu['data'] = section res[menu['id']] = menu if 'portada_cine_destacados' in res: res['portada_cine_destacados']['visible'] = True res['portada_cine_destacados']['name'] = 'Cine - Géneros' if 'portada_series_destacados' in res: res['portada_series_destacados']['visible'] = True res['portada_series_destacados']['name'] = 'Series - Géneros' return res 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 export_key_file(self, filename): import shutil if sys.version_info[0] > 2: filename = bytes(filename, 'utf-8') if self.account['access_token']: data = {'timestamp': int(time.time()*1000), 'response':{'access_token': self.account['access_token'], 'token_type': 'bearer'}}; with io.open(filename, 'w', encoding='utf-8', newline='') as handle: handle.write(json.dumps(data, ensure_ascii=False)) else: shutil.copyfile(self.cache.config_directory + 'auth.key', filename) def load_key_file(self): content = self.cache.load_file('auth.key') if content: data = json.loads(content) if 'response' in data: self.account['access_token'] = data['response']['access_token'] elif 'data' in data: data = json.loads(data['data']) self.account['access_token'] = data['response']['access_token'] def save_key_file(self, d): data = {'timestamp': int(time.time()*1000), 'response': d} self.cache.save_file('auth.key', json.dumps(data, ensure_ascii=False)) def delete_session_files(self): for f in ['access_token.conf', 'account.json', 'device_id.conf', 'devices.json', 'profile_id.conf', 'tokens.json', 'channels2.json', 'channels_UHD.json', 'channels_HD.json', 'epg2.json', 'epg_UHD.json', 'epg_HD.json']: self.cache.remove_file(f) def get_profile_image_url(self, img_id): content = self.cache.load_file('avatars.json') if content: data = json.loads(content) else: url = self.endpoints['avatares'] data = self.net.load_data(url) self.cache.save_file('avatars.json', json.dumps(data, ensure_ascii=False)) #print_json(data) for avatar in data: if avatar['id'] == img_id: for link in avatar['links']: if link['sizes'] == '512x512': return link['href'] return None def export_channels(self): if sys.version_info[0] >= 3: from urllib.parse import urlencode else: from urllib import urlencode channels = self.get_channels() res = [] for c in channels: if not c['subscribed']: continue t = {} t['name'] = c['channel_name'] t['id'] = c['id'] t['logo'] = c['art']['icon'] t['preset'] = c['dial'] args = urlencode({'action': 'play', 'stype': 'tv', 'id': c['id'], 'url': c['url'], 'session_request': c['session_request']}) t['stream'] = 'plugin://plugin.video.movistarplus/?' + args res.append(t) return res def export_channels_to_m3u8(self, filename): channels = self.export_channels() items = [] for t in channels: item = '#EXTINF:-1 tvg-name="{name}" tvg-id="{id}" tvg-logo="{logo}" tvg-chno="{preset}" group-title="Movistar+" catchup="vod",{name}\n{stream}\n\n'.format( name=t['name'], id=t['id'], logo=t['logo'], preset=t['preset'], stream=t['stream']) items.append(item) res = '#EXTM3U\n## Movistar+\n{}'.format(''.join(items)) with io.open(filename, 'w', encoding='utf-8', newline='') as handle: handle.write(res) def export_epg(self, date=None, duration=2): if sys.version_info[0] >= 3: from urllib.parse import urlencode else: from urllib import urlencode #now = time.time()*1000 res = {} epg = self.get_epg(date, duration) channels = self.get_channels() for channel in channels: id = channel['id'] if not channel['subscribed'] or not id in epg: continue res[id] = [] for e in epg[id]: t = {} t['title'] = e['desc1'] t['subtitle'] = e['desc2'] t['start'] = datetime.utcfromtimestamp(e['start']/1000).strftime('%Y-%m-%dT%H:%M:%SZ') t['stop'] = datetime.utcfromtimestamp(e['end']/1000).strftime('%Y-%m-%dT%H:%M:%SZ') #aired = (e['end'] <= now) #if aired: if True: program_id = str(e['show_id']) url = 'https://grmovistar.imagenio.telefonica.net/asfe/rest/tvMediaURLs?tvProgram.id='+ program_id +'&svc=startover' session_request = '{"contentID":' + program_id +',"streamType":"CUTV"}' args = urlencode({'action': 'play', 'stype': 'u7d', 'id': program_id, 'url': url, 'session_request': session_request}) t['stream'] = 'plugin://plugin.video.movistarplus/?' + args """ if self.add_extra_info and id in ['HOLLYW', 'TCM', 'AMC', 'MV3', 'MV2', 'CPSER', 'FOXGE', 'TNT']: i = {'id': e['id'], 'show_id': e['show_id']} self.add_video_extra_info(i) #print(i) t['description'] = i['info'].get('plot') t['image'] = i['art'].get('poster') t['credits'] = [] for text in i['info'].get('director', []): t['credits'].append({'type': 'director', 'name': text}) for text in i['info'].get('cast', []): t['credits'].append({'type': 'actor', 'name': text}) """ res[id].append(t) return res def export_epg_to_xml(self, filename, ndays=3, report_func=None): if sys.version_info[0] < 3: # Python 2 from cgi import escape as html_escape else: # Python 3 from html import escape as html_escape channels = self.export_channels() res = [] res.append('\n' + '\n' + '\n') for t in channels: res.append('\n'.format(t['id']) + ' {}\n'.format(t['name']) + ' \n'.format(t['logo']) + '\n') if True: epg = {} today = datetime.today() total_items = 0 for i in range(0, ndays, 1): date = today + timedelta(days=i) strdate = date.strftime('%Y-%m-%dT00:00:00') LOG('epg: {}'.format(strdate)) if report_func: report_func(date.strftime('%d/%m/%Y')) e = self.export_epg(strdate, 1) LOG('epg: channels: {}'.format(len(e))) for ch in e: if not ch in epg: epg[ch] = [] total_items += len(e[ch]) epg[ch].extend(e[ch]) LOG('epg: total items: {}'.format(total_items)) else: epg = self.export_epg() for ch in channels: if not ch['id'] in epg: continue for e in epg[ch['id']]: start = datetime.strptime(e['start'], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y%m%d%H%M%S +0000") stop = datetime.strptime(e['stop'], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y%m%d%H%M%S +0000") url = e.get('stream', None) if url: url = url.replace('&', '&') res.append('\n' + ' {}\n'.format(html_escape(e['title'])) + ' {}\n'.format(html_escape(e['subtitle']))) if 'image' in e: res.append(' \n'.format(e['image'])) if 'description' in e: res.append(' {}\n'.format(html_escape(e['description']))) if 'credits' in e and len(e['credits']) > 0: res.append(' \n'); for c in e['credits']: if c['type'] == 'director': res.append(' {}\n'.format(c['name'])) elif c['type'] == 'actor': res.append(' {}\n'.format(c['name'])) res.append(' \n'); res.append('\n') res.append('\n') with io.open(filename, 'w', encoding='utf-8', newline='') as handle: handle.write(''.join(res)) def save_credentials(self, username, password): from .b64 import encode_base64 data = {'username': username, 'password': encode_base64(password)} self.cache.save_file('credentials.json', json.dumps(data, ensure_ascii=False)) def load_credentials(self): from .b64 import decode_base64 content = self.cache.load_file('credentials.json') if content: data = json.loads(content) return data['username'], decode_base64(data['password']) else: return '', '' def get_token_expire_date(self, token): from .b64 import decode_base64 res = 0 try: l = token.split('.') if len(l) > 1: padding = len(l[1]) % 4 l[1] += '=' * (4 - padding) if padding != 0 else '' s = decode_base64(l[1]) data = json.loads(s) res = data['exp'] except: pass return res