версия для 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

+20
View File
@@ -0,0 +1,20 @@
# 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.
#
+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)
+26
View File
@@ -0,0 +1,26 @@
Copyright (c) 2011 Mikel Azkolain
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of copyright holders nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+372
View File
@@ -0,0 +1,372 @@
"""
Created on 09/08/2011
@author: mikel
"""
__all__ = ["fonts", "includes"]
import logging
import os
import re
import shutil
import sys
import time
import xml.etree.ElementTree as ET
from datetime import datetime
from os import listdir
from os.path import isdir, dirname, basename
import xbmc
import xbmcgui
class SkinUtilsError(Exception):
pass
def reload_skin():
xbmc.executebuiltin("XBMC.ReloadSkin()")
def setup_logging():
# Keep comaptibility with Python2.6
if hasattr(logging, 'NullHandler'):
logger = logging.getLogger('skinutils')
logger.addHandler(logging.NullHandler())
def get_logger():
return logging.getLogger('skinutils')
def debug_log(msg):
get_logger().debug(msg)
def get_sha1_obj():
# SHA1 lib 2.4 compatibility
try:
from hashlib import sha1
return sha1()
except:
import sha
return sha.new()
def sha1_file(file, block_size=2 ** 20):
f = open(file, 'rb')
sha1 = get_sha1_obj()
while True:
data = f.read(block_size)
if not data:
break
sha1.update(data)
f.close()
return sha1.hexdigest()
def try_remove_file(file, wait=0.5, tries=10):
removed = False
num_try = 0
while num_try < tries and not removed:
try:
os.remove(file)
return True
except OSError:
num_try += 1
time.sleep(wait)
return False
def case_file_exists(file):
if not os.path.isfile(file):
return False
else:
file_dir = dirname(file)
if not isdir(file_dir):
return False
else:
dir_contents = listdir(file_dir)
return basename(file) in dir_contents
def get_current_skin_path():
return os.path.normpath(xbmc.translatePath("special://skin/"))
def get_skin_name():
return os.path.basename(get_current_skin_path())
def get_local_skin_path():
user_addons_path = xbmc.translatePath("special://home/addons")
return os.path.normpath(
os.path.join(user_addons_path, get_skin_name())
)
def copy_skin_to_userdata(ask_user=True):
# Warn user before doing this weird thing
d = xbmcgui.Dialog()
msg1 = "This addon needs to install some extra resources."
msg2 = "This installation requires a manual XBMC restart."
msg3 = "Begin installation now? After that it will exit."
make_copy = (
not ask_user or
d.yesno("Notice", msg1, msg2, msg3)
)
if make_copy:
# Get skin dest name
local_skin_path = get_local_skin_path()
# If it was not copied before...
if not os.path.exists(local_skin_path):
shutil.copytree(get_current_skin_path(), local_skin_path)
return make_copy
def is_invalid_local_skin():
# Get skin paths
current_skin_path = get_current_skin_path()
local_skin_path = get_local_skin_path()
# If the local path does not exist
if not os.path.isdir(local_skin_path):
return False
else:
# Get addon xml paths
current_xml = os.path.join(current_skin_path, 'addon.xml')
local_xml = os.path.join(local_skin_path, 'addon.xml')
# Both files must exist
if not os.path.isfile(current_xml) or not os.path.isfile(local_xml):
return True
# If sum of both files mismatch, got it!
elif sha1_file(current_xml) != sha1_file(local_xml):
return True
# Otherwise everything is ok
else:
return False
def fix_invalid_local_skin():
local_skin_path = get_local_skin_path()
time_suffix = datetime.now().strftime('%Y%m%d%H%M%S')
backup_skin_path = local_skin_path + '-skinutils-' + time_suffix
# Just move the skin, if it already exists someone is trolling us...
shutil.move(local_skin_path, backup_skin_path)
# And now do the real copy
copy_skin_to_userdata(ask_user=False)
# Inform the user about the operation...
d = xbmcgui.Dialog()
l1 = "Your local skin is not in use (probably outdated)."
l2 = "Press OK to apply a fix (archiving the old skin)."
l3 = "You will need to restart XBMC once more."
d.ok("Notice", l1 + '\n' + '\n' + l2 + '\n' + l3)
sys.exit()
# Skin was copied but XBMC was not restarted
def check_needs_restart():
# Get skin paths
current_skin_path = get_current_skin_path()
local_skin_path = get_local_skin_path()
# Local skin exists and does not match current skin path
if os.path.isdir(local_skin_path) and current_skin_path != local_skin_path:
# Check if the local skin is a leftover from a previous XBMC install
if is_invalid_local_skin():
fix_invalid_local_skin()
# Local skin is correct, a restart is needed
else:
d = xbmcgui.Dialog()
d.ok("Notice", "Restart XBMC to complete the installation.")
sys.exit()
def do_write_test(path):
test_file = os.path.join(path, 'write_test.txt')
get_logger().debug('performing write test: %s' % test_file)
try:
# Open and cleanup
open(test_file, 'w').close()
os.remove(test_file)
return True
except Exception:
return False
def skin_is_local():
return get_current_skin_path() == get_local_skin_path()
def check_skin_writability():
# Some debug info
debug_log("-- skinutils debug info --")
debug_log("current skin path: %s\n" % get_current_skin_path())
debug_log("local path should be: %s" % get_local_skin_path())
# Check if XBMC needs a restart
check_needs_restart()
# Get the current skin's path
skin_path = get_local_skin_path()
# Check if it's local or not (contained in userdata)
if not skin_is_local():
copy_skin_to_userdata()
sys.exit()
# Check if this path is writable
elif not os.access(skin_path, os.W_OK) or not do_write_test(skin_path):
raise IOError("Skin directory is not writable.")
def make_backup(path):
backup_path = path + '-skinutilsbackup'
# If the backup already exists, don't overwrite it
if not os.path.exists(backup_path):
shutil.copy(path, backup_path)
def restore_backup(path):
backup_path = path + '-skinutilsbackup'
# Do nothing if no backup exists
if os.path.exists(backup_path):
# os.rename is atomic on unix, and it will overwrite silently
if os.name != 'nt':
os.rename(backup_path, path)
# Windows will complain if the file exists
else:
os.remove(path)
os.rename(backup_path, path)
def is_invalid_xml(file):
contents = open(file, 'r').read()
# Check for invalid comments
pattern = re.compile('<!--(.*?)-->', re.MULTILINE | re.DOTALL)
group_pattern = re.compile('^-|--|-$')
for match in re.finditer(pattern, contents):
if re.match(group_pattern, match.group(1)) is not None:
return True
# Check also for whitespace prior to declaration
whitespace_pattern = re.compile('^\s+', re.MULTILINE)
return whitespace_pattern.match(contents) is not None
def sanitize_xml(file):
contents = open(file, 'r').read()
# Remove leading whitespace to declaration
contents = contents.lstrip()
# Strip invalid comments
p = re.compile('<!--.*?-->', re.MULTILINE | re.DOTALL)
clean_contents, num_repl = re.subn(p, '', contents)
open(file, 'w').write(clean_contents)
def install_resources():
pass
class DocumentCache:
__cached_docs = None
def __init__(self):
self.__cached_docs = {}
def _check_file_exists(self, file):
if not os.path.isfile(file):
raise IOError('File not found: %s' % file)
def contains(self, file):
return file in self.__cached_docs
def _check_file_known(self, file):
if not self.contains(file):
raise KeyError('Unknown file: %s' % file)
def list_files(self):
return self.__cached_docs.keys()
def items(self):
return self.__cached_docs.items()
def add(self, file):
self._check_file_exists(file)
self.__cached_docs[file] = None
def read(self, file):
self._check_file_exists(file)
# If there is no cached data...
if not self.contains(file) or self.__cached_docs[file] is None:
# Check if the file about to load is sane
if is_invalid_xml(file):
make_backup(file)
sanitize_xml(file)
# Parse the document
self.__cached_docs[file] = ET.parse(file)
return self.__cached_docs[file]
def write(self, file):
self._check_file_known(file)
# If there is a document in cache it may contain modifications
if self.__cached_docs[file] is not None:
make_backup(file)
self.__cached_docs[file].write(file)
def write_all(self):
for item in self.__cached_docs:
self.write(item)
def clear(self, file):
self._check_file_known(file)
self.__cached_docs[file] = None
def clear_all(self):
for item in self.__cached_docs:
self.clear(item)
def rollback(self, file):
self._check_file_known(file)
restore_backup(file)
self.clear(file)
def rollback_all(self):
for item in self.__cached_docs:
self.rollback(item)
setup_logging()
+210
View File
@@ -0,0 +1,210 @@
"""
Created on 09/08/2011
@author: mikel
"""
import os
import shutil
import xml.etree.ElementTree as ET
import xbmc
from . import SkinUtilsError, check_skin_writability, reload_skin, try_remove_file, case_file_exists, \
DocumentCache, get_logger
class FontXmlError(SkinUtilsError):
pass
class FontManager:
__installed_names = None
__installed_fonts = None
__doc_cache = None
def _list_skin_font_files(self):
font_xml_list = []
skin_path = xbmc.translatePath("special://skin/")
# Go into each dir. Could be 720, 1080...
for dir_item in os.listdir(skin_path):
dir_path = os.path.join(skin_path, dir_item)
if os.path.isdir(dir_path):
# Try with font.xml
file = os.path.join(dir_path, "font.xml")
if case_file_exists(file):
font_xml_list.append(file)
# Don't try the next step on windows, wasted time
file = os.path.join(dir_path, "Font.xml")
if case_file_exists(file):
font_xml_list.append(file)
return font_xml_list
def __init__(self):
self.__installed_names = []
self.__installed_fonts = []
self.__doc_cache = DocumentCache()
# Check if the environment is sane
check_skin_writability()
# Initialize the doc cache with the skin's files
for file in self._list_skin_font_files():
self.__doc_cache.add(file)
def is_name_installed(self, name):
return name in self.__installed_names
def is_font_installed(self, file):
return file in self.__installed_fonts
def _get_font_attr(self, node, name):
attrnode = node.find(name)
if attrnode is not None:
return attrnode.text
def _copy_font_file(self, file):
skin_font_path = xbmc.translatePath("special://skin/fonts/")
file_name = os.path.basename(file)
dest_file = os.path.join(skin_font_path, file_name)
# TODO: Unix systems could use symlinks
# Check if it's already there
if dest_file not in self.__installed_fonts:
self.__installed_fonts.append(dest_file)
# Overwrite if file exists
shutil.copyfile(file, dest_file)
def _add_font_attr(self, fontdef, name, value):
attr = ET.SubElement(fontdef, name)
attr.text = value
attr.tail = "\n\t\t\t"
return attr
def _install_font_def(self, skin_file, name, filename, size, style="", aspect="", linespacing=""):
# Add it to the registry
self.__installed_names.append(name)
# Get the parsed skin font file
font_doc = self.__doc_cache.read(skin_file)
# Iterate over all the fontsets on the file
for fontset in font_doc.getroot().findall("fontset"):
fontset.findall("font")[-1].tail = "\n\t\t"
fontdef = ET.SubElement(fontset, "font")
fontdef.text, fontdef.tail = "\n\t\t\t", "\n\t"
self._add_font_attr(fontdef, "name", name)
# We get the full file path to the font, so let's basename
self._add_font_attr(fontdef, "filename", os.path.basename(filename))
self._copy_font_file(filename)
last = self._add_font_attr(fontdef, "size", size)
if style:
if style in ["normal", "bold", "italics", "bolditalics", "lighten"]:
last = self._add_font_attr(fontdef, "style", style)
else:
raise FontXmlError(
"Font '%s' has an invalid style definition: %s"
% (name, style)
)
if aspect:
last = self._add_font_attr(fontdef, "aspect", aspect)
if linespacing:
last = self._add_font_attr(fontdef, "linespacing", linespacing)
last.tail = "\n\t\t"
def _install_file(self, doc_cache, user_file, skin_file, font_path):
user_doc = doc_cache.read(user_file)
# Handle only the first fontset
fontset = user_doc.getroot().find("fontset")
if len(fontset):
# Every font definition inside it
for item in fontset.findall("font"):
name = self._get_font_attr(item, "name")
# Basic check for malformed defs.
if name is None:
raise FontXmlError("Malformed XML: No name for font definition.")
# Omit already defined fonts
elif not self.is_name_installed(name):
font_file_path = os.path.join(
font_path, self._get_font_attr(item, "filename")
)
self._install_font_def(
skin_file,
name,
font_file_path,
self._get_font_attr(item, "size"),
self._get_font_attr(item, "style"),
self._get_font_attr(item, "aspect"),
self._get_font_attr(item, "linespacing")
)
def _get_res_folder(self, path):
return os.path.basename(os.path.dirname(path))
def _get_res_filename(self, res_folder, user_file):
path, ext = os.path.splitext(user_file)
return path + '-' + res_folder + ext
def install_file(self, user_file, font_path, commit=True, clear=True):
doc_cache = DocumentCache()
# If the file does not exist the following will fail
doc_cache.add(user_file)
# Install the file into every cached skin font file
for skin_file in self.__doc_cache.list_files():
res_folder = self._get_res_folder(skin_file)
res_file = self._get_res_filename(res_folder, user_file)
# If an specific res file exists...
if os.path.isfile(res_file):
self._install_file(doc_cache, res_file, skin_file, font_path)
# Otherwise use the dafault fallback
else:
self._install_file(doc_cache, user_file, skin_file, font_path)
# If save was requested
if commit:
self.__doc_cache.write_all()
# Clear cached docs after write (if requested)
if clear:
self.__doc_cache.clear_all()
def remove_font(self, name):
pass
def remove_installed_names(self):
self.__doc_cache.rollback_all()
def remove_installed_fonts(self):
for item in self.__installed_fonts:
if not try_remove_file(item):
get_logger().error(
'Failed removing font file "%s". XBMC may still be using it.' % item
)
def cleanup(self):
self.remove_installed_names()
# Reload skin so font files are no longer in use, and then delete them
reload_skin()
self.remove_installed_fonts()
def __del__(self):
self.cleanup()
+90
View File
@@ -0,0 +1,90 @@
"""
Created on 09/08/2011
@author: mikel
"""
import os
import xml.etree.ElementTree as ET
import xbmc
from . import SkinUtilsError, check_skin_writability, case_file_exists, DocumentCache, get_logger
class IncludeXmlError(SkinUtilsError):
pass
class IncludeManager:
__installed_names = None
__doc_cache = None
def _list_skin_include_files(self):
include_list = []
skin_path = xbmc.translatePath("special://skin/")
# Go into each dir. Could be 720, 1080...
for dir_item in os.listdir(skin_path):
dir_path = os.path.join(skin_path, dir_item)
if os.path.isdir(dir_path):
file = os.path.join(dir_path, "includes.xml")
if case_file_exists(file):
include_list.append(file)
file = os.path.join(dir_path, "Includes.xml")
if case_file_exists(file):
include_list.append(file)
return include_list
def __init__(self):
self.__installed_names = []
self.__doc_cache = DocumentCache()
# Check if the environment is sane
check_skin_writability()
# Initialize the doc cache with found files
for file in self._list_skin_include_files():
self.__doc_cache.add(file)
def is_name_installed(self, name):
return name in self.__installed_names
def add_include(self, name, node):
for file in self.__doc_cache.list_files():
doc = self.__doc_cache.read(file)
doc.getroot().append(node)
self.__installed_names.append(name)
def install_file(self, file, commit=True, clear=True):
get_logger().info('install include: %s' % file)
tree = ET.parse(file)
# Handle all includes
for item in tree.getroot().findall("include"):
name = item.get("name")
if name is None:
get_logger().warning('Only named includes are supported.')
elif self.is_name_installed(name):
get_logger().warning('Include name "%s" already installed' % name)
else:
self.add_include(name, item)
# If a save was requested
if commit:
self.__doc_cache.write_all()
# If we where requested to clear the cached docs
if clear:
self.__doc_cache.clear_all()
def remove_installed_names(self):
self.__doc_cache.rollback_all()
def cleanup(self):
self.remove_installed_names()
def __del__(self):
self.cleanup()