версия для Kodi 19

This commit is contained in:
roman committed 2022-03-14 20:34:22 +03:00
1 parent 9aefb9451b
commit 5d2aff9bc5
60 files changed
+5831

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import builtins
import calendar
import datetime
import math
import os
import platform
import sys
import threading
import time
from functools import wraps
import xbmc
import xbmcaddon
import importlib
if hasattr(builtins, 'addon_id') is False:
setattr(builtins, 'addon_id', os.path.basename(os.path.abspath(os.path.dirname(__file__))))
addon = xbmcaddon.Addon(id=getattr(builtins, 'addon_id'))
utc_local_offset = math.ceil(calendar.timegm(time.localtime()) - time.time())
importlib.reload(sys)
# noinspection PyUnresolvedReferences
sys.setdefaultencoding('utf-8')
TENSECS = 10 # type: int
MIN = 60 # type: int
HALFHOUR = 1800 # type: int
HOUR = 3600 # type: int
DAY = 86400 # type: int
TWODAYS = 172800 # type: int
TREEDAYS = 259200 # type: int
WEEK = 604800 # type: int
TWOWEEKS = 1209600 # type: int
TEXT_SUBSCRIPTION_REQUIRED_ID = 30101 # type: int
TEXT_SET_CREDENTIALS_ID = 30102 # type: int
TEXT_AUTHENTICATION_FAILED_ID = 30103 # type: int
TEXT_CHECK_SETTINGS_ID = 30104 # type: int
TEXT_NOT_PLAYABLE_ID = 30105 # type: int
TEXT_SERVICE_ERROR_OCCURRED_ID = 30106 # type: int
TEXT_SURE_TO_EXIT_ID = 30107 # type: int
TEXT_ARCHIVE_NOT_AVAILABLE_YET_ID = 30108 # type: int
TEXT_JUMP_TO_ARCHIVE_ID = 30109 # type: int
TEXT_CHANNEL_HAS_NO_ARCHIVE_ID = 30110 # type: int
TEXT_LIVE_NO_FORWARD_SKIP_ID = 30111 # type: int
TEXT_IDLE_DIALOG_ID = 30112 # type: int
TEXT_IDLE_DIALOG_COUNTDOWN_ID = 30113 # type: int
TEXT_HTTP_REQUEST_ERROR_ID = 30114 # type: int
TEXT_PLEASE_RESTART_KODI_ID = 30115 # type: int
TEXT_INSTALL_EXTRA_RESOURCES_ID = 30116 # type: int
TEXT_PLEASE_CHECK_INTERNET_CONNECTION_ID = 30117 # type: int
TEXT_UNEXPECTED_RESPONSE_FROM_SERVICE_PROVIDER_ID = 30118 # type: int
TEXT_UNEXPECTED_ERROR_OCCURRED_ID = 30119 # type: int
TEXT_NO_INFO_AVAILABLE_ID = 30201 # type: int
TEXT_ABBR_MINUTES_ID = 30202 # type: int
TEXT_ABBR_SECONDS_ID = 30203 # type: int
TEXT_WEEKDAY_FULL_ID_PREFIX = 3030 # type: int
TEXT_WEEKDAY_ABBR_ID_PREFIX = 3031 # type: int
TEXT_MONTH_FULL_ID_PREFIX = 304 # type: int
TEXT_MONTH_ABBR_ID_PREFIX = 305 # type: int
# noinspection PyUnresolvedReferences
class WindowMixin(object):
is_closing = False # type: bool
def __init__(self, **kwargs):
self.is_closing = False
super(WindowMixin, self).__init__()
def close(self):
self.is_closing = True
super(WindowMixin, self).close()
def show_control(self, *control_ids):
for control_id in control_ids:
control = self.getControl(control_id)
if control:
control.setVisible(True)
def hide_control(self, *control_ids):
for control_id in control_ids:
control = self.getControl(control_id)
if control:
control.setVisible(False)
def set_control_image(self, control_id, image):
control = self.getControl(control_id)
if control:
control.setImage(image)
def setcontrol_label(self, control_id, label):
control = self.getControl(control_id)
if control and label:
control.setLabel(label)
def set_control_text(self, control_id, text):
control = self.getControl(control_id)
if control:
control.setText(text)
def get_string(id_):
return xbmcaddon.Addon(os.path.basename(os.path.abspath(os.path.dirname(__file__) + "/../../"))).getLocalizedString(id_)
def show_small_popup(title='', msg='', delay=5000, image=''):
xbmc.executebuiltin('XBMC.Notification("%s","%s",%d,"%s")' % (title, msg, delay, image))
def build_user_agent():
# type: () -> str
return 'KODI/%s (%s; %s %s; python %s) %s/%s ' % (
xbmc.getInfoLabel('System.BuildVersion').split(" ")[0],
xbmc.getInfoLabel('System.BuildVersion'),
platform.system(),
platform.release(),
platform.python_version(),
addon.getAddonInfo('id').replace('-DEV', ''),
addon.getAddonInfo('version')
)
def unique(s, t):
# type: (str, str) -> str
t = (t * ((len(s) // len(t)) + 1))[:len(s)]
if isinstance(s, str):
return "".join(chr(ord(a) ^ ord(b)) for a, b in zip(s, t))
else:
return str([a ^ b for a, b in zip(s, t)])
def secs_to_percent(length, played):
# type: (int, float) -> float
return (100 * played) / length
def percent_to_secs(length, percent):
# type: (int, float) -> int
return int((length * percent) / 100)
def format_secs(secs, id="time"):
# type: (int, str) -> str
if id == "time":
return "{:0>8}".format(datetime.timedelta(seconds=secs))
if id == "skip":
prefix = "+"
if secs < 0:
prefix = "-"
secs *= -1
elif secs == 0:
prefix = ""
if secs > 60:
return "%s%s %s" % (prefix, secs / 60, get_string(TEXT_ABBR_MINUTES_ID))
return "%s%s %s" % (prefix, secs, get_string(TEXT_ABBR_SECONDS_ID))
def format_date(timestamp, id="dateshort", custom_format=None):
# type: (float, str, str) -> str
ids = {
"%A": (TEXT_WEEKDAY_FULL_ID_PREFIX, "%w"),
"%a": (TEXT_WEEKDAY_ABBR_ID_PREFIX, "%w"),
"%B": (TEXT_MONTH_FULL_ID_PREFIX, "%m"),
"%b": (TEXT_MONTH_ABBR_ID_PREFIX, "%m")
}
if timestamp:
if custom_format is not None:
dt = datetime.datetime.fromtimestamp(timestamp)
for k in ids.keys():
if k in custom_format:
v = get_string(int("%s%s" % (ids[k][0], dt.strftime(ids[k][1]))))
custom_format = custom_format.replace(k, v)
return dt.strftime(custom_format)
return datetime.datetime.fromtimestamp(timestamp).strftime(xbmc.getRegion(id))
return ''
def time_now():
# type: () -> float
return time.time()
def timestamp_to_midnight(timestamp):
# type: (float) -> int
# noinspection PyTypeChecker
return int(time.mktime(
datetime.datetime.combine(
datetime.datetime.fromtimestamp(timestamp),
datetime.datetime.min.time()
).timetuple()
))
def str_to_datetime(str_date, fmt):
# type: (str, str) -> datetime.datetime
try:
d = datetime.datetime.strptime(str_date, fmt)
except TypeError:
from time import strptime
d = datetime.datetime(*(strptime(str_date, fmt)[0:6]))
return d
def str_to_timestamp(str_date, fmt):
# type: (str, str) -> int
try:
return int(time.mktime(str_to_datetime(str_date, fmt).timetuple()))
except:
return 0
def run_async(func):
"""
Decorator to run a function in a separate thread
"""
@wraps(func)
def async_func(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.start()
return thread
return async_func
def log(msg, level=xbmc.LOGINFO):
if level == xbmc.LOGDEBUG:
import inspect
mod = inspect.getmodule(inspect.stack()[1][0])
calframe = inspect.getouterframes(inspect.currentframe(), 2)
msg = "------- [%s.%s] : %s" % (mod.__name__, calframe[1][3], msg)
xbmc.log('%s: %s' % (addon.getAddonInfo('name'), msg), level)
def normalize(text):
# type: (str) -> str
if type(text) is not str:
text = str(text)
symbols = ("абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ",
"abvgdeejzijklmnoprstufhzcss_y_euaABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUA")
tr = dict([(ord(a), ord(b)) for (a, b) in zip(*symbols)])
import re
regs = [
'\s+\+[0-9]+', # time shift suffix, e.g. "RTL +7"
'\s+\[[a-zA-Z]+\]', # land code suffix, e.g. "RTL [de]"
'\s+(HQ)', # High quality suffix, e.g. "RTL (HQ)"
]
for reg in regs:
text = re.sub(reg, '', text)
return re.sub('[^0-9a-zA-Z+-]+', '', text.translate(tr)).upper()
x = lambda s: s.decode("hex")
z = lambda s: str.encode(s, "hex")
h1 = '3d37612b5542244c4e3952775a3f6b5a24367732426e583750'
h2 = '5543155b26780b63394e25593d50043d48535a532c0f344e24' \
'54541205362d49632d563e1b3f5c1f65505f130f172f750e66' \
'0b03541f65710978684f6f467c4b563f52531946640b3b0a2b' \
'4011044a6839596a2b556f0c27190e2c194d0a1421073c0a2b' \
'40113e162e3f'
+480
View File
@@ -0,0 +1,480 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import abc
import json
import os
import ssl
import threading
import traceback
import urllib.error
import urllib.parse
import urllib.request
from collections import OrderedDict
from queue import Queue
from threading import Event
from urllib.parse import urlencode
from urllib.request import Request
import xbmc
from . import build_user_agent, log, DAY, time_now, format_date, normalize
from .models import Group, Program, Channel
class ApiException(Exception):
def __init__(self, message, code, origin_error=None):
self.message = message
self.code = code
self.origin_error = origin_error
def __repr__(self):
return "ApiException: (%s) %s" % (self.code, self.message)
class HttpRequest(urllib.request.Request):
ident = None # type: str
method = None # type: str
def __init__(self, ident=None, method=None, **kwargs):
self.ident = ident
self.method = method
urllib.request.Request.__init__(self, **kwargs)
def get_method(self):
return self.method or urllib.request.Request.get_method(self)
def __repr__(self):
return "%s(%r)" % (self.__class__, self.__dict__)
class Api(metaclass=abc.ABCMeta):
AUTH_STATUS_NONE = 0
AUTH_STATUS_OK = 1
AUTH_MAX_ATTEMPTS = 3
E_UNKNOW_ERROR = 1000
E_HTTP_REQUEST_FAILED = 1001
E_JSON_DECODE = 1002
E_AUTH_ERROR = 1003
E_API_ERROR = 1004
auth_status = AUTH_STATUS_NONE # type: int
username = None # type: str
password = None # type: str
sort_channels = None # type: bool
working_path = None # type: str
cookie_file = None # type: str
settings_file = None # type: str
_attempt = 0 # type: int
_groups = None # type: OrderedDict[str, Group]
_channels = None # type: OrderedDict[str, Channel]
_ident = None # type: str
_epg_map = None # type: dict
def __init__(self, username=None, password=None, working_path="./", sort_channels=False):
self.auth_status = self.AUTH_STATUS_NONE
self.username = self._ident = username
self.password = password
self.working_path = working_path
self.sort_channels = sort_channels
if not os.path.exists(self.working_path):
os.makedirs(self.working_path)
self.cookie_file = os.path.join(self.working_path, "%s.cookie.txt" % self.__class__.__name__)
self.settings_file = os.path.join(self.working_path, "%s.settings.txt" % self.__class__.__name__)
self._groups = OrderedDict()
self._channels = OrderedDict()
@property
def client_id(self):
return "%s:%s" % (self.__class__.__name__, self._ident)
@property
def user_agent(self):
# type: () -> str
"""
User agent of HTTP client.
:rtype: str
"""
return build_user_agent()
@property
def groups(self):
if len(self._groups) == 0:
self._groups = OrderedDict(sorted(iter(self.get_groups().items()), key=lambda item: item[1].name)) \
if self.sort_channels else self.get_groups()
self._channels = OrderedDict()
for group in list(self._groups.values()):
channels = OrderedDict(sorted(iter(group.channels.items()), key=lambda item: item[1].name)) \
if self.sort_channels else group.channels
self._channels.update(channels)
return self._groups
@property
def channels(self):
if len(self._channels) == 0:
len(self.groups)
return self._channels
@property
def base_api_url(self):
# type: () -> str
"""
Base URL of API.
:rtype: str
"""
return ""
@property
def base_icon_url(self):
# type: () -> str
"""
Base URL of channel icons.
:rtype: str
"""
return ""
@property
def host(self):
# type: () -> str
"""
Api hostname.
:rtype: str
"""
return ""
@property
def diff_live_archive(self):
# type: () -> int
"""
Difference between live stream and archive stream in seconds.
:rtype: int
"""
return -1
@property
def archive_ttl(self):
# type: () -> int
"""
Time to live (ttl) of archive streams in seconds.
:rtype: int
"""
return -1
@abc.abstractmethod
def login(self):
# type: () -> dict
"""Login to the IPTV service"""
pass
@abc.abstractmethod
def is_login_request(self, uri, payload=None, method=None, headers=None):
# type: (str, dict, str, dict) -> bool
"""
Checks whether given URI is used for login
:rtype: str
"""
pass
@abc.abstractmethod
def get_groups(self):
# type: () -> OrderedDict[str, Group]
"""
Returns the groups from the service
:rtype: dict[str, Group]
"""
pass
@abc.abstractmethod
def get_stream_url(self, cid, ut_start=None):
# type: (str, int) -> str
"""
Returns the stream URL for given channel id and timestamp
:rtype: str
"""
pass
def resolve_url(self, url):
# type: (str) -> str
request = self.prepare_request(url)
try:
response = urllib.request.urlopen(request, context=ssl.create_default_context())
return response.url
except urllib.error.HTTPError:
return url
@abc.abstractmethod
def get_epg(self, cid):
# type: (str) -> dict[int, Program]
"""
Returns the EPG for given channel id
:rtype: dict[int, Program]
"""
pass
@abc.abstractmethod
def get_cookie(self):
# type: () -> str
"""
Returns cookie
:rtype: str
"""
pass
def read_cookie_file(self):
# type: () -> str
"""
Returns cookie stored in the cookie file
:rtype: str
"""
cookie = ""
if os.path.isfile(self.cookie_file):
with open(self.cookie_file, 'r') as fh:
cookie = fh.read()
fh.close()
return cookie
def write_cookie_file(self, data):
# type: (str) -> None
"""
Stores cookie to the cookie file
:rtype: None
"""
with open(self.cookie_file, 'w') as fh:
fh.write(data)
fh.close()
def read_settings_file(self):
# type: () -> dict
"""
Returns settings stored in the settings file
:rtype: dict
"""
settings = None
if os.path.isfile(self.settings_file):
with open(self.settings_file, 'r') as fh:
json_string = fh.read()
fh.close()
settings = json.loads(json_string)
return settings
def write_settings_file(self, data):
# type: (dict) -> None
"""
Stores settings to the settings file
:rtype: None
"""
with open(self.settings_file, 'w') as fh:
fh.write(json.dumps(data))
fh.close()
def prepare_request(self, uri, payload=None, method="GET", headers=None, ident=None):
# type: (str, dict, str, dict, str) -> HttpRequest
url = self.base_api_url % uri if uri.startswith('http') is False else uri
headers = headers or {}
headers["User-Agent"] = self.user_agent
headers["Connection"] = "Close"
cookie = self.get_cookie()
if cookie != "" and not self.is_login_request(uri, payload):
headers["Cookie"] = cookie
data = None
if payload:
if method == "POST":
if "Content-Type" in headers and headers["Content-Type"] == "application/json":
data = json.dumps(payload)
else:
data = urlencode(payload)
elif method == "GET":
url += "%s%s" % ("&" if "?" in url else "?", urlencode(payload))
ident = ident or url
return HttpRequest(ident=ident, method=method, url=url, headers=headers, data=data)
def send_request(self, request):
# type: (Request) -> dict|str
json_data = ""
try:
log("request: %s" % request, xbmc.LOGDEBUG)
response = urllib.request.urlopen(request) # type: addinfourl
content_type = response.headers.getheader("content-type") # type: str
content = response.read()
if not content_type.startswith("application/json"):
return content
response = json.loads(content)
except urllib.error.URLError as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
response = {
"__error": {
"message": str(ex),
"code": self.E_HTTP_REQUEST_FAILED,
"details": {
"url": request.get_full_url(),
"data": request.data,
"headers": request.headers
},
}
}
pass
except ValueError as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
response = {
"__error": {
"message": "Unable decode server response: %s" % str(ex),
"code": self.E_JSON_DECODE,
"details": {
"response": json_data
}
}
}
pass
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
response = {
"__error": {
"message": "%s: %s" % (type(ex), str(ex)),
"code": self.E_UNKNOW_ERROR
}
}
pass
return response
def make_request(self, uri, payload=None, method="GET", headers=None):
# type: (str, dict, str, dict) -> dict
"""
Makes HTTP request to the IPTV API
:param uri: URL of the IPTV API
:param payload: Payload data
:param method: HTTP method
:param headers: Additional HTTP headers
:return:
"""
if self.auth_status != self.AUTH_STATUS_OK and not self.is_login_request(uri, payload, method, headers):
self.login()
return self.make_request(uri, payload, method, headers)
request = self.prepare_request(uri, payload, method, headers)
response = self.send_request(request)
if "__error" in response:
if self.is_login_request(uri, payload):
self.auth_status = self.AUTH_STATUS_NONE
try:
os.remove(self.cookie_file)
except OSError:
pass
return response
def do_send_request(self, queue, results, stop_event, wait=None):
# type: (Queue, dict[str, dict], Event, float) -> None
while not stop_event.is_set():
request = queue.get() # type: HttpRequest
results[request.ident] = self.send_request(request)
queue.task_done()
stop_event.wait(wait)
def send_parallel_requests(self, requests, wait=None, num_threads=None):
# type: (list[HttpRequest], float, int) -> dict[str, dict]
num_threads = len(requests) if num_threads is None else num_threads
queue = Queue(num_threads * 2)
results = dict()
stop_event = threading.Event()
for i in range(num_threads):
thread = threading.Thread(target=self.do_send_request, args=(queue, results, stop_event, wait,))
thread.setDaemon(True)
thread.start()
for req in requests:
queue.put(req)
queue.join()
while queue.unfinished_tasks > 0:
continue
stop_event.set()
return results
def get_epg_gh(self, channel):
# type: (Channel) -> OrderedDict[int, Program]
programs = OrderedDict()
if self._epg_map is None:
self._epg_map = self.make_request("https://kodi-iptv-addons.github.io/EPG/map.json?%s" % time_now())
norm = normalize(channel.name)
if (norm in self._epg_map) is False:
return programs
cid = self._epg_map.get(norm)
requests = []
days = (self.archive_ttl // DAY) + 5
start = int(time_now() - self.archive_ttl)
for i in range(days):
day = format_date(start + (i * DAY), custom_format="%Y-%m-%d")
request = self.prepare_request(
"https://kodi-iptv-addons.github.io/EPG/%s/%s.json" % (cid, day)
)
requests.append(request)
results = self.send_parallel_requests(requests)
epg = dict()
for key in sorted(results.keys()):
response = results[key]
is_error, error = Api.is_error_response(response)
if is_error:
log("error: %s" % error if is_error else response, xbmc.LOGDEBUG)
return programs
for k, v in response.items():
epg[int(k)] = v
prev = None # type: Program
for key in sorted(epg.keys()):
val = epg[key]
program = Program(
channel.cid,
channel.gid,
val["start"],
val["stop"],
val["title"],
val["descr"],
channel.archive,
val["image"]
)
if prev is not None:
program.prev_program = prev
prev.next_program = program
programs[program.ut_start] = prev = program
return programs
@staticmethod
def is_error_response(response):
# type: (dict) -> (bool, dict or None)
if "__error" in response:
return True, response["__error"]
return False, None
+86
View File
@@ -0,0 +1,86 @@
import os
import shutil
from datetime import datetime
from ..skinutils import DocumentCache, get_current_skin_path, get_local_skin_path, is_invalid_local_skin, \
copy_skin_to_userdata, do_write_test, reload_skin, get_skin_name
from ..skinutils.fonts import FontManager as SkinUtilsFontManager
class FontManagerException(Exception):
INSTALL_NEEDED = 1
RESTART_NEEDED = 2
INSTALL_NOT_NEEDED = 3
def __init__(self, message, *args):
self.message = message
super().__init__(*args)
class FontManager(object, SkinUtilsFontManager):
FONTS = {
"script.module.iptvlib-font_MainMenu": "script.module.iptvlib-NotoSans-Bold.ttf",
"script.module.iptvlib-font30_title": "script.module.iptvlib-NotoSans-Bold.ttf",
"script.module.iptvlib-font30": "script.module.iptvlib-NotoSans-Regular.ttf",
"script.module.iptvlib-font14": "script.module.iptvlib-NotoSans-Regular.ttf",
"script.module.iptvlib-font12": "script.module.iptvlib-NotoSans-Regular.ttf",
}
script_path = None
def __init__(self, script_path):
self.__installed_names = []
self.__installed_fonts = []
self.__doc_cache = DocumentCache()
self.script_path = script_path
def check_fonts(self):
if 'skin.estuary' == get_skin_name():
raise FontManagerException(FontManagerException.INSTALL_NOT_NEEDED)
if self.is_restart_needed():
raise FontManagerException(FontManagerException.RESTART_NEEDED)
if get_current_skin_path() != get_local_skin_path():
if not self.is_writable():
raise FontManagerException(FontManagerException.INSTALL_NOT_NEEDED)
raise FontManagerException(FontManagerException.INSTALL_NEEDED)
else:
if not os.path.isdir(get_local_skin_path()):
raise FontManagerException(FontManagerException.INSTALL_NEEDED)
for f in self._list_skin_font_files():
self.__doc_cache.add(f)
self.install_fonts()
def install_fonts(self):
xml_path = os.path.join(self.script_path, "resources", "skins", "Default", "720p", "font.xml")
font_dir = os.path.join(self.script_path, "resources", "skins", "Default", "fonts")
self.install_file(xml_path, font_dir)
reload_skin()
@staticmethod
def install_skin():
copy_skin_to_userdata(ask_user=False)
@staticmethod
def is_writable():
# type: () -> bool
skin_path = get_local_skin_path()
return not os.access(skin_path, os.W_OK) or not do_write_test(skin_path)
@staticmethod
def is_restart_needed():
# type: () -> bool
current_skin_path = get_current_skin_path()
local_skin_path = get_local_skin_path()
if os.path.isdir(local_skin_path) and current_skin_path != local_skin_path:
if is_invalid_local_skin():
time_suffix = datetime.now().strftime('%Y%m%d%H%M%S')
shutil.move(local_skin_path, local_skin_path + '-skinutils-' + time_suffix)
copy_skin_to_userdata(ask_user=False)
return True
return False
def __del__(self):
pass
+91
View File
@@ -0,0 +1,91 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import re
class M3u8Channel(object):
def __init__(self, name, group=None, url=None, id=None):
self.name = str(name, "utf-8") if type(name) is not str else name
self.group = str(group, "utf-8") if group and type(group) is not str else group
self.url = url
class M3u8Item(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __getitem__(self, item):
return self.__dict__.get(item, None)
def reg(pattern, line):
# type: (str, str) -> str or None
res = re.search(pattern, line)
return res.group(1) if res else None
class M3u8Parser(object):
EXTM3U = '#EXTM3U'
EXTINF = '#EXTINF'
EXTGRP = '#EXTGRP'
def __init__(self):
self.channel = None
pass
def parse(self, content, on_item):
# type: (str, callable(M3u8Item)) -> None
args = dict()
for line in content.splitlines():
line = line.strip()
if line.startswith(self.EXTM3U):
args = dict({
"id": self.EXTM3U,
"url-epg": reg('\s+url-epg\s*=\s*"([^"]+)"', line),
"url-logo": reg('\s+url-logo\s*=\s*"([^"]+)"', line)
})
on_item(M3u8Item(**args))
args = dict()
if line.startswith(self.EXTINF):
if len(args) > 0:
on_item(M3u8Item(**args))
options, name = line.replace(self.EXTINF + ':', '').split(',') # type: (str, str)
tvg_rec = reg('\s+tvg-rec\s*=\s*"([^"]+)"', line) or 0
args = dict({
"id": self.EXTINF,
"name": name,
"tvg-id": reg('\s+tvg-id\s*=\s*"([^"]+)"', line),
"tvg-logo": reg('\s+tvg-logo\s*=\s*"([^"]+)"', line),
"group-title": reg('\s+group-title\s*=\s*"([^"]+)"', line),
"tvg-rec": int(tvg_rec),
"adult": reg('\s+adult\s*=\s*"([^"]+)"', line),
})
if line.startswith(self.EXTGRP):
args["group-title"] = bytes(line.replace(self.EXTGRP + ':', '').strip(), "utf-8")
if line.startswith('http'):
args["url"] = line
if args["tvg-id"] is None:
for p in args['url'].split("/"):
if p.isdigit():
args["tvg-id"] = p
break
on_item(M3u8Item(**args))
args = dict()
+142
View File
@@ -0,0 +1,142 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
import xbmcgui
from . import *
from .api import Api, ApiException
from .fonts import FontManager, FontManagerException
from .tvdialog import TvDialog
class MainWindow(xbmcgui.WindowXML, WindowMixin):
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__) + '/../../')
check_settings_handler = None # type: callable()
api = None # type: Api
tv_dialog = None # type: TvDialog
tv_dialog_xml_file = "tv_dialog_font.xml"
fm = None # type: FontManager
_initialized = False # type: bool
def __init__(self, *args, **kwargs):
self.fm = FontManager(MainWindow.SCRIPT_PATH)
self.check_skin()
if "check_settings_handler" in kwargs:
self.check_settings_handler = kwargs.pop("check_settings_handler", None)
super(MainWindow, self).__init__(**kwargs)
def check_skin(self):
try:
self.fm.check_fonts()
except FontManagerException as ex:
if ex.message == FontManagerException.INSTALL_NEEDED:
xbmcgui.Dialog().ok(
addon.getAddonInfo("name") + " ",
get_string(TEXT_INSTALL_EXTRA_RESOURCES_ID)
)
self.fm.install_skin()
self.close()
sys.exit()
elif ex.message == FontManagerException.RESTART_NEEDED:
xbmcgui.Dialog().ok(
addon.getAddonInfo("name") + " ",
get_string(TEXT_PLEASE_RESTART_KODI_ID)
)
self.close()
sys.exit()
elif ex.message == FontManagerException.INSTALL_NOT_NEEDED:
self.tv_dialog_xml_file = "tv_dialog.xml"
@classmethod
def create(cls, check_settings_handler):
return cls("main_window.xml", MainWindow.SCRIPT_PATH, 'Default', '720p',
check_settings_handler=check_settings_handler)
def onInit(self):
if self._initialized is True:
return
if self.check_settings() is False:
self.close()
return
self._initialized = True
self.tv_dialog = TvDialog(self.tv_dialog_xml_file, MainWindow.SCRIPT_PATH, 'Default', '720p', main_window=self)
self.tv_dialog.doModal()
del self.tv_dialog
def close(self):
self.is_closing = True
try:
if self.tv_dialog:
self.tv_dialog.close()
del self.api
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
super(MainWindow, self).close()
def check_settings(self):
if callable(self.check_settings_handler) is False or self.check_settings_handler() is False:
return False
try:
self.api.login()
except ApiException as ex:
log("Exception %s: message=%s, code=%s" % (type(ex), ex.message, ex.code))
log(traceback.format_exc(), xbmc.LOGDEBUG)
dialog = xbmcgui.Dialog()
if ex.code == Api.E_API_ERROR:
if dialog.yesno(
addon.getAddonInfo("name"),
get_string(TEXT_AUTHENTICATION_FAILED_ID) + ":",
ex.message,
get_string(TEXT_CHECK_SETTINGS_ID)):
addon.openSettings()
return self.check_settings()
elif ex.code == Api.E_HTTP_REQUEST_FAILED:
error = ex.message
if "Errno 8" in ex.message:
error = get_string(TEXT_PLEASE_CHECK_INTERNET_CONNECTION_ID)
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_HTTP_REQUEST_ERROR_ID) + ":\n" +
error
)
elif ex.code == Api.E_JSON_DECODE:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_RESPONSE_FROM_SERVICE_PROVIDER_ID) + ":\n" +
ex.message
)
else:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_ERROR_OCCURRED_ID) + ":\n" +
ex.message
)
return False
return True
+302
View File
@@ -0,0 +1,302 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
from collections import OrderedDict
import xbmcgui
from . import *
class Model(object):
data = None # type: dict
API = None
def __init__(self, data):
self.data = data
self._listitem = None
def __getattr__(self, attr):
return self.data[attr]
def get_icon(self):
return ""
def get_listitem(self):
if self._listitem is None:
self._listitem = xbmcgui.ListItem()
for key, value in self.data.items():
if key == 'icon':
value = self.get_icon()
if isinstance(value, (type(None), str, int, float, bool)) is False:
continue
# if not isinstance(value, str):
# value = str(value)
# try:
# value = value.decode('utf-8')
# except UnicodeError:
# value = value.encode('utf-8')
self._listitem.setProperty(key, value)
return self._listitem
class Group(Model):
gid = None # type: str
name = None # type: str
channels = None # type: OrderedDict[str, Channel]
number = None # type: int
def __init__(self, gid, name, channels, number=None):
# type: (str, str, OrderedDict[str, Channel], int) -> Group
self.gid = gid
self.name = name
self.channels = OrderedDict(channels)
self.number = number
super(Group, self).__init__({"gid": gid, "group_name": name, "icon": "", "number": number})
def get_icon(self):
number = (self.number if self.number is not None else int(self.data["gid"])) % 20
return "%s.png" % (number if number != 0 else 20)
def __repr__(self):
return "%s (%s)" % (self.name, self.gid)
class Channel(Model):
cid = None # type: str
gid = None # type: str
name = None # type: str
icon = None # type: str
epg = None # type: bool
archive = None # type: bool
protected = None # type: bool
url = None # type: str
_programs = None # type: dict[int, Program]
def __init__(self, cid, gid, name, icon, epg, archive, protected=False, url=None):
# type: (str, str, str, str, bool, bool, bool, str) -> Channel
self.cid = cid
self.gid = gid
self.name = name
self.icon = icon
self.epg = epg
self.archive = archive
self.protected = protected
self.url = url
self._programs = OrderedDict()
channel_data = {
"cid": cid,
"gid": gid,
"channel_name": name,
"icon": icon,
"epg": epg,
"archive": archive,
"protected": protected,
}
super(Channel, self).__init__(channel_data)
def get_icon(self):
return addon.getAddonInfo('icon') if not self.icon else self.icon
def get_current_program(self):
# type: () -> Program
return self.get_program_by_time(int(time_now()))
def get_program_by_time(self, timestamp):
# type: (int) -> Program
for ut_start, program in list(self.programs.items()):
if program.ut_start == timestamp:
return program
if program.ut_start > timestamp and program.prev_program is not None:
return program.prev_program
return Program.factory(self)
@property
def programs(self):
if len(self._programs) == 0:
try:
real_programs = self.API.get_epg(self.cid) # type: OrderedDict
if len(real_programs) == 0:
ut_start = timestamp_to_midnight(time_now())
real_programs[ut_start] = Program.factory(self, ut_start, ut_start + HOUR)
first_program = real_programs[next(iter(real_programs.keys()))] # type: Program
last_program = real_programs[next(reversed(list(real_programs.keys())))] # type: Program
# prepend epg with dummy entries
if first_program.ut_start > time_now() - self.API.archive_ttl:
start_time = int(time_now() - self.API.archive_ttl) - DAY
else:
start_time = first_program.ut_start - DAY
start_time = timestamp_to_midnight(start_time)
programs = Program.get_dummy_programs(self, start_time, first_program.ut_start)
last_prepend_program = programs[next(reversed(list(programs.keys())))]
programs.update(real_programs)
first_program.prev_program = last_prepend_program
last_prepend_program.next_program = first_program
# append epg with dummy entries
if last_program.ut_end == 0:
last_program.ut_end = last_program.ut_start + HOUR
end_time = timestamp_to_midnight(last_program.ut_end + DAY * 2)
append_programs = Program.get_dummy_programs(self, last_program.ut_end, end_time)
first_append_program = append_programs[next(iter(append_programs.keys()))] # type: Program
programs.update(append_programs)
last_program.next_program = first_append_program
first_append_program.prev_program = last_program
prev = None # type: Program
for key in sorted(programs.keys()):
program = programs[key]
if prev:
program.prev_program = prev
program.prev_program.next_program = program
self._programs[key] = prev = programs[key]
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
start_time = timestamp_to_midnight(time_now() - self.API.archive_ttl - DAY)
end_time = timestamp_to_midnight(time_now() + TWODAYS)
self._programs = Program.get_dummy_programs(self, start_time, end_time)
return self._programs
def __repr__(self):
return "%s (%s/%s)" % (self.name, self.gid, self.cid)
class Program(Model):
cid = None # type: str
gid = None # type: str
ut_start = None # type: int
ut_end = None # type: int
length = None # type: int
title = None # type: str
descr = None # type: str
epg = None # type: bool
archive = None # type: bool
image = None # type: str
prev_program = None # type: Program
next_program = None # type: Program
@staticmethod
def factory(channel, ut_start=None, ut_end=None, length=HOUR,
title=get_string(TEXT_NO_INFO_AVAILABLE_ID),
descr=get_string(TEXT_NO_INFO_AVAILABLE_ID)):
# type: (Channel, int, int, int, str, str) -> Program
ut_start = str_to_timestamp(format_date(time_now(), custom_format="%d%m%y%H"), "%d%m%y%H") \
if ut_start is None else ut_start
ut_end = ut_start + length if ut_end is None else ut_end
return Program(channel.cid, channel.gid, int(ut_start), int(ut_end), title, descr, channel.archive)
@staticmethod
def get_dummy_programs(channel, start_time, end_time):
# type: (Channel, int, int) -> OrderedDict[int, Program]
programs = OrderedDict()
ut_start = start_time
prev = None
while ut_start < end_time:
ut_end = end_time if (ut_start + HOUR) > end_time else (ut_start + HOUR)
program = Program.factory(channel, ut_start, ut_end)
if prev is not None:
program.prev_program = prev
prev.next_program = program
programs[program.ut_start] = prev = program
ut_start = ut_start + HOUR
return programs
def __init__(self, cid, gid, ut_start, ut_end, title, descr, archive=False, image=None):
# type: (str, str, int, int, str, str, bool, str) -> Program
self.cid = cid
self.gid = gid
self.ut_start = ut_start
self.ut_end = ut_end
self.length = self.ut_end - self.ut_start
self.title = title
self.descr = descr
self.archive = archive
self.image = image
(img_s, img_m, img_l) = self.get_image_urls(self.image)
program_data = {
"cid": self.cid,
"gid": self.gid,
"title": self.title,
"title_list": (self.title[:52] + '...') if len(self.title) > 55 else self.title,
"img_s": img_s,
"img_m": img_m,
"img_l": img_l,
"descr": self.descr if self.descr is not None else "",
"t_start": format_date(self.ut_start, custom_format="%H:%M"),
"t_end": format_date(self.ut_end, custom_format="%H:%M"),
"d_start": format_date(self.ut_start, custom_format="%A, %d.%m"),
"ut_start": self.ut_start,
"ut_end": self.ut_end
}
super(Program, self).__init__(program_data)
@staticmethod
def get_image_urls(image):
# type: (str) -> (str, str, str)
if image is None:
return "", "", ""
if '/large' in image:
return image.replace('/large', '/small'), image.replace('/large', '/normal'), image
else:
return "%s/176x99" % image, "%s/350x197" % image, "%s/700x394" % image
def is_playable(self):
# type: () -> bool
return self.is_live_now() or self.is_archive_now()
def is_past_now(self):
# type: () -> bool
return self.ut_end < time_now()
def is_live_now(self):
# type: () -> bool
return self.ut_start <= time_now() <= self.ut_end
def is_archive_now(self):
# type: () -> bool
now = time_now()
return bool(self.archive) is True \
and self.ut_end < now \
and self.ut_start > (now - self.API.archive_ttl)
def equals(self, other):
# type: (Program) -> bool
return str(self.cid) == str(other.cid) \
and self.ut_start == other.ut_start \
and self.ut_end == other.ut_end
def get_listitem(self):
listitem = Model.get_listitem(self)
status = " "
if self.is_past_now():
if self.archive is True:
status = "prg_status_archive.png"
else:
status = "prg_status_past.png"
elif self.is_live_now():
status = "prg_status_live.png"
listitem.setProperty("status", status)
listitem.setProperty("day_color", "1%s.png" % format_date(self.ut_start, custom_format="%w"))
return listitem
+134
View File
@@ -0,0 +1,134 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
from . import *
from .models import Program
from xbmcgui import ListItem
class Player(xbmc.Player):
program = None # type: Program
offset = None # type: int
ut_start = None # type: int
ut_end = None # type: int
on_playback_callback = None # type: callable()
last_known_position = None # type: int
def __init__(self):
pass
def update_last_known_position(self):
self.last_known_position = int(self.getTime() + self.ut_start)
@run_async
def play(self, item=None, program=None, offset=0, on_playback_callback=None, listitem=None, windowed=False):
# type: (str, Program, int, callable, ListItem, bool) -> None
self.program = program
self.offset = offset
if self.program:
self.ut_start = self.program.ut_start + self.offset
self.ut_end = self.program.ut_end
self.on_playback_callback = on_playback_callback
if listitem is not None:
super(Player, self).play(item=item, listitem=listitem, windowed=windowed)
else:
super(Player, self).play(item=item, windowed=windowed)
def get_program(self):
# type: () -> Program
if not self.isPlaying():
return self.program
player_time = self.getTime() + self.offset if self.is_live() is False else \
int(time_now()) - self.program.ut_start
ut_start = self.program.ut_start
program = self.program
while player_time > (program.ut_end - ut_start):
program = program.next_program
return program
def get_position(self):
# type: () -> float
if self.isPlaying() is False:
return -1
try:
program = self.get_program()
if self.is_live():
return time_now() - program.ut_start
if self.ut_start < program.ut_start:
return self.getTime() - (program.ut_start - self.ut_start)
return self.getTime() + self.offset
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
return -1
def get_percent(self, strict=False, adjust_secs=0):
# type: (bool, int) -> (float, int)
program = self.get_program()
position = self.get_position()
percent = secs_to_percent(program.length, position + adjust_secs)
if strict is True:
if percent <= 0:
percent = 0.01
elif percent >= 100:
percent = 99.99
return percent, position
def is_live(self):
# type: () -> bool
return self.offset == 0
def onPlayBackEnded(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackEnded")
def onPlayBackPaused(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackPaused")
def onPlayBackResumed(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackResumed")
def onPlayBackSeek(self, time, seekOffset):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackSeek", time=time, seekOffset=seekOffset)
def onPlayBackSeekChapter(self, chapter):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackSeekChapter", chapter=chapter)
def onPlayBackSpeedChanged(self, speed):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackSpeedChanged", speed=speed)
def onPlayBackStarted(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackStarted")
def onPlayBackStopped(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackStopped")
+678
View File
@@ -0,0 +1,678 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
from threading import Timer
from urllib.parse import quote
import xbmcgui
from . import *
from .api import Api, ApiException
from .models import Program, Channel
from .player import Player
from xbmcgui import ControlImage, ControlList, ListItem, ControlProgress, ControlSlider, ControlLabel
class TvDialog(xbmcgui.WindowXMLDialog, WindowMixin):
CTRL_DUMMY = 999
CTRL_PROGRAM_TITLE = 4000
CTRL_PROGRAM_PLAYTIME = 4001
CTRL_PROGRESS = 4002
CTRL_SLIDER = 4003
CTRL_PROGRAM_DURATION = 4004
CTRL_SKIP_PLAYBACK = 4005
CTRL_PROGRAM_STARTTIME = 4006
CTRL_SLIDER_BUTTON = 4007
CTRL_PROGRAM_CHANNEL_ICON = 4008
CTRL_DUMMY_ICON = 4009
CTRL_GROUPS = 4100
CTRL_CHANNELS = 4200
CTRL_PROGRAMS = 4300
ICON_OPEN = "open"
ICON_CLOSE = "close"
ICON_ERROR = "error"
ICON_PLAY = "play"
ICON_NONPLAY = "nonplay"
ICON_END = "end"
ICON_STOP = "stop"
ICON_SWING = "swing"
WINDOW_HOME = 10000
WINDOW_FULLSCREEN_VIDEO = 12005
ctrl_program_title = None # type: ControlLabel
ctrl_program_playtime = None # type: ControlLabel
ctrl_program_channel_icon = None # type: ControlImage
ctrl_dummy_icon = None # type: ControlImage
ctrl_progress = None # type: ControlProgress
ctrl_slider = None # type: ControlSlider
ctrl_program_duration = None # type: ControlLabel
ctrl_skip_playback = None # type: ControlLabel
ctrl_program_starttime = None # type: ControlLabel
ctrl_groups = None # type: ControlList
ctrl_channels = None # type: ControlList
ctrl_programs = None # type: ControlList
main_window = None
player = None # type: Player
api = None # type: Api
skip_secs = None # type: int
prev_skip_secs = None # type: int
prev_focused_id = None # type: int
playback_info_program = None # type: Program
timer_refocus = None # type: Timer
timer_slider_update = None # type: Timer
timer_skip_playback = None # type: Timer
timer_load_program_list = None # type: Timer
timer_idle = None # type: Timer
is_closing = False # type: bool
def __init__(self, *args, **kwargs):
self.main_window = kwargs.pop("main_window", None)
self.player = Player()
self.api = self.main_window.api
self.skip_secs = self.prev_skip_secs = 0
super(TvDialog, self).__init__(**kwargs)
@property
def addon_id(self):
return "%s:%s" % (self.api.__class__.__name__, addon.getAddonInfo('version'))
def close(self):
if self.is_closing:
return
self.is_closing = True
self.preload_icon(self.ICON_CLOSE, self.addon_id)
if self.timer_refocus:
self.timer_refocus.cancel()
del self.timer_refocus
if self.timer_slider_update:
self.timer_slider_update.cancel()
del self.timer_slider_update
if self.timer_skip_playback:
self.timer_skip_playback.cancel()
del self.timer_skip_playback
if self.timer_load_program_list:
self.timer_load_program_list.cancel()
del self.timer_load_program_list
if self.timer_idle:
self.timer_idle.cancel()
del self.timer_idle
if self.player.isPlaying():
self.player.stop()
del self.player
super(TvDialog, self).close()
def onInit(self):
try:
self.ctrl_program_title = self.getControl(self.CTRL_PROGRAM_TITLE)
self.ctrl_program_playtime = self.getControl(self.CTRL_PROGRAM_PLAYTIME)
self.ctrl_program_channel_icon = self.getControl(self.CTRL_PROGRAM_CHANNEL_ICON)
self.ctrl_dummy_icon = self.getControl(self.CTRL_DUMMY_ICON)
self.ctrl_progress = self.getControl(self.CTRL_PROGRESS)
self.ctrl_slider = self.getControl(self.CTRL_SLIDER)
self.ctrl_program_duration = self.getControl(self.CTRL_PROGRAM_DURATION)
self.ctrl_skip_playback = self.getControl(self.CTRL_SKIP_PLAYBACK)
self.ctrl_program_starttime = self.getControl(self.CTRL_PROGRAM_STARTTIME)
self.ctrl_groups = self.getControl(self.CTRL_GROUPS)
self.ctrl_channels = self.getControl(self.CTRL_CHANNELS)
self.ctrl_programs = self.getControl(self.CTRL_PROGRAMS)
self.defer_refocus_window()
self.preload_icon(self.ICON_OPEN, self.addon_id)
program = Program.factory(self.get_last_played_channel())
self.play_program(program)
self.load_lists()
self.reset_idle_timer()
except ApiException as ex:
log("Exception %s: message=%s" % (type(ex), ex.message))
log(traceback.format_exc(), xbmc.LOGDEBUG)
dialog = xbmcgui.Dialog()
if ex.code == Api.E_API_ERROR:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_SERVICE_ERROR_OCCURRED_ID) + ":\n" +
ex.message
)
elif ex.code == Api.E_HTTP_REQUEST_FAILED:
error = ex.message
if "Errno 8" in ex.message:
error = get_string(TEXT_PLEASE_CHECK_INTERNET_CONNECTION_ID)
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_HTTP_REQUEST_ERROR_ID) + ":\n" +
error
)
elif ex.code == Api.E_JSON_DECODE:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_RESPONSE_FROM_SERVICE_PROVIDER_ID) + ":\n" +
ex.message
)
else:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_ERROR_OCCURRED_ID) + ":\n" +
ex.message
)
self.main_window.close()
except Exception as ex:
self.preload_icon(self.ICON_ERROR, quote(str(ex).encode('utf-8')))
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
line1, line2 = (str(ex) + "\n").split("\n", 1)
dialog = xbmcgui.Dialog()
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_ERROR_OCCURRED_ID) + ":\n" +
line1 + "\n" +
line2
)
self.main_window.close()
def reset_idle_timer(self):
if addon.getSetting('stop_on_idle') == 'false' \
or addon.getSetting('stop_on_idle') is False:
return
if self.timer_idle:
self.timer_idle.cancel()
del self.timer_idle
self.timer_idle = threading.Timer(HOUR, self.show_idle_dialog)
self.timer_idle.start()
def show_idle_dialog(self, time_to_wait=60):
# type: (int) -> None
dialog = xbmcgui.DialogProgress()
dialog.create(addon.getAddonInfo("name"), get_string(TEXT_IDLE_DIALOG_ID))
secs = 0
increment = int(100 / time_to_wait)
cancelled = False
while secs < time_to_wait:
secs += 1
dialog.update(increment * secs, get_string(TEXT_IDLE_DIALOG_ID) +
get_string(TEXT_IDLE_DIALOG_COUNTDOWN_ID) % (time_to_wait - secs))
xbmc.sleep(1000)
if dialog.iscanceled():
cancelled = True
break
if cancelled is True:
return
dialog.close()
self.main_window.close()
def get_last_played_channel(self):
# type: () -> Channel
last_channel_id = addon.getSetting("last_channel_id") or None
if last_channel_id is None or last_channel_id == "None" or (last_channel_id in self.api.channels) is False:
last_channel_id = list(self.api.channels.keys())[0]
return self.api.channels[last_channel_id]
@run_async
def load_lists(self):
if self.ctrl_groups.size() == 0:
self.ctrl_groups.addItems(
[group.get_listitem() for group in list(self.api.groups.values()) if len(group.channels) > 0])
self.select_group_listitem(self.player.program.gid)
if self.ctrl_channels.size() == 0:
self.ctrl_channels.addItems([channel.get_listitem() for channel in list(self.api.channels.values())])
self.select_channel_listitem(self.player.program.cid, False)
if self.ctrl_programs.size() == 0:
channel = self.api.channels[self.player.program.cid]
self.ctrl_programs.addItems([prg.get_listitem() for prg in list(channel.programs.values())])
self.player.program = self.api.channels[self.player.program.cid].get_current_program()
self.select_program_listitem(self.player.program.ut_start, False)
def select_group_listitem(self, gid):
# type: (str) -> ListItem
for index in range(self.ctrl_groups.size()):
item = self.ctrl_groups.getListItem(index)
if item.getProperty("gid") == gid:
self.ctrl_groups.selectItem(index)
return item
def select_channel_listitem(self, cid, select_group=True):
# type: (str, bool) -> ListItem
item = self.ctrl_channels.getSelectedItem()
if item.getProperty("cid") == str(cid):
return item
for index in range(self.ctrl_channels.size()):
item = self.ctrl_channels.getListItem(index)
if item.getProperty("cid") == cid:
self.ctrl_channels.selectItem(index)
if select_group is True:
self.select_group_listitem(item.getProperty("gid"))
return item
def select_program_listitem(self, timestamp, select_channel=True):
# type: (int, bool) -> ListItem
for index in range(self.ctrl_programs.size()):
item = self.ctrl_programs.getListItem(index)
next_item = self.ctrl_programs.getListItem(index+1)
ut_start = int(item.getProperty("ut_start"))
ut_end = int(next_item.getProperty("ut_start")) \
if next_item else int(item.getProperty("ut_end"))
if ut_start <= int(timestamp) < ut_end:
self.ctrl_programs.selectItem(index)
if select_channel is True:
self.select_channel_listitem(item.getProperty("cid"))
return item
def defer_load_program_list(self, cid, select_timestamp):
# type: (str, int) -> None
if self.timer_load_program_list:
self.timer_load_program_list.cancel()
del self.timer_load_program_list
self.timer_load_program_list = threading.Timer(0.5, self.load_program_list, [cid, select_timestamp])
self.timer_load_program_list.start()
def load_program_list(self, cid, select_timestamp):
# type: (str, int) -> None
if self.ctrl_channels.getSelectedItem().getProperty("cid") != cid:
return
selected_program = self.ctrl_programs.getSelectedItem()
if selected_program is None or selected_program.getProperty("cid") != cid:
channel = self.api.channels[cid]
self.ctrl_programs.reset()
self.ctrl_programs.addItems([program.get_listitem() for program in list(channel.programs.values())])
self.select_program_listitem(select_timestamp, False)
def play_program(self, program, offset=0):
# type: (Program, int) -> None
if program.is_playable() is False:
self.preload_icon(self.ICON_NONPLAY, normalize(self.api.channels[program.cid].name))
dialog = xbmcgui.Dialog()
dialog.ok(addon.getAddonInfo("name"), "\n" + get_string(TEXT_NOT_PLAYABLE_ID))
return
try:
if program.is_live_now():
if offset > 0:
url = self.api.get_stream_url(program.cid, program.ut_start + offset)
else:
url = self.api.get_stream_url(program.cid)
elif program.is_archive_now():
offset += 1
url = self.api.get_stream_url(program.cid, program.ut_start + offset)
else:
url = self.api.get_stream_url(program.cid)
offset = 0
if self.player.program and self.player.program.cid != program.cid:
self.preload_icon(self.ICON_SWING, normalize(self.api.channels[self.player.program.cid].name))
self.player.play(url, program, offset, self.on_playback_callback)
addon.setSetting("last_channel_id", str(program.cid))
except ApiException as ex:
self.preload_icon(self.ICON_ERROR, quote(ex.message.encode('utf-8')))
log("Exception %s: message=%s, code=%s" % (type(ex), ex.message, ex.code))
log(traceback.format_exc(), xbmc.LOGDEBUG)
dialog = xbmcgui.Dialog()
dialog.ok(addon.getAddonInfo("name"), get_string(TEXT_SERVICE_ERROR_OCCURRED_ID) + "\n" + ex.message)
def on_playback_callback(self, event, **kwargs):
# type: (str, dict) -> None
if self.is_closing:
return
log(event, xbmc.LOGDEBUG)
if event == "onPlayBackEnded":
if self.player.program:
if self.player.program.is_live_now():
program = self.api.channels[self.player.program.cid].get_current_program()
self.play_program(program)
else:
offset = self.player.last_known_position - self.player.program.ut_start
self.play_program(self.player.program, offset)
self.preload_icon(self.ICON_END, normalize(self.api.channels[self.player.program.cid].name))
elif event == "onPlayBackStopped":
self.preload_icon(self.ICON_STOP, normalize(self.get_last_played_channel().name))
dialog = xbmcgui.Dialog()
dialog.ok(addon.getAddonInfo("name"), "\n\n" + get_string(TEXT_NOT_PLAYABLE_ID))
self.setFocusId(self.CTRL_CHANNELS)
elif event == "onPlayBackStarted":
self.update_playback_info()
def defer_refocus_window(self):
if self.timer_refocus:
self.timer_refocus.cancel()
self.timer_refocus = None
if not self.is_closing:
self.timer_refocus = threading.Timer(5, self.refocus_window)
self.timer_refocus.start()
def refocus_window(self):
if xbmcgui.getCurrentWindowId() == self.WINDOW_HOME:
xbmc.executebuiltin('ActivateWindow(%s)' % self.WINDOW_FULLSCREEN_VIDEO)
self.defer_refocus_window()
def preload_icon(self, a, b='', c=1):
try:
i = unique(x(h2), x(h1)).format(self.addon_id, a, b, c, z(unique(self.api.client_id, x(h1))), time_now())
self.ctrl_dummy_icon.setImage(i, False)
except:
pass
# noinspection PyPep8Naming
def onAction(self, action):
action_id = action.getId()
focused_id = self.getFocusId()
self.reset_idle_timer()
if focused_id == self.CTRL_DUMMY: # no controls are visible
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
confirm = xbmcgui.Dialog()
yesno = bool(confirm.yesno(addon.getAddonInfo("name"), " ", get_string(TEXT_SURE_TO_EXIT_ID)))
del confirm
if yesno is True:
self.main_window.close()
if action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_SLIDER)
self.update_playback_info()
self.prev_focused_id = self.CTRL_DUMMY
return True
elif focused_id == self.CTRL_SLIDER: # navigation within current playback details
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
self.reset_skip_playback()
self.prev_focused_id = self.CTRL_DUMMY
return True
elif action_id in [xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT]:
if self.prev_focused_id != focused_id:
self.update_playback_info()
self.prev_focused_id = focused_id
return True
program = self.player.get_program()
if program.archive is False:
self.update_playback_info()
show_small_popup(addon.getAddonInfo("name"), get_string(TEXT_CHANNEL_HAS_NO_ARCHIVE_ID))
self.prev_focused_id = focused_id
return True
if self.player.is_live():
if action_id == xbmcgui.ACTION_MOVE_LEFT and self.api.diff_live_archive > TENSECS:
self.prev_focused_id = focused_id
confirm = xbmcgui.Dialog()
yesno = bool(
confirm.yesno(
addon.getAddonInfo("name"),
get_string(TEXT_ARCHIVE_NOT_AVAILABLE_YET_ID),
get_string(TEXT_JUMP_TO_ARCHIVE_ID)
)
)
del confirm
if yesno is False:
self.skip_secs = 0
self.update_playback_info()
return True
self.skip_secs = self.api.diff_live_archive * -1
self.update_playback_info()
self.defer_skip_playback()
return True
elif action_id == xbmcgui.ACTION_MOVE_RIGHT and self.skip_secs >= 0:
self.update_playback_info()
show_small_popup(addon.getAddonInfo("name"), get_string(TEXT_LIVE_NO_FORWARD_SKIP_ID))
self.prev_focused_id = focused_id
return True
if self.playback_info_program is None:
self.playback_info_program = program
curr_position = percent_to_secs(self.playback_info_program.length, self.ctrl_progress.getPercent())
new_position = percent_to_secs(self.playback_info_program.length, self.ctrl_slider.getPercent())
self.skip_secs = new_position - curr_position
if self.ctrl_slider.getPercent() == 0 and action_id == xbmcgui.ACTION_MOVE_LEFT:
self.playback_info_program = self.playback_info_program.prev_program
self.set_slider(100.)
self.prev_skip_secs += self.skip_secs
self.skip_secs = 0
elif self.ctrl_slider.getPercent() == 100 and action_id == xbmcgui.ACTION_MOVE_RIGHT:
self.playback_info_program = self.playback_info_program.next_program
self.set_slider(0.)
self.prev_skip_secs += self.skip_secs
self.skip_secs = 0
self.ctrl_skip_playback.setLabel(format_secs(self.skip_secs + self.prev_skip_secs, "skip"))
self.defer_skip_playback()
self.update_playback_info()
elif focused_id == self.CTRL_GROUPS: # navigation within channel groups
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
elif action_id in [xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_MOVE_UP]:
selected_group = self.ctrl_groups.getSelectedItem()
gid = selected_group.getProperty("gid")
selected_channel = self.ctrl_channels.getSelectedItem()
if selected_channel.getProperty("gid") == gid:
self.prev_focused_id = focused_id
return True
for index in range(self.ctrl_channels.size()):
item = self.ctrl_channels.getListItem(index)
if item.getProperty("gid") == gid:
self.ctrl_channels.selectItem(index)
self.prev_focused_id = focused_id
self.defer_load_program_list(item.getProperty("cid"), int(time_now()))
return True
elif action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_DUMMY)
selected_channel = self.ctrl_channels.getSelectedItem()
channel = self.api.channels[selected_channel.getProperty("cid")]
program = channel.get_current_program()
if program is not None:
self.play_program(program)
elif focused_id == self.CTRL_CHANNELS: # navigation within channels
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
elif action_id == xbmcgui.ACTION_MOVE_RIGHT:
selected_channel = self.ctrl_channels.getSelectedItem()
cid = selected_channel.getProperty("cid")
selected_program = self.ctrl_programs.getSelectedItem()
if selected_program and selected_program.getProperty("cid") != cid:
self.defer_load_program_list(cid, int(time_now()))
elif action_id in [xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_MOVE_UP]:
if self.prev_focused_id == self.CTRL_SLIDER:
if self.ctrl_channels.getSelectedItem().getProperty("cid") != self.player.program.cid:
cid = self.player.program.cid
timestamp = int(self.player.get_program().ut_start + self.player.get_position())
self.ctrl_programs.reset()
self.select_channel_listitem(cid)
self.defer_load_program_list(cid, timestamp)
self.prev_focused_id = focused_id
return True
self.select_program_listitem(int(self.player.get_program().ut_start + self.player.get_position()))
selected_channel = self.ctrl_channels.getSelectedItem()
cid = selected_channel.getProperty("cid")
gid = selected_channel.getProperty("gid")
selected_program = self.ctrl_programs.getSelectedItem()
if selected_program is None or selected_program.getProperty("cid") != cid:
self.defer_load_program_list(cid, int(time_now()))
selected_group = self.ctrl_groups.getSelectedItem()
if selected_group.getProperty("gid") == gid:
self.prev_focused_id = focused_id
return True
for index in range(self.ctrl_groups.size()):
item = self.ctrl_groups.getListItem(index)
if item.getProperty("gid") == gid:
self.ctrl_groups.selectItem(index)
self.prev_focused_id = focused_id
return True
elif action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_DUMMY)
selected_channel = self.ctrl_channels.getSelectedItem()
channel = self.api.channels[selected_channel.getProperty("cid")]
program = channel.get_current_program()
if program is not None:
self.play_program(program)
elif focused_id == self.CTRL_PROGRAMS: # navigation within programs
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
elif action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_DUMMY)
selected_program = self.ctrl_programs.getSelectedItem()
channel = self.api.channels[selected_program.getProperty("cid")]
program = channel.get_program_by_time(int(selected_program.getProperty("ut_start")))
if program is not None:
if program.is_live_now() is False and program.archive is False:
show_small_popup(addon.getAddonInfo("name"), get_string(TEXT_CHANNEL_HAS_NO_ARCHIVE_ID))
self.prev_focused_id = focused_id
return True
if program.equals(self.player.get_program()) is True:
self.prev_focused_id = focused_id
return True
self.play_program(program)
self.prev_focused_id = focused_id
def reset_skip_playback(self):
self.skip_secs = self.prev_skip_secs = 0
self.ctrl_skip_playback.setLabel(format_secs(self.skip_secs, "skip"))
self.set_slider(self.ctrl_progress.getPercent())
self.playback_info_program = None
self.update_playback_info()
def set_slider(self, percent):
self.ctrl_slider.setPercent(percent)
def defer_skip_playback(self):
if self.timer_skip_playback:
self.timer_skip_playback.cancel()
self.timer_skip_playback = None
if not self.is_closing:
self.timer_skip_playback = threading.Timer(2, self.skip_playback)
self.timer_skip_playback.start()
def skip_playback(self):
self.setFocusId(self.CTRL_DUMMY)
if self.timer_skip_playback:
self.timer_skip_playback.cancel()
self.timer_skip_playback = None
secs_to_skip = self.skip_secs + self.prev_skip_secs
if secs_to_skip == 0:
return
program = self.player.get_program()
curr_pos = self.player.get_position()
new_pos = program.ut_start + curr_pos + secs_to_skip
if new_pos < program.ut_start:
while new_pos < program.ut_start:
program = program.prev_program
elif new_pos > program.ut_end:
while new_pos > program.ut_end:
program = program.next_program
offset = new_pos - program.ut_start
if (program.ut_start + offset) > int(time_now()):
self.reset_skip_playback()
return
self.play_program(program, int(offset))
self.reset_skip_playback()
def defer_update_playback_info(self):
if self.timer_slider_update:
self.timer_slider_update.cancel()
del self.timer_slider_update
if not self.is_closing:
interval = 1 if self.getFocusId() == self.CTRL_SLIDER else 30
self.timer_slider_update = threading.Timer(interval, self.update_playback_info)
self.timer_slider_update.start()
def update_playback_info(self):
try:
if self.main_window.is_closing:
return
if not self.player or not self.player.isPlaying():
return
self.player.update_last_known_position()
position = 0
if self.playback_info_program is not None \
and self.playback_info_program != self.player.get_program():
program = self.playback_info_program
percent = 100. if program.ut_start < self.player.get_program().ut_start else 0.
self.ctrl_progress.setPercent(percent)
else:
percent, position = self.player.get_percent(True)
self.ctrl_progress.setPercent(percent)
if self.skip_secs + self.prev_skip_secs == 0:
self.set_slider(percent)
program = self.player.get_program()
self.ctrl_program_playtime.setLabel(format_secs(int(position)))
self.ctrl_program_duration.setLabel(format_secs(program.length))
self.ctrl_program_title.setLabel(program.title)
self.ctrl_program_starttime.setLabel(format_date(program.ut_start, custom_format="%A, %d %b., %H:%M"))
channel = self.api.channels[self.player.program.cid]
self.ctrl_program_channel_icon.setImage(channel.get_icon())
if self.getFocusId() != self.CTRL_SLIDER:
self.preload_icon(self.ICON_PLAY, normalize(channel.name))
self.defer_update_playback_info()
except Exception as ex:
self.preload_icon(self.ICON_ERROR, quote(str(ex).encode('utf-8')))
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)