Files
kodimovplus/resources/lib/plugin.py
T
XoséandClaude Opus 5.5 69a37ff064 1.0.0+kodimov.12: no duplicate TV guide instance; retry the web player registration
- The TV guide setup always wrote instance-settings-91.xml, so on a box
  already set up with another instance (from the original add-on) both read
  channels.m3u8 and every channel showed twice. The setup now rewrites the
  instance that already reads the list, and the service deletes extra ones
  at start (keeps the one that isn't 91, which holds what was watched) and
  restarts IPTV Simple when nothing plays.
- The web player registration for the new servers got one 401 error page
  and gave up, and the TV guide list then lacked LALIGA, DAZN LALIGA and
  Liga de Campeones until the next export 6 hours later. The registration
  is tried 3 times, and after a failure the service exports the list again
  every half hour, up to 6 times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 14:38:12 +01:00

1007 lines
39 KiB
Python

# encoding: utf-8
#
# SPDX-License-Identifier: LGPL-2.1-or-later
from __future__ import unicode_literals, absolute_import, division
import sys
import json
import io
import os.path
from datetime import datetime, timedelta
from .timeconv import isodate2date
import xbmc
import xbmcgui
import xbmcplugin
import xbmcaddon
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
from .log import LOG
from .movistar import *
from .addon import *
from .gui import *
from .useragent import useragent
kodi_version= int(xbmc.getInfoLabel('System.BuildVersion')[:2])
# Get the plugin url in plugin:// notation.
_url = sys.argv[0]
# Get the plugin handle as an integer number.
_handle = int(sys.argv[1])
def get_url(**kwargs):
for key, value in kwargs.items():
if isinstance(value, unicode):
kwargs[key] = value.encode('utf-8')
return '{0}?{1}'.format(_url, urlencode(kwargs))
def play(params):
LOG('play - params: {}'.format(params))
if not 'url' in params:
show_notification(addon.getLocalizedString(30204))
return
url = params['url']
channel_id = params['id']
stype = params['stype']
token = m.account['ssp_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()
"""
session_opened = False
if addon.getSettingBool('open_session') or stype == 'vod':
d = m.open_session(params['session_request'])
session_opened = True
LOG('Open session: d: {}'.format(d))
if d['resultCode'] != 0:
show_notification(d['resultText'])
return
if 'resultData' in d and 'cToken' in d['resultData']:
token = d['resultData']['cToken']
if stype == 'vod':
d = m.delete_session()
session_opened = False
LOG('Delete session: d: {}'.format(d))
LOG("token: {}".format(token))
if stype in ['u7d', 'rec']:
d = m.get_u7d_url(url)
if 'resultText' in d:
show_notification(d['resultText'])
return
else:
url = d['url']
#if True:
# url = url.replace('DASH_WPC_WIDEVINE', 'DASH_TV_WIDEVINE')
# U7D from start and end times
#if stype == 'tv' and all(param in params for param in ['cas_id', 'start_time', 'end_time']):
# start = datetime.utcfromtimestamp(int(params['start_time']))
# end = datetime.utcfromtimestamp(int(params['end_time']))
# catchup_url = 'https://stover-wp0.cdn.telefonica.com/{cas_id}/vxfmt=dp/Manifest.mpd?device_profile=DASH_TV_WIDEVINE&start_time={start_time}&end_time={end_time}'
# url = catchup_url.format(cas_id=params['cas_id'], start_time=start.strftime('%Y-%m-%dT%H:%M:%SZ'), end_time=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
# LOG('url from capchup: {}'.format(url))
# Read the proxy address
proxy = addon.getSetting('proxy_address')
LOG('proxy address: {}'.format(proxy))
if addon.getSettingBool('manifest_modification') and proxy:
url = '{}/?manifest={}'.format(proxy, url)
LOG("url: {}".format(url))
headers = 'Content-Type=application/octet-stream'
headers += '&User-Agent=' + useragent
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'
#headers += '&Host=wv-ottlic-f3.imagenio.telefonica.net'
manifest_headers = 'User-Agent=' + useragent
if params.get('new_api') == '1':
# LALIGA, DAZN LALIGA, Liga de Campeones: their streams only open with a
# CDN token from the new servers. The player keeps the token it starts
# with, so it must have hours left.
from . import kodimov
try:
cdn_token = kodimov.new_api_cdn_token(m, min_life=3 * 3600)
except Exception as e:
LOG('new_api_cdn_token failed: {}'.format(e))
show_notification(addon.getLocalizedString(30510).format(e))
return
manifest_headers += '&x-tcdn-token=' + cdn_token
elif True: #stype in ['tv', 'u7d', 'rec']:
cdn_token = m.cache.load('cdn.conf', 60)
if not cdn_token:
cdn_token = m.get_cdntoken()
if not cdn_token:
time.sleep(2)
cdn_token = m.get_cdntoken()
if cdn_token:
m.cache.save_file('cdn.conf', cdn_token)
else:
show_notification('Ha pasado algo')
return
#LOG('cdn_token: {}'.format(cdn_token))
manifest_headers += '&x-tcdn-token=' + cdn_token
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)
license_type = 'com.widevine.alpha'
license_key = ''
if format(addon.getSetting('drm_type')) == 'Playready':
license_type = 'com.microsoft.playready'
if addon.getSettingBool('use_proxy_for_license') and proxy:
request_id = str(int(time.time()*1000))
license_key = '{}/license?token={}&stype={}&session_request={}&request_id={}'.format(proxy, quote_plus(token), stype, quote_plus(params['session_request']), request_id)
LOG('license_key: {}'.format(license_key))
else:
license_key = license_url
headers += '&nv-authorizations={}'.format(token)
if kodi_version > 20:
play_item.setProperty('inputstream.adaptive.drm_legacy', '{}|{}|{}'.format(license_type, license_key, headers))
else:
play_item.setProperty('inputstream.adaptive.license_type', license_type)
play_item.setProperty('inputstream.adaptive.license_key', '{}|{}|R{{SSM}}|'.format(license_key, headers))
play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
#play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
if addon.getSettingBool('best_quality'):
# Left to itself, InputStream Adaptive starts these streams at 576p and
# never gets to the 720p/1080p versions (they use a different key)
best = '4K' if addon.getSettingBool('uhd') else '1080p'
play_item.setProperty('inputstream.adaptive.stream_selection_type', 'fixed-res')
play_item.setProperty('inputstream.adaptive.chooser_resolution_max', best)
play_item.setProperty('inputstream.adaptive.chooser_resolution_secure_max', best)
play_item.setProperty('inputstream.adaptive.stream_headers', manifest_headers)
play_item.setProperty('inputstream.adaptive.manifest_headers', manifest_headers)
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 stype == 'u7d':
play_item.setProperty('inputstream.adaptive.play_timeshift_buffer', 'true')
if kodi_version < 19:
play_item.setProperty('inputstreamaddon', 'inputstream.adaptive')
else:
play_item.setProperty('inputstream', 'inputstream.adaptive')
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
from ttml2ssa import Ttml2SsaAddon
ttml = Ttml2SsaAddon()
#ttml.use_language_filter = False
subtype = ttml.subtitle_type()
subfolder = os.path.join(profile_dir, 'subtitles')
if not os.path.exists(subfolder):
os.makedirs(subfolder)
sublist = m.get_subtitles(params['url'])
LOG('sublist: {}'.format(sublist))
subpaths = []
for sub in sublist:
filename = os.path.join(subfolder, sub['lang'])
LOG('Converting {}'.format(sub['filename']))
subheaders = m.net.headers.copy()
subheaders['x-tcdn-token'] = cdn_token
response = m.net.session.get(sub['url'], allow_redirects=True, headers=subheaders)
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)
#if True:
# m.unregister_device()
# m.register_device()
xbmcplugin.setResolvedUrl(_handle, True, listitem=play_item)
LOG('**** session_opened: {}'.format(session_opened))
watermarks = []
if stype == 'tv' and 'uid' in params:
watermarks = m.get_watermarks(params['uid'])
LOG('watermarks: {}'.format(watermarks))
if session_opened or watermarks:
window = xbmcgui.Window(12005)
label = xbmcgui.ControlLabel(0, 0, 400, 20, m.account['id'], textColor='0xFFFFFFFF', alignment=6)
from .player import MyPlayer
player = MyPlayer()
monitor = xbmc.Monitor()
while not monitor.abortRequested() and player.running:
monitor.waitForAbort(10)
#LOG('**** waiting')
if player.isPlaying() and watermarks:
now = isodate2date(datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'))
for wm in list(watermarks):
start = isodate2date(wm['start'])
stop = isodate2date(wm['stop'])
if now >= start:
watermarks.remove(wm)
if now < stop:
duration = (stop - start).total_seconds()
#show_notification(m.account['id'], xbmcgui.NOTIFICATION_INFO)
w = window.getWidth()
h = window.getHeight()
#LOG('window h: {} w: {}'.format(h, w))
pos_x = w-label.getWidth()-60
pos_y = h-200
label.setPosition(pos_x, pos_y)
window.addControl(label)
time.sleep(duration)
window.removeControl(label)
if session_opened:
d = m.delete_session()
LOG('Delete session: d: {}'.format(d))
LOG('Playback finished')
def add_videos(category, ctype, videos, ref=None, url_next=None, url_prev=None, from_wishlist=False):
#LOG("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("t: {}".format(t))
if 'subscribed' in t:
if addon.getSettingBool('only_subscribed') and t['subscribed'] == False: continue
t['info']['title'] = m.colorize_title(t)
if 'availability' in t:
ends = int(t['availability'].get('end', '0'))
if ends > 0:
now = int(time.time()*1000)
n_hours = int((ends - now) / (1000 * 60 * 60))
n_days = int(n_hours / 24)
if (n_days <= addon.getSettingInt('expdays')):
if n_days < 1:
t['info']['title'] += ' [COLOR red](' + addon.getLocalizedString(30401).format(n_hours) + ')[/COLOR]'
else:
t['info']['title'] += ' [COLOR red](' + addon.getLocalizedString(30400).format(n_days) + ')[/COLOR]'
title_name = t['info']['title']
if not 'type' in t: continue
wishlist_action = None
if t['type'] in ['movie', 'series', 'season']:
id = t['id']
stype = 'vod'
possible_to_add = False
if from_wishlist:
# Remove from wishlist
if 'favorite_data' in t:
if 'favorites2' in t['favorite_data']:
id = t['favorite_data']['favorites2']['id']
stype = t['favorite_data']['favorites2']['family']
possible_to_add = True
elif t['type'] == 'season' and 'favorites2.season' in t['favorite_data']:
id = t['favorite_data']['favorites2.season']['id']
stype = t['favorite_data']['favorites2.season']['family']
possible_to_add = True
else:
# Add to wishlist
if t['type'] in ['series', 'season']:
possible_to_add = t.get('seguible', False)
if t['type'] == 'movie':
if t.get('stream_type') == 'vod':
possible_to_add = t['url'] != ''
if 'show_id' in t:
possible_to_add = t['url'] != ''
stype = 'tv'
id = t['show_id']
if possible_to_add:
if not from_wishlist:
op = 'add'
message = 30175
else:
op = 'delete'
message = 30176
wishlist_action = (addon.getLocalizedString(message), "RunPlugin(" + get_url(action='to_wishlist', id=id, op=op, stype=stype) + ")")
if t['type'] == 'movie':
list_item = xbmcgui.ListItem(label = title_name)
#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'])
actions = []
if t.get('stream_type') == 'u7d':
#LOG(t)
if 'show_id' in t and not t.get('aired', False):
record_program_action = (addon.getLocalizedString(30171), "RunPlugin(" + get_url(action='add_recording', id=t['show_id']) + ")")
actions.append(record_program_action)
if 'serie_id' in t:
record_season_action = (addon.getLocalizedString(30169), "RunPlugin(" + get_url(action='add_recording_season', id=t['serie_id']) + ")")
actions.append(record_season_action)
if 'rec' in t:
action = get_url(action='delete_recording', id=t['rec']['id'], name=t['rec']['name'])
actions.append((addon.getLocalizedString(30173), "RunPlugin(" + action + ")"))
if wishlist_action:
actions.append(wishlist_action)
if len(actions) > 0:
#LOG(actions)
list_item.addContextMenuItems(actions)
url = get_url(action='play', id=t['id'], url=t['url'], session_request=t['session_request'], stype=t['stream_type'])
if t.get('new_api'): url += '&new_api=1'
if 'show_id' in t: url += '&show_id={}'.format(t['show_id'])
if 'uid' in t: url += '&uid={}'.format(t['uid'])
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'])
if wishlist_action:
list_item.addContextMenuItems([wishlist_action])
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'])
if wishlist_action:
list_item.addContextMenuItems([wishlist_action])
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, from_wishlist=from_wishlist, name=category), list_item, True)
xbmcplugin.endOfDirectory(_handle)
def list_devices(params):
LOG('list_devices: params: {}'.format(params))
devices = m.get_devices()
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]'
img_url = m.get_profile_image_url(p['image_id'])
art = {'icon': img_url} if img_url else None
select_action = get_url(action='profiles', id=p['id'], name='select')
add_menu_option(name, select_action, art=art)
close_folder(cacheToDisc=False)
def list_epg(params):
LOG('list_epg: {}'.format(params))
from .timeconv import my_strftime
if 'id' in params:
if not 'date' in params:
today = datetime.today()
for i in range(6, 0, -1):
past_date = today - timedelta(days=i)
date = past_date.strftime('%Y-%m-%dT00:00:00')
display_str = my_strftime(past_date, '%a %d')
add_menu_option(display_str, get_url(action='epg', id=params['id'], name=params['name'], date=date))
add_videos(params['name'], 'movies', m.epg_to_movies(params['id'], params.get('date'), duration=1))
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 = []
url_next = None
url_prev = None
if 'Contenidos' in data:
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, from_wishlist=False):
data = m.download_list(url, use_hz=True)
l = []
url_next = None
url_prev = None
if 'Contenidos' in data:
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', from_wishlist=from_wishlist)
def list_vod0():
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
sections = m.get_vod_sections()
close_folder()
def list_vod():
open_folder(addon.getLocalizedString(30111)) # VOD
sections = m.get_vod_sections()
for key, s in sections.items():
if not s['visible']: continue
name = s['name'].encode('utf-8')
add_menu_option(name, get_url(action='list_section', name=name, key=key))
close_folder()
def list_section(key):
open_folder(addon.getLocalizedString(30111)) # VOD
sections = m.get_vod_sections()
items = sections[key]['data']
for item in items:
name = item['name'].encode('utf-8')
add_menu_option(name, get_url(action='listing', name=name, url=item['url']))
close_folder()
def search(params):
search_term = params.get('search_term', None)
if search_term:
if sys.version_info[0] < 3:
search_term = search_term.decode('utf-8')
if params.get('name', None) == 'delete':
m.delete_search(search_term)
xbmc.executebuiltin("Container.Refresh")
else:
url = m.get_search_url(search_term)
listing_hz(addon.getLocalizedString(30117), url)
return
if params.get('name', None) == 'new':
search_term = input_window(addon.getLocalizedString(30116)) # Search term
if search_term:
if sys.version_info[0] < 3:
search_term = search_term.decode('utf-8')
m.add_search(search_term)
xbmc.executebuiltin("Container.Refresh")
return
open_folder(addon.getLocalizedString(30113)) # Search
add_menu_option(addon.getLocalizedString(30113), get_url(action='search', name='new')) # New search
for i in reversed(m.search_list):
remove_action = get_url(action='search', search_term=i, name='delete')
cm = [(addon.getLocalizedString(30114), "RunPlugin(" + remove_action + ")")]
add_menu_option(i.encode('utf-8'), get_url(action='search', search_term=i), cm)
close_folder(cacheToDisc=False)
def order_recording(program_id):
data = m.order_recording(program_id)
if data and 'resultText' in data:
show_notification(data['resultText'])
else:
show_notification(addon.getLocalizedString(30172), xbmcgui.NOTIFICATION_INFO)
def order_recording_season(season_id):
data = m.order_recording_season(season_id)
if data and 'resultText' in data:
show_notification(data['resultText'])
else:
show_notification(addon.getLocalizedString(30172), xbmcgui.NOTIFICATION_INFO)
def delete_recording(id, name):
if sys.version_info[0] < 3:
name = name.decode('utf-8')
res = xbmcgui.Dialog().yesno(addon.getLocalizedString(30173), addon.getLocalizedString(30174).format(name))
if res == True:
m.delete_recording(id)
xbmc.executebuiltin("Container.Refresh")
def clear_session():
m.delete_session_files()
def logout():
clear_session()
m.cache.remove_file('auth.key')
def login():
def ask_credentials(username='', password=''):
username = input_window(addon.getLocalizedString(30163), username) # Username
if username:
password = input_window(addon.getLocalizedString(30164), password, hidden=True) # Password
if password:
return username, password
return None, None
store_credentials = addon.getSettingBool('store_credentials')
if store_credentials:
username, password = m.load_credentials()
else:
username, password = ('', '')
username, password = ask_credentials(username, password)
if username:
if store_credentials:
m.save_credentials(username, password)
success, _ = m.login(username, password)
if success:
clear_session()
else:
show_notification(addon.getLocalizedString(30166)) # Failed
def network_login():
# The account number and the device are found/registered automatically.
account_number = addon.getSetting('account_number')
# Keeps device_id.conf: clearing the session would register a new device.
if m.try_network_login(account_number, force=True):
show_notification(addon.getLocalizedString(30503), xbmcgui.NOTIFICATION_INFO) # Logged in
xbmc.executebuiltin('Container.Refresh')
else:
reason = (m.network_login_result or (False, ''))[1]
show_notification(addon.getLocalizedString(30504).format(reason)) # Failed
def login_with_key():
filename = xbmcgui.Dialog().browseSingle(1, addon.getLocalizedString(30182), '', '.key')
if filename:
m.install_key_file(filename)
clear_session()
def export_key():
directory = xbmcgui.Dialog().browseSingle(0, addon.getLocalizedString(30185), '')
if directory:
m.export_key_file(directory + 'movistarplus.key')
def import_credentials():
filename = xbmcgui.Dialog().browseSingle(1, addon.getLocalizedString(30193), '', '.json')
if filename:
m.import_credentials(filename)
def export_credentials():
directory = xbmcgui.Dialog().browseSingle(0, addon.getLocalizedString(30194), '')
if directory:
m.export_credentials(os.path.join(directory, 'credenciales.json'))
def select_account(id, name):
m.switch_account(id)
open_folder(name)
add_menu_option(addon.getLocalizedString(30501), get_url(action='network_login')) # Login from home network
add_menu_option(addon.getLocalizedString(30183), get_url(action='login')) # Login with username
if addon.getSettingBool('enable_key_login'):
add_menu_option(addon.getLocalizedString(30181), get_url(action='login_with_key')) # Login with key
if os.path.exists(os.path.join(m.cache.config_directory, 'auth.key')):
add_menu_option(addon.getLocalizedString(30184), get_url(action='export_key')) # Export key
add_menu_option(addon.getLocalizedString(30150), get_url(action='logout')) # Close session
add_menu_option(addon.getLocalizedString(30196), get_url(action='import_credentials'))
if os.path.exists(os.path.join(m.cache.config_directory, 'credentials.json')):
add_menu_option(addon.getLocalizedString(30195), get_url(action='export_credentials'))
close_folder(cacheToDisc=False)
def list_accounts(params):
LOG('list_accounts: {}'.format(params))
if params.get('name') == 'new_account':
m.create_new_account()
accounts = m.get_accounts()
open_folder(addon.getLocalizedString(30160)) # Accounts
for account in accounts:
name = account['name']
login = account.get('login')
if login:
name += ' ({})'.format(login)
if account['id'] == m.account_dir:
display_name = '[B][COLOR blue]' + name + '[/COLOR][/B]'
else:
display_name = name
add_menu_option(display_name, get_url(action='select_account', id=account['id'], name=name))
add_menu_option(addon.getLocalizedString(30161), get_url(action='accounts', name='new_account')) # Add new
close_folder(cacheToDisc=False)
if params.get('name') == 'new_account':
xbmc.executebuiltin('Container.Update({},replace)'.format(xbmc.getInfoLabel('Container.FolderPath')))
def iptv(params):
LOG('iptv: params: {}'.format(params))
if m.logged:
#try:
if True:
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 create_iptv_settings():
from .iptv import save_iptv_settings
from . import kodimov
show_notification(addon.getLocalizedString(30319), xbmcgui.NOTIFICATION_INFO)
output_file = 'instance-settings-91.xml' if kodi_version > 19 else 'settings.xml'
epg_url = None
if False: #addon.getSettingBool('use_external_epg'):
epg_url = addon.getSetting('epg_url')
try:
pvr_addon = xbmcaddon.Addon('pvr.iptvsimple')
pvr_dir = translatePath(pvr_addon.getAddonInfo('profile'))
LOG('pvr_dir: {}'.format(pvr_dir))
filename = os.path.join(pvr_dir, output_file)
if kodi_version > 19:
# Rewrite the instance that already reads this add-on's list, if any:
# a second one would show every channel twice
existing = kodimov.iptv_instance_files(pvr_dir, 'kodimovplus')
if existing: filename = existing[0]
if os.path.exists(filename):
res = xbmcgui.Dialog().yesno(addon.getLocalizedString(30322), addon.getLocalizedString(30323))
if res == False: return
# Write the channel list and the programme guide first: if IPTV Simple
# starts before epg.xml exists, Kodi stores an empty guide for every
# channel and doesn't ask again for hours.
export_epg_now()
if not os.path.exists(os.path.join(profile_dir, 'epg.xml')):
show_notification(addon.getLocalizedString(30509)) # Log in first
return
save_iptv_settings(filename, 'KodiMov+', 'kodimovplus', epg_url)
# IPTV Simple now reads epg.xml, so the service must keep it up to date.
addon.setSettingBool('auto_export_epg', True)
# Restart IPTV Simple so it loads the new instance and channel list.
kodimov.restart_iptv_simple()
# Show Movistar's own channel numbers instead of 1, 2, 3... Kodi only
# accepts it once IPTV Simple is back; the service keeps trying otherwise.
numbers_set = os.path.join(profile_dir, 'numbers-set')
if os.path.exists(numbers_set): os.remove(numbers_set)
for _ in range(5):
if kodimov.ensure_channel_numbers(numbers_set): break
xbmc.sleep(2000)
except:
show_notification(addon.getLocalizedString(30324))
def export_channels_now():
if not m.logged: return
channels_filename = os.path.join(profile_dir, 'channels.m3u8')
if m.export_channels_to_m3u8(channels_filename, addon.getSettingBool('only_subscribed')):
LOG('export_channels_now: channel list changed')
def export_epg_now(quiet=False, reload_guide=False):
# quiet: background refresh from the service, no pop-ups while watching TV
def notify(message):
if not quiet: show_notification(message, xbmcgui.NOTIFICATION_INFO)
def report_progress(message):
notify(addon.getLocalizedString(30314).format(message))
if not m.logged: return
only_subscribed = addon.getSettingBool('only_subscribed')
channels_filename = os.path.join(profile_dir, 'channels.m3u8')
epg_filename = os.path.join(profile_dir, 'epg.xml')
notify(addon.getLocalizedString(30310))
m.export_channels_to_m3u8(channels_filename, only_subscribed)
if True: #not addon.getSettingBool('use_external_epg'):
notify(addon.getLocalizedString(30311))
changed = m.export_epg_to_xml(epg_filename, addon.getSettingInt('export_days'), report_progress, only_subscribed)
if changed and reload_guide:
from . import kodimov
try:
pvr_dir = translatePath(xbmcaddon.Addon('pvr.iptvsimple').getAddonInfo('profile'))
except RuntimeError: # IPTV Simple not installed or disabled
pvr_dir = None
if pvr_dir: kodimov.reload_iptv_guide(pvr_dir, 'kodimovplus')
notify(addon.getLocalizedString(30313))
def to_wishlist(params):
stype = params['stype']
if params['op'] == 'add':
retcode, message = m.add_to_wishlist(params['id'], stype)
else:
retcode, message = m.delete_from_wishlist(params['id'], stype)
if retcode in [201, 204]:
message = 30177 if params['op'] == 'add' else 30178
show_notification(addon.getLocalizedString(message), xbmcgui.NOTIFICATION_INFO)
if params['op'] == 'delete':
xbmc.executebuiltin("Container.Refresh")
else:
show_notification(str(retcode) +': '+ message)
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'] == 'login':
login()
elif params['action'] == 'network_login':
network_login()
elif params['action'] == 'export_channels_now':
export_channels_now()
elif params['action'] == 'new_api_keep_warm':
if m.logged:
from . import kodimov
kodimov.new_api_keep_warm(m)
elif params['action'] == 'export_key':
export_key()
elif params['action'] == 'export_credentials':
export_credentials()
elif params['action'] == 'import_credentials':
import_credentials()
elif params['action'] == 'select_account':
select_account(params['id'], params['name'])
elif params['action'] == 'accounts':
list_accounts(params)
elif params['action'] == 'logout':
logout()
elif params['action'] == 'epg':
list_epg(params)
elif params['action'] == 'add_recording':
order_recording(params['id'])
elif params['action'] == 'delete_recording':
delete_recording(params['id'], params['name'])
elif params['action'] == 'add_recording_season':
order_recording_season(params['id'])
elif params['action'] == 'wishlist':
# Wishlist
listing_hz(addon.getLocalizedString(30102), m.get_wishlist_url(), from_wishlist=True)
elif params['action'] == 'continue-watching':
listing_hz(addon.getLocalizedString(30102), m.get_viewings_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'], params.get('from_wishlist'))
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()
elif params['action'] == 'list_section':
list_section(params['key'])
elif params['action'] == 'search':
search(params)
elif params['action'] == 'create_iptv_settings':
create_iptv_settings()
elif params['action'] == 'export_epg_now':
export_epg_now(quiet=params.get('quiet') == '1', reload_guide=params.get('reload_guide') == '1')
elif params['action'] == 'to_wishlist':
to_wishlist(params)
elif 'iptv' in params['action']:
iptv(params)
else:
# Main
open_folder(addon.getLocalizedString(30101)) # Menu
if m.logged:
add_menu_option(addon.getLocalizedString(30104), get_url(action='tv'), icon='tv.png') # TV
add_menu_option(addon.getLocalizedString(30107), get_url(action='epg'), icon='epg.png') # EGP
add_menu_option(addon.getLocalizedString(30102), get_url(action='wishlist'), icon='mylist.png') # My list
add_menu_option(addon.getLocalizedString(30103), get_url(action='recordings'), icon='recording.png') # Recordings
add_menu_option(addon.getLocalizedString(30122), get_url(action='continue-watching'), icon='continue.png') # Continue
add_menu_option(addon.getLocalizedString(30111), get_url(action='vod'), icon='vod.png') # VOD
add_menu_option(addon.getLocalizedString(30112), get_url(action='search'), icon='search.png') # Search
add_menu_option(addon.getLocalizedString(30180), get_url(action='profiles'), icon='profiles.png') # Profiles
add_menu_option(addon.getLocalizedString(30108), get_url(action='devices'), icon='devices.png') # Devices
else:
if m.expired_access_token:
show_notification(addon.getLocalizedString(30208))
add_menu_option(addon.getLocalizedString(30501), get_url(action='network_login'), icon='account.png') # Login from home network
add_menu_option(addon.getLocalizedString(30160), get_url(action='accounts'), icon='account.png') # Accounts
add_menu_option(addon.getLocalizedString(30450), get_url(action='show_donation_dialog'), icon='qr.png') # Donation
close_folder(cacheToDisc=False)
def run():
# 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
params = sys.argv[2][1:]
if 'show_donation_dialog' in params:
from .customdialog import show_donation_dialog
show_donation_dialog()
return
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,
network_login=addon.getSettingBool('network_login'),
account_number=addon.getSetting('account_number'))
m.add_extra_info = addon.getSettingBool('add_extra_info')
m.hide_unplayable = addon.getSettingBool('hide_unplayable')
m.add_extra_channels = addon.getSettingBool('extra_channels')
if addon.getSettingBool('uhd'):
m.quality = 'UHD'
profile_id = addon.getSetting('profile_id').upper()
LOG('profile_id: {}'.format(profile_id))
if profile_id not in ['', 'AUTO']:
m.account['platform'] = profile_id
# Clear cache
LOG('Cleaning cache. {} files removed.'.format(m.cache.clear_cache()))
if '/iptv/channels' in sys.argv[0]: params += '&action=iptv-channels'
elif '/iptv/epg' in sys.argv[0]: params += '&action=iptv-epg'
router(params)