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 @@
-
+
+
+
+
+
+
+
+