diff --git a/addon.xml b/addon.xml index 60ad498..01ba1c5 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ @@ -33,6 +33,8 @@ resources/screen1.jpg resources/screen2.jpg resources/screen3.jpg + resources/screen4.jpg + resources/screen5.jpg diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index da515a8..670eb36 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -272,3 +272,19 @@ msgstr "" msgctxt "#30204" msgid "Video not available" msgstr "" + +msgctxt "#30300" +msgid "IPTV Manager" +msgstr "" + +msgctxt "#30301" +msgid "Install IPTV Manager add-on" +msgstr "" + +msgctxt "#30302" +msgid "Enable IPTV Manager integration" +msgstr "" + +msgctxt "#30303" +msgid "IPTV Manager settings" +msgstr "" diff --git a/resources/language/resource.language.es_es/strings.po b/resources/language/resource.language.es_es/strings.po index daf5534..decb923 100644 --- a/resources/language/resource.language.es_es/strings.po +++ b/resources/language/resource.language.es_es/strings.po @@ -272,3 +272,19 @@ msgstr "Formato no soportado" msgctxt "#30204" msgid "Video not available" msgstr "Vídeo no disponible" + +msgctxt "#30300" +msgid "IPTV Manager" +msgstr "IPTV Manager" + +msgctxt "#30301" +msgid "Install IPTV Manager add-on" +msgstr "Instalar IPTV Manager" + +msgctxt "#30302" +msgid "Enable IPTV Manager integration" +msgstr "Habilitar integración con IPTV Manager" + +msgctxt "#30303" +msgid "IPTV Manager settings" +msgstr "Opciones de IPTV Manager" diff --git a/resources/lib/cache.py b/resources/lib/cache.py index ad9b7b4..6082846 100644 --- a/resources/lib/cache.py +++ b/resources/lib/cache.py @@ -48,3 +48,16 @@ class Cache(object): files.extend(glob.glob(self.config_directory + ext)) for filename in files: os.remove(filename) + + def clear_cache(self, days=10): + from datetime import datetime + deleted = 0 + files = glob.glob(os.path.join(self.config_directory + 'cache', '*.json')) + now = datetime.now() + for filename in files: + modified_time = datetime.fromtimestamp(os.path.getmtime(filename)) + time_diff = now - modified_time + if time_diff.days > days: + os.remove(filename) + deleted += 1 + return deleted diff --git a/resources/lib/iptvmanager.py b/resources/lib/iptvmanager.py new file mode 100644 index 0000000..a5f58d2 --- /dev/null +++ b/resources/lib/iptvmanager.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""IPTV Manager Integration module""" + +import json +import socket +from .movistar import Movistar + +class IPTVManager: + """Interface to IPTV Manager""" + + def __init__(self, port): + """Initialize IPTV Manager object""" + self.port = port + + def via_socket(func): + """Send the output of the wrapped function to socket""" + + def send(self, m): + """Decorator to send over a socket""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', self.port)) + try: + sock.sendall(json.dumps(func(self, m)).encode()) + finally: + sock.close() + + return send + + @via_socket + def send_channels(self, m): + """Return JSON-STREAMS formatted python datastructure to IPTV Manager""" + return dict(version=1, streams=m.export_channels()) + + @via_socket + def send_epg(self, m): + """Return JSON-EPG formatted python data structure to IPTV Manager""" + return dict(version=1, epg=m.export_epg()) diff --git a/resources/lib/movistar.py b/resources/lib/movistar.py index 774d125..d3fa59d 100644 --- a/resources/lib/movistar.py +++ b/resources/lib/movistar.py @@ -11,6 +11,7 @@ import io import os import time import re +from datetime import datetime from .log import LOG, print_json from .network import Network @@ -48,6 +49,8 @@ class Movistar(object): # 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() @@ -297,9 +300,10 @@ class Movistar(object): data = self.net.load_data(url, headers) self.cache.save_file('devices.json', json.dumps(data, ensure_ascii=False)) #print_json(data) + if not isinstance(data, list): return [] res = [] for d in data: - if d['Id'] != '-': + if d.get('Id') != '-': dev = {} dev['id'] = d['Id'] dev['name'] = d['Name'] @@ -436,7 +440,8 @@ class Movistar(object): if 'TituloHorLinea2' in p: pr['desc2'] = p['TituloHorLinea2'] pr['show_id'] = p['ShowId'] - if 'links' in p: pr['links'] = p['links'] + pr['id'] = p['Id'] + #if 'links' in p: pr['links'] = p['links'] epg[id].append(pr) return epg @@ -497,8 +502,9 @@ class Movistar(object): ch['desc2'] = desc2 plot += "[B]{}[/B] {} {}\n".format(programs[i]['start_str'], desc1, desc2) if self.add_extra_info: - ch['art'] = {'poster': None, 'fanart': None} - self.add_video_extra_info(programs[i]['show_id'], ch, catalog='events') + 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 @@ -541,6 +547,7 @@ class Movistar(object): t['stream_type'] = 'tv' t['info']['mediatype'] = 'movie' t['info']['title'] = str(c['Dial']) +'. ' + c['Nombre'] + t['channel_name'] = c['Nombre'] t['id'] = c['CodCadenaTv'] if add_epg_info: t['desc1'] = c['Nombre'] @@ -650,8 +657,9 @@ class Movistar(object): 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['id'], t) + self.add_video_extra_info(t) if 'Recording' in data: t['stream_type'] = 'rec' t['rec'] = {'id': data['Recording']['id'], @@ -680,11 +688,49 @@ class Movistar(object): res.append(t) return res - def add_video_extra_info(self, id, t, catalog=''): + def add_video_extra_info(self, t): + #LOG('add_video_extra_info: t: {}'.format(t)) try: - url = self.endpoints['ficha'].format(deviceType='webplayer', id=id, profile=self.account['platform'], mediatype='FOTOV', version='7.1', mode='GLOBAL', catalog=catalog, channels='', state='', mdrm='true', demarcation=self.account['demarcation'], legacyBoxOffice='') - #print(url) - data = self.net.load_data(url) + 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.endpoints['ficha'].format(deviceType='webplayer', id=id, profile=self.account['platform'], mediatype='FOTOV', version='7.1', mode='GLOBAL', catalog=catalog, channels='', state='', mdrm='true', demarcation=self.account['demarcation'], legacyBoxOffice='') + #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'): @@ -774,7 +820,7 @@ class Movistar(object): t['stream_type'] = 'u7d' t['session_request'] = '{"contentID":' + str(t['id']) + ', "streamType":"CUTV"}' if self.add_extra_info: - self.add_video_extra_info(t['id'], t) + self.add_video_extra_info(t) res.append(t) return res @@ -829,19 +875,23 @@ class Movistar(object): t['stream_type'] = 'u7d' t['info']['mediatype'] = 'movie' t['url'] = '' - t['id'] = p['show_id'] + t['id'] = p['id'] + t['show_id'] = p['show_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['id'], t, catalog='events') + self.add_video_extra_info(t) res.append(t) return res @@ -935,3 +985,111 @@ class Movistar(object): 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(False) + 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): + 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() + channels = self.get_channels(False) + for channel in channels: + if not channel['subscribed']: continue + id = channel['id'] + 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: + 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): + 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') + + epg = self.export_epg() + for ch in channels: + 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(e['title']) + + ' {}'.format(e['subtitle']) + + '\n') + + res.append('\n') + with io.open(filename, 'w', encoding='utf-8', newline='') as handle: + handle.write(''.join(res)) + diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 5e38295..3117da3 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -148,6 +148,14 @@ def play(params): play_item.setMimeType('application/dash+xml') play_item.setContentLookup(False) + # Add info + if addon.getSettingBool('add_extra_info') and stype in ['vod', 'u7d', 'rec']: + t = {'id': params['id']} + if 'show_id' in params: t['show_id'] = params['show_id'] + m.add_video_extra_info(t) + play_item.setInfo('video', t['info']) + play_item.setArt(t['art']) + # Subtitles if stype == 'vod' and addon.getSettingBool('use_ttml2ssa'): # Convert subtitles @@ -238,6 +246,7 @@ def add_videos(category, ctype, videos, ref=None, url_next=None, url_prev=None): list_item.addContextMenuItems([(addon.getLocalizedString(30173), "RunPlugin(" + action + ")")]) url = get_url(action='play', id=t['id'], url=t['url'], session_request=t['session_request'], stype=t['stream_type']) + if 'show_id' in t: url += '&show_id={}'.format(t['show_id']) xbmcplugin.addDirectoryItem(_handle, url, list_item, False) elif t['type'] == 'series': list_item = xbmcgui.ListItem(label = title_name) @@ -466,6 +475,19 @@ def list_users(): add_menu_option(addon.getLocalizedString(30150), get_url(action='logout')) # Close session close_folder() +def iptv(params): + LOG('iptv: params: {}'.format(params)) + if m.logged: + try: + from .iptvmanager import IPTVManager + port = int(params['port']) + if params['action'] == 'iptv-channels': + IPTVManager(port).send_channels(m) + elif params['action'] == 'iptv-epg': + IPTVManager(port).send_epg(m) + except: + pass + def router(paramstring): """ Router function that calls other functions @@ -523,6 +545,8 @@ def router(paramstring): list_vod() elif params['action'] == 'search': search(params) + elif 'iptv' in params['action']: + iptv(params) else: # Main open_folder(addon.getLocalizedString(30101)) # Menu @@ -582,9 +606,15 @@ def run(): if profile_id in ['OTT', 'NODTH']: m.account['platform'] = addon.getSetting('profile_id') + # Clear cache + LOG('Cleaning cache. {} files removed.'.format(m.cache.clear_cache())) + 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:]) + params = sys.argv[2][1:] + if '/iptv/channels' in sys.argv[0]: params += '&action=iptv-channels' + elif '/iptv/epg' in sys.argv[0]: params += '&action=iptv-epg' + router(params) diff --git a/resources/screen1.jpg b/resources/screen1.jpg index a90413d..9916223 100644 Binary files a/resources/screen1.jpg and b/resources/screen1.jpg differ diff --git a/resources/screen2.jpg b/resources/screen2.jpg index 22c8e2e..49c613d 100644 Binary files a/resources/screen2.jpg and b/resources/screen2.jpg differ diff --git a/resources/screen3.jpg b/resources/screen3.jpg index 2514c87..ca60062 100644 Binary files a/resources/screen3.jpg and b/resources/screen3.jpg differ diff --git a/resources/screen4.jpg b/resources/screen4.jpg new file mode 100644 index 0000000..7266cab Binary files /dev/null and b/resources/screen4.jpg differ diff --git a/resources/screen5.jpg b/resources/screen5.jpg new file mode 100644 index 0000000..6027369 Binary files /dev/null and b/resources/screen5.jpg differ diff --git a/resources/settings.xml b/resources/settings.xml index a37d975..6d5314f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -4,7 +4,7 @@ - + @@ -14,12 +14,19 @@ - + + + + + + + +