diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9e7cc30 --- /dev/null +++ b/__init__.py @@ -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. +# diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..5990bdb --- /dev/null +++ b/addon.xml @@ -0,0 +1,61 @@ + + + + + + + + + resources/skins/Default/media/icon.png + + + + Common library for IPTV Addons + Common library for IPTV Addons + all + GNU LESSER GENERAL PUBLIC LICENSE Version 3, June 2007 + https://github.com/kodi-iptv-addons + +v1.1.8 (2020-04-27) +- Fixed bug in URL resolver +v1.1.7 (2020-04-23) +- Fixed crash bug on exit +v1.1.6 (2020-04-15) +- Fixed minor bugs in EPG retrieval +- Added API method to resolve redirections in stream urls +v1.1.5 (2020-02-03) +- Fixed bugs in group display +v1.1.3 (2019-02-05) +- Increased scrolling timeout for program details +v1.1.2 (2019-01-31) +- Implemented usage of alternative EPG with program images +- Respect stop on idle addon settings +v1.0.21 (2018-12-03) +- Bug fixes in EPG generation +v1.0.19 (2018-11-26) +- Adapted skin for KODI 18 +v1.0.18 (2018-11-22) +- Bug fixes in EPG generation +v1.0.17 (2018-11-05) +- Bug fixes in EPG generation +v1.0.16 (2018-11-03) +- Improved EPG generation +v1.0.15 (2018-11-02) +- Improved error handling +- Added fonts used in skin +- Bug fixes +v1.0.2 (2018-10-12) +- Improved HTTP downloading +- Bug fixes +v1.0.1 (2018-10-11) +- Improved error handling +- Fixed bug in date formatting +- Added day color bar for EPG entries +v1.0.0 (2018-10-10) +- Initial release + + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..aa4df7d --- /dev/null +++ b/changelog.txt @@ -0,0 +1,37 @@ +v1.1.8 (2020-04-27) +- Fixed bug in URL resolver +v1.1.7 (2020-04-23) +- Fixed crash bug on exit +v1.1.6 (2020-04-15) +- Fixed minor bugs in EPG retrieval +- Added API method to resolve redirections in stream urls +v1.1.5 (2020-02-03) +- Fixed bugs in group display +v1.1.3 (2019-02-05) +- Increased scrolling timeout for program details +v1.1.2 (2019-01-31) +- Implemented usage of alternative EPG with program images +- Respect stop on idle addon settings +v1.0.21 (2018-12-03) +- Bug fixes in EPG generation +v1.0.19 (2018-11-26) +- Adapted skin for KODI 18 +v1.0.18 (2018-11-22) +- Bug fixes in EPG generation +v1.0.17 (2018-11-05) +- Bug fixes in EPG generation +v1.0.16 (2018-11-03) +- Improved EPG generation +v1.0.15 (2018-11-02) +- Improved error handling +- Added fonts used in skin +- Bug fixes +v1.0.2 (2018-10-12) +- Improved HTTP downloading +- Bug fixes +v1.0.1 (2018-10-11) +- Improved error handling +- Fixed bug in date formatting +- Added day color bar for EPG entries +v1.0.0 (2018-10-10) +- Initial release diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..9e7cc30 --- /dev/null +++ b/lib/__init__.py @@ -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. +# diff --git a/lib/iptvlib/__init__.py b/lib/iptvlib/__init__.py new file mode 100644 index 0000000..323642d --- /dev/null +++ b/lib/iptvlib/__init__.py @@ -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' diff --git a/lib/iptvlib/api.py b/lib/iptvlib/api.py new file mode 100644 index 0000000..6a0d78a --- /dev/null +++ b/lib/iptvlib/api.py @@ -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 diff --git a/lib/iptvlib/fonts.py b/lib/iptvlib/fonts.py new file mode 100644 index 0000000..d5794c8 --- /dev/null +++ b/lib/iptvlib/fonts.py @@ -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 diff --git a/lib/iptvlib/m3u8.py b/lib/iptvlib/m3u8.py new file mode 100644 index 0000000..bafccf6 --- /dev/null +++ b/lib/iptvlib/m3u8.py @@ -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() diff --git a/lib/iptvlib/mainwindow.py b/lib/iptvlib/mainwindow.py new file mode 100644 index 0000000..abc7f85 --- /dev/null +++ b/lib/iptvlib/mainwindow.py @@ -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 diff --git a/lib/iptvlib/models.py b/lib/iptvlib/models.py new file mode 100644 index 0000000..72ae777 --- /dev/null +++ b/lib/iptvlib/models.py @@ -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 diff --git a/lib/iptvlib/player.py b/lib/iptvlib/player.py new file mode 100644 index 0000000..29fd0e1 --- /dev/null +++ b/lib/iptvlib/player.py @@ -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") diff --git a/lib/iptvlib/tvdialog.py b/lib/iptvlib/tvdialog.py new file mode 100644 index 0000000..5ff1082 --- /dev/null +++ b/lib/iptvlib/tvdialog.py @@ -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) diff --git a/lib/skinutils/LICENSE.txt b/lib/skinutils/LICENSE.txt new file mode 100644 index 0000000..6c800e8 --- /dev/null +++ b/lib/skinutils/LICENSE.txt @@ -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. diff --git a/lib/skinutils/__init__.py b/lib/skinutils/__init__.py new file mode 100644 index 0000000..f043e22 --- /dev/null +++ b/lib/skinutils/__init__.py @@ -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() diff --git a/lib/skinutils/fonts.py b/lib/skinutils/fonts.py new file mode 100644 index 0000000..a372b5d --- /dev/null +++ b/lib/skinutils/fonts.py @@ -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() diff --git a/lib/skinutils/includes.py b/lib/skinutils/includes.py new file mode 100644 index 0000000..6a70dae --- /dev/null +++ b/lib/skinutils/includes.py @@ -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() diff --git a/resources/language/English/strings.po b/resources/language/English/strings.po new file mode 100644 index 0000000..47f41c3 --- /dev/null +++ b/resources/language/English/strings.po @@ -0,0 +1,260 @@ +# XBMC Media Center language file +# Addon Name: IPTV Library +# Addon id: script.module.iptvlib +# Addon Provider: Dmitry Vinogradov +msgid "" +msgstr "" + +# Empty string +msgctxt "#30000" +msgid " " +msgstr " " + +# Dialogs & Error messages +msgctxt "#30101" +msgid "Subscription required" +msgstr "Subscription required" + +msgctxt "#30102" +msgid "Set your subscription credentials for IPTV provider" +msgstr "Set your subscription credentials for IPTV provider" + +msgctxt "#30103" +msgid "Authentication failed" +msgstr "Authentication failed" + +msgctxt "#30104" +msgid "You should check your addon settings" +msgstr "You should check your addon settings" + +msgctxt "#30105" +msgid "Selected program is not playable" +msgstr "Selected program is not playable" + +msgctxt "#30106" +msgid "Service error occurred" +msgstr "Service error occurred" + +msgctxt "#30107" +msgid "Are you sure you want to exit application?" +msgstr "Are you sure you want to exit application?" + +msgctxt "#30108" +msgid "Archive is not available yet." +msgstr "Archive is not available yet." + +msgctxt "#30109" +msgid "Do you want to jump to the next available position?" +msgstr "Do you want to jump to the next available position?" + +msgctxt "#30110" +msgid "Archive on this channel is not available" +msgstr "Archive on this channel is not available" + +msgctxt "#30111" +msgid "Unable to skip forward on live TV" +msgstr "Unable to skip forward on live TV" + +msgctxt "#30112" +msgid "Your playback will be stopped due to inactivity" +msgstr "Your playback will be stopped due to inactivity" + +msgctxt "#30113" +msgid "in %s seconds" +msgstr "in %s seconds" + +msgctxt "#30114" +msgid "HTTP request failed" +msgstr "HTTP request failed" + +msgctxt "#30115" +msgid "Please restart KODI to complete add-on installation" +msgstr "Please restart KODI to complete add-on installation" + +msgctxt "#30116" +msgid "This add-on needs to install some extra resources." +msgstr "This add-on needs to install some extra resources. This installation requires a manual KODI restart." + +msgctxt "#30117" +msgid "Please check your internet connection" +msgstr "Please check your internet connection" + +msgctxt "#30118" +msgid "Unexpected response from service provider" +msgstr "Unexpected response from service provider" + +msgctxt "#30119" +msgid "Unexpected error occurred" +msgstr "Unexpected error occurred" + +# Application's strings +msgctxt "#30201" +msgid "No information available" +msgstr "No information available" + +msgctxt "#30202" +msgid "min." +msgstr "min." + +msgctxt "#30203" +msgid "sec." +msgstr "sec." + +# Date formatting strings + +# Weekday as full name +msgctxt "#30300" +msgid "Sunday" +msgstr "Sunday" + +msgctxt "#30301" +msgid "Monday" +msgstr "Monday" + +msgctxt "#30302" +msgid "Tuesday" +msgstr "Tuesday" + +msgctxt "#30303" +msgid "Wednesday" +msgstr "Wednesday" + +msgctxt "#30304" +msgid "Thursday" +msgstr "Thursday" + +msgctxt "#30305" +msgid "Friday" +msgstr "Friday" + +msgctxt "#30306" +msgid "Saturday" +msgstr "Saturday" + +# Weekday as abbreviated name +msgctxt "#30310" +msgid "Sun" +msgstr "Sun" + +msgctxt "#30311" +msgid "Mon" +msgstr "Mon" + +msgctxt "#30312" +msgid "Tue" +msgstr "Tue" + +msgctxt "#30313" +msgid "Wed" +msgstr "Wed" + +msgctxt "#30314" +msgid "Thu" +msgstr "Thu" + +msgctxt "#30315" +msgid "Fri" +msgstr "Fri" + +msgctxt "#30316" +msgid "Sat" +msgstr "Sat" + +# Month as full name +msgctxt "#30401" +msgid "January" +msgstr "January" + +msgctxt "#30402" +msgid "February" +msgstr "February" + +msgctxt "#30403" +msgid "March" +msgstr "March" + +msgctxt "#30404" +msgid "April" +msgstr "April" + +msgctxt "#30405" +msgid "May" +msgstr "May" + +msgctxt "#30406" +msgid "June" +msgstr "June" + +msgctxt "#30407" +msgid "July" +msgstr "July" + +msgctxt "#30408" +msgid "August" +msgstr "August" + +msgctxt "#30409" +msgid "September" +msgstr "September" + +msgctxt "#30410" +msgid "October" +msgstr "October" + +msgctxt "#30411" +msgid "November" +msgstr "November" + +msgctxt "#30412" +msgid "December" +msgstr "December" + +# Month as abbreviated name +msgctxt "#30501" +msgid "Jan" +msgstr "Jan" + +msgctxt "#30502" +msgid "Feb" +msgstr "Feb" + +msgctxt "#30503" +msgid "Mar" +msgstr "Mar" + +msgctxt "#30504" +msgid "Apr" +msgstr "Apr" + +msgctxt "#30505" +msgid "May" +msgstr "May" + +msgctxt "#30506" +msgid "Jun" +msgstr "Jun" + +msgctxt "#30507" +msgid "Jul" +msgstr "Jul" + +msgctxt "#30508" +msgid "Aug" +msgstr "Aug" + +msgctxt "#30509" +msgid "Sep" +msgstr "Sep" + +msgctxt "#30510" +msgid "Oct" +msgstr "Oct" + +msgctxt "#30511" +msgid "Nov" +msgstr "Nov" + +msgctxt "#30512" +msgid "Dec" +msgstr "Dec" + diff --git a/resources/language/German/strings.po b/resources/language/German/strings.po new file mode 100644 index 0000000..34511d3 --- /dev/null +++ b/resources/language/German/strings.po @@ -0,0 +1,259 @@ +# XBMC Media Center language file +# Addon Name: IPTV Library +# Addon id: script.module.iptvlib +# Addon Provider: Dmitry Vinogradov +msgid "" +msgstr "" + +# Empty string +msgctxt "#30000" +msgid " " +msgstr " " + +# Dialogs & Error messages +msgctxt "#30101" +msgid "Subscription required" +msgstr "Abonnement erforderlich" + +msgctxt "#30102" +msgid "Set your subscription credentials for IPTV provider" +msgstr "Geben Sie Ihre Zugangsdaten für Ihren IPTV Provider" + +msgctxt "#30103" +msgid "Authentication failed" +msgstr "Anmeldeversuch fehlgeschlagen" + +msgctxt "#30104" +msgid "You should check your addon settings" +msgstr "Bitte überprüfen Sie Ihre Addon-Einstellungen" + +msgctxt "#30105" +msgid "Selected program is not playable" +msgstr "Ausgewählte Programm kann nicht abgespielt werden" + +msgctxt "#30106" +msgid "Service error occurred" +msgstr "Service Fehler ist aufgetreten" + +msgctxt "#30107" +msgid "Are you sure you want to exit application?" +msgstr "Sind Sie sicher, dass Sie das Programm beenden möchten?" + +msgctxt "#30108" +msgid "Archive is not available yet." +msgstr "Archiv ist noch nicht verfügbar." + +msgctxt "#30109" +msgid "Do you want to jump to the next available position?" +msgstr "Soll der nächstmögliche Archiv-abschnitt angezeigt werden?" + +msgctxt "#30110" +msgid "Archive on this channel is not available" +msgstr "Archiv für diesen Kanal ist nicht verfügbar" + +msgctxt "#30111" +msgid "Unable to skip forward on live TV" +msgstr "Vorspulen innerhalb der Live-Sendungen ist nicht möglich" + +msgctxt "#30112" +msgid "Your playback will be stopped due to inactivity" +msgstr "Aufgrund deiner Inaktivität wird die wiedergabe" + +msgctxt "#30113" +msgid "in %s seconds" +msgstr "in %s Sekunden beendet" + +msgctxt "#30114" +msgid "HTTP request failed" +msgstr "HTTP Anfrage ist fehlgeschlagen" + +msgctxt "#30115" +msgid "Please restart KODI to complete add-on installation" +msgstr "Bitte starten Sie KODI neu um die Installation zu beenden" + +msgctxt "#30116" +msgid "This add-on needs to install some extra resources." +msgstr "Dieses Addon muss einige zusätzliche Ressourcen installieren. Diese Installation erfordert einen manuellen KODI-Neustart." + +msgctxt "#30117" +msgid "Please check your internet connection" +msgstr "Bitte prüfen Sie Ihre Internetverbindung" + +msgctxt "#30118" +msgid "Unexpected response from service provider" +msgstr "Unerwartete Antwort vom Service Anbieter" + +msgctxt "#30119" +msgid "Unexpected error occurred" +msgstr "Ein unerwarteter Fehler ist aufgetreten" + +# Application's strings +msgctxt "#30201" +msgid "No information available" +msgstr "Keine Information verfügbar" + +msgctxt "#30202" +msgid "min." +msgstr "min." + +msgctxt "#30203" +msgid "sec." +msgstr "sek." + +# Date formatting strings + +# Weekday as full name +msgctxt "#30300" +msgid "Sunday" +msgstr "Sonntag" + +msgctxt "#30301" +msgid "Monday" +msgstr "Montag" + +msgctxt "#30302" +msgid "Tuesday" +msgstr "Dienstag" + +msgctxt "#30303" +msgid "Wednesday" +msgstr "Mittwoch" + +msgctxt "#30304" +msgid "Thursday" +msgstr "Donnerstag" + +msgctxt "#30305" +msgid "Friday" +msgstr "Freitag" + +msgctxt "#30306" +msgid "Saturday" +msgstr "Samstag" + +# Weekday as abbreviated name +msgctxt "#30310" +msgid "Sun" +msgstr "So" + +msgctxt "#30311" +msgid "Mon" +msgstr "Mo" + +msgctxt "#30312" +msgid "Tue" +msgstr "Di" + +msgctxt "#30313" +msgid "Wed" +msgstr "Mi" + +msgctxt "#30314" +msgid "Thu" +msgstr "Do" + +msgctxt "#30315" +msgid "Fri" +msgstr "Fr" + +msgctxt "#30316" +msgid "Sat" +msgstr "Sa" + +# Month as full name +msgctxt "#30401" +msgid "January" +msgstr "Januar" + +msgctxt "#30402" +msgid "February" +msgstr "Februar" + +msgctxt "#30403" +msgid "March" +msgstr "März" + +msgctxt "#30404" +msgid "April" +msgstr "April" + +msgctxt "#30405" +msgid "May" +msgstr "Mai" + +msgctxt "#30406" +msgid "June" +msgstr "Juni" + +msgctxt "#30407" +msgid "July" +msgstr "Juli" + +msgctxt "#30408" +msgid "August" +msgstr "August" + +msgctxt "#30409" +msgid "September" +msgstr "September" + +msgctxt "#30410" +msgid "October" +msgstr "Oktober" + +msgctxt "#30411" +msgid "November" +msgstr "November" + +msgctxt "#30412" +msgid "December" +msgstr "Dezember" + +# Month as abbreviated name +msgctxt "#30501" +msgid "Jan" +msgstr "Jan" + +msgctxt "#30502" +msgid "Feb" +msgstr "Feb" + +msgctxt "#30503" +msgid "Mar" +msgstr "Mär" + +msgctxt "#30504" +msgid "Apr" +msgstr "Apr" + +msgctxt "#30505" +msgid "May" +msgstr "Mai" + +msgctxt "#30506" +msgid "Jun" +msgstr "Jun" + +msgctxt "#30507" +msgid "Jul" +msgstr "Jul" + +msgctxt "#30508" +msgid "Aug" +msgstr "Aug" + +msgctxt "#30509" +msgid "Sep" +msgstr "Sept" + +msgctxt "#30510" +msgid "Oct" +msgstr "Okt" + +msgctxt "#30511" +msgid "Nov" +msgstr "Nov" + +msgctxt "#30512" +msgid "Dec" +msgstr "Dez" diff --git a/resources/language/Russian/strings.po b/resources/language/Russian/strings.po new file mode 100644 index 0000000..59f7bfa --- /dev/null +++ b/resources/language/Russian/strings.po @@ -0,0 +1,259 @@ +# XBMC Media Center language file +# Addon Name: IPTV Library +# Addon id: script.module.iptvlib +# Addon Provider: Dmitry Vinogradov +msgid "" +msgstr "" + +# Empty string +msgctxt "#30000" +msgid " " +msgstr " " + +# Dialogs & Error messages +msgctxt "#30101" +msgid "Subscription required" +msgstr "Требуется подписка" + +msgctxt "#30102" +msgid "Set your subscription credentials for IPTV provider" +msgstr "Введите данные о подписке для сервиса IPTV" + +msgctxt "#30103" +msgid "Authentication failed" +msgstr "Ошибка при авторизации" + +msgctxt "#30104" +msgid "You should check your addon settings" +msgstr "Проверьте настройки проложения" + +msgctxt "#30105" +msgid "Selected program is not playable" +msgstr "Выбранная программа не воспроизводится" + +msgctxt "#30106" +msgid "Service error occurred" +msgstr "Ошибка на стороне провайдера" + +msgctxt "#30107" +msgid "Are you sure you want to exit application?" +msgstr "Вы действительно желаете закрыть проложение?" + +msgctxt "#30108" +msgid "Archive is not available yet." +msgstr "Архив пока не доступен." + +msgctxt "#30109" +msgid "Do you want to jump to the next available position?" +msgstr "Перейти к ближайшей позиции в архиве?" + +msgctxt "#30110" +msgid "Archive on this channel is not available" +msgstr "Архив для данного канала не доступен" + +msgctxt "#30111" +msgid "Unable to skip forward on live TV" +msgstr "Перемотка вперед при просмотре прямых трансляций не возможна" + +msgctxt "#30112" +msgid "Your playback will be stopped due to inactivity" +msgstr "В виду неактивности воспроизведение будет приостановлено" + +msgctxt "#30113" +msgid "in %s seconds" +msgstr "через %s секунд" + +msgctxt "#30114" +msgid "HTTP request failed" +msgstr "Ошибка при выполнении HTTP запроса" + +msgctxt "#30115" +msgid "Please restart KODI to complete add-on installation" +msgstr "Пожалуйста перезагрузите KODI для завершения установки дополнения" + +msgctxt "#30116" +msgid "This add-on needs to install some extra resources." +msgstr "Это дополнение требует установки дополнительных ресурсов. После этого потребуется перезагрузка KODI." + +msgctxt "#30117" +msgid "Please check your internet connection" +msgstr "Проверьте подключение к Интернету" + +msgctxt "#30118" +msgid "Unexpected response from service provider" +msgstr "Неожиданный ответ от IPTV провайдера" + +msgctxt "#30119" +msgid "Unexpected error occurred" +msgstr "Произошла непредвиденная ошибка" + +# Application's strings +msgctxt "#30201" +msgid "No information available" +msgstr "Информация не доступна" + +msgctxt "#30202" +msgid "min." +msgstr "мин." + +msgctxt "#30203" +msgid "sec." +msgstr "сек." + +# Date formatting strings + +# Weekday as full name +msgctxt "#30300" +msgid "Sunday" +msgstr "Воскресение" + +msgctxt "#30301" +msgid "Monday" +msgstr "Понедельник" + +msgctxt "#30302" +msgid "Tuesday" +msgstr "Вторник" + +msgctxt "#30303" +msgid "Wednesday" +msgstr "Среда" + +msgctxt "#30304" +msgid "Thursday" +msgstr "Четверг" + +msgctxt "#30305" +msgid "Friday" +msgstr "Пятница" + +msgctxt "#30306" +msgid "Saturday" +msgstr "Суббота" + +# Weekday as abbreviated name +msgctxt "#30310" +msgid "Sun" +msgstr "Вс" + +msgctxt "#30311" +msgid "Mon" +msgstr "Пн" + +msgctxt "#30312" +msgid "Tue" +msgstr "Вт" + +msgctxt "#30313" +msgid "Wed" +msgstr "Ср" + +msgctxt "#30314" +msgid "Thu" +msgstr "Чт" + +msgctxt "#30315" +msgid "Fri" +msgstr "Пт" + +msgctxt "#30316" +msgid "Sat" +msgstr "Сб" + +# Month as full name +msgctxt "#30401" +msgid "January" +msgstr "Январь" + +msgctxt "#30402" +msgid "February" +msgstr "Февраль" + +msgctxt "#30403" +msgid "March" +msgstr "Март" + +msgctxt "#30404" +msgid "April" +msgstr "Апрель" + +msgctxt "#30405" +msgid "May" +msgstr "Май" + +msgctxt "#30406" +msgid "June" +msgstr "Июнь" + +msgctxt "#30407" +msgid "July" +msgstr "Июль" + +msgctxt "#30408" +msgid "August" +msgstr "Август" + +msgctxt "#30409" +msgid "September" +msgstr "Сентябрь" + +msgctxt "#30410" +msgid "October" +msgstr "Октябрь" + +msgctxt "#30411" +msgid "November" +msgstr "Ноябрь" + +msgctxt "#30412" +msgid "December" +msgstr "Декабрь" + +# Month as abbreviated name +msgctxt "#30501" +msgid "Jan" +msgstr "Янв" + +msgctxt "#30502" +msgid "Feb" +msgstr "Фев" + +msgctxt "#30503" +msgid "Mar" +msgstr "Мар" + +msgctxt "#30504" +msgid "Apr" +msgstr "Апр" + +msgctxt "#30505" +msgid "May" +msgstr "Май" + +msgctxt "#30506" +msgid "Jun" +msgstr "Июн" + +msgctxt "#30507" +msgid "Jul" +msgstr "Июл" + +msgctxt "#30508" +msgid "Aug" +msgstr "Авг" + +msgctxt "#30509" +msgid "Sep" +msgstr "Сен" + +msgctxt "#30510" +msgid "Oct" +msgstr "Окт" + +msgctxt "#30511" +msgid "Nov" +msgstr "Ноя" + +msgctxt "#30512" +msgid "Dec" +msgstr "Дек" diff --git a/resources/skins/Default/720p/font.xml b/resources/skins/Default/720p/font.xml new file mode 100644 index 0000000..1eb6e30 --- /dev/null +++ b/resources/skins/Default/720p/font.xml @@ -0,0 +1,65 @@ + + + + + + script.module.iptvlib-font_MainMenu + script.module.iptvlib-NotoSans-Bold.ttf + 60 + + + script.module.iptvlib-font30_title + script.module.iptvlib-NotoSans-Bold.ttf + 30 + + + script.module.iptvlib-font30 + script.module.iptvlib-NotoSans-Regular.ttf + 30 + + + + + script.module.iptvlib-font14 + script.module.iptvlib-NotoSans-Regular.ttf + 33 + + + + script.module.iptvlib-font12 + script.module.iptvlib-NotoSans-Regular.ttf + 25 + + + + + + + script.module.iptvlib-font_MainMenu + arial.ttf + 52 + + + + script.module.iptvlib-font30_title + arial.ttf + 30 + + + + script.module.iptvlib-font30 + arial.ttf + 30 + + + script.module.iptvlib-font14 + arial.ttf + 30 + + + script.module.iptvlib-font12 + arial.ttf + 22 + + + diff --git a/resources/skins/Default/720p/main_window.xml b/resources/skins/Default/720p/main_window.xml new file mode 100644 index 0000000..5afa595 --- /dev/null +++ b/resources/skins/Default/720p/main_window.xml @@ -0,0 +1,46 @@ + + + + no + WindowOpen + WindowClose + + 1 + 0 + 0 + + + + + + Background + 0 + 0 + 1280 + 720 + black.png + 500 + + + + diff --git a/resources/skins/Default/720p/tv_dialog.xml b/resources/skins/Default/720p/tv_dialog.xml new file mode 100644 index 0000000..0050a3f --- /dev/null +++ b/resources/skins/Default/720p/tv_dialog.xml @@ -0,0 +1,848 @@ + + + + + SetProperty(channel_id,0,home) + ClearProperty(channel_id,home) + + + 1 + 0 + 0 + + + 999 + + + + + Dummy control used to set focus to on video start + -100 + -100 + 0 + 0 + center + center + font_MainMenu + black + white.png + white.png + - + 4003 + 4003 + 4003 + 4003 + + + Dummy image used for preload of icons + -100 + -100 + 1 + 1 + keep + + + + + OSD for current playing program + 0 + 0 + 720 + 1280 + Control.HasFocus(4003) + + + + + + + + + + Background + 0 + 0 + 1280 + 720 + fade.png + 300 + + + Skip time within playback + !String.StartsWith(Control.GetLabel(4005),0 ) + 640 + 400 + 50 + 350 + center + font_MainMenu + white + grey + + + Channel icon + 546 + 480 + 106 + 188 + keep + default_channel.png + FFFFFFFF + + + Program start time + 640 + 600 + 15 + 400 + + font14 + center + center + + + Program title + -100 + -100 + 0 + 0 + + + Program title + 150 + 630 + 15 + 980 + + font30_title + center + center + + + Program playtime + 10 + 670 + 15 + 110 + + font14 + center + center + + + Program playtime progress bar + 150 + 670 + 980 + 15 + white50.png + white50.png + + + Program playtime progress bar slider + 155 + 665 + 980 + 25 + slider_bar.png + slider_bar.png + slider_bar.png + 4200 + + + Program duration + 1150 + 670 + 15 + 110 + + font14 + center + center + + + -100 + -100 + 1 + 1 + 4200 + + + + + + Group containing channelgroups, channels and program lists + 0 + 0 + 720 + 1300 + VisibleChange + + Control.HasFocus(4100)|Control.HasFocus(4200)|Control.HasFocus(4300) + + + Background for whole group + 720 + 1280 + black.png + A6FFFFFF + + + Channelgroup list + + List background + 720 + 300 + listitem-bg.png + CC000000 + + + Conditional + + + Conditional + + + + Channelgroups list + 4200 + vertical + false + 6 + 200 + + + Colored list item background + 0 + 0 + 78 + 20 + $INFO[ListItem.Property(icon)] + + + Overlay over colored list item background (unfocused) + 0 + 0 + 78 + 20 + white50.png + FF000000 + + + List item background + 20 + 0 + 280 + 78 + listitem-bg.png + FF000000 + + + Conditional + + + Conditional + + + + Group name + 48 + 1 + 252 + 80 + font14 + center + white + left + + + + Conditional + + + Conditional + + + + + + Colored list item background for selected group without focus + 0 + 1 + 20 + 77 + $INFO[ListItem.Property(icon)] + !Control.HasFocus(4100) + VisibleChange + + + Colored list item background for selected group + 0 + 0 + 300 + 78 + $INFO[ListItem.Property(icon)] + + + Conditional + + + Conditional + + + + Group name + 48 + 1 + 252 + 80 + font14 + center + black + left + + + + Conditional + + + Conditional + + + + + + + Container for channel and program list + 20 + 0 + 720 + 500 + VisibleChange + Conditional + + Channel list + + Shadow + separator-y.png + -20 + 0 + 720 + 20 + Control.HasFocus(4200)|Control.HasFocus(4300) + + + List background + 720 + 500 + listitem-bg.png + CC000000 + + Conditional + + Conditional + + + + Channels list + 4100 + 4300 + vertical + false + 6 + 200 + + + List item background for channel icon + 0 + 0 + 78 + 150 + listitem-bg.png + FF000000 + + + List item background for text + 150 + 0 + 78 + 350 + listitem-bg.png + FF000000 + + Conditional + + Conditional + + + + Channel icon + 5 + 10 + 60 + 140 + keep + $INFO[ListItem.Property(icon)] + FFFFFFFF + + + Vertical bar (separator) + 150 + 18 + 44 + 2 + white.png + 33FFFFFF + + Conditional + + Conditional + + + + Channel name + 170 + 1 + 80 + 330 + font14 + center + white + left + + + Conditional + + Conditional + + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4100) + + + + + Icon background + 0 + 0 + 78 + 150 + listitem-bg-fo.png + Conditional + + + Text background + 150 + 0 + 78 + 350 + listitem-bg-fo.png + + Conditional + + Conditional + + Conditional + + + Icon + 5 + 10 + 60 + 140 + keep + $INFO[ListItem.Property(icon)] + FFFFFFFF + + + Vertical bar + 150 + 18 + 44 + 2 + black.png + 33FFFFFF + + Conditional + + Conditional + + + + Label + 170 + 1 + 80 + 330 + font14 + center + black + left + + Conditional + + Conditional + + Conditional + + + Label + 170 + 1 + 80 + 330 + font14 + center + white + left + + Conditional + + Conditional + + Conditional + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4100) + + + -100 + -100 + 0 + 0 + - + Skin.SetString(channel_id, 481) + + + + + + Program list + 150 + 0 + 720 + 500 + + Conditional + + + [Control.IsVisible(4200)|Control.IsVisible(4300)] + +String.IsEqual(Container(4200).ListItem.Property(cid), Container(4300).ListItem.Property(cid)) + + + + + + + + + Shadow + separator-y.png + -20 + 0 + 20 + 720 + Control.HasFocus(4300) + + + List background + 720 + 500 + listitem-bg.png + CC000000 + + + 4200 + 999 + vertical + false + 6 + 5 + 200 + + + List item background + 0 + 0 + 78 + 500 + listitem-bg.png + FF000000 + + + day color bar + 0 + 0 + 78 + 2 + $INFO[Listitem.Property(day_color)] + + + Program start time + 12 + 2 + 80 + 70 + font14 + top + white + left + ListItem.Property(t_start) + + + Program status icon (Archive, Past, Live) + 28 + 37 + 25 + 25 + $INFO[Listitem.Property(status)] + + + Program title + 85 + 2 + 76 + 290 + font14 + top + white + left + true + + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4200) + + + Program image + 10 + 90 + $INFO[ListItem.Property(img_s)] + keep + + + + + List item background + 0 + 0 + 78 + 500 + listitem-bg-fo.png + Conditional + + + day color bar + 0 + 0 + 78 + 2 + $INFO[Listitem.Property(day_color)] + Conditional + + + Selected Program start time (on focus) + 12 + 2 + 80 + 70 + font14 + top + black + left + ListItem.Property(t_start) + Conditional + + + Program status icon (Archive, Past, Live) + 28 + 37 + 25 + 25 + $INFO[Listitem.Property(status)] + + + Selected Program start time + 12 + 2 + 80 + 70 + font14 + top + white + left + ListItem.Property(t_start) + Conditional + + + Selected Program title on focus + 85 + 2 + 76 + 290 + font14 + top + black + left + true + + Conditional + + + Selected Program title + 85 + 2 + 76 + 290 + font14 + top + white + left + true + + Conditional + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4200) + + + Program image + 10 + 90 + $INFO[ListItem.Property(img_s)] + keep + + + + + Detailed information of the selected program + 500 + 0 + 720 + 780 + Control.HasFocus(4300) + VisibleChange + + List background + 720 + 780 + listitem-bg.png + CC000000 + + + Program status icon (Archive, Past, Live) + 30 + 55 + 25 + 25 + $INFO[Container(4300).ListItem.Property(status)] + + + Program start time and date + 28 + 55 + 30 + 500 + font12 + + + + Program Information + 30 + 100 + 215 + 500 + font14 + true + true + + + + Program image + 30 + 320 + 500 + $INFO[Container(4300).ListItem.Property(img_l)] + keep + + + + + + + + + + diff --git a/resources/skins/Default/720p/tv_dialog_font.xml b/resources/skins/Default/720p/tv_dialog_font.xml new file mode 100644 index 0000000..9349ab2 --- /dev/null +++ b/resources/skins/Default/720p/tv_dialog_font.xml @@ -0,0 +1,848 @@ + + + + + SetProperty(channel_id,0,home) + ClearProperty(channel_id,home) + + + 1 + 0 + 0 + + + 999 + + + + + Dummy control used to set focus to on video start + -100 + -100 + 0 + 0 + center + center + script.module.iptvlib-font_MainMenu + black + white.png + white.png + - + 4003 + 4003 + 4003 + 4003 + + + Dummy image used for preload of icons + -100 + -100 + 1 + 1 + keep + + + + + OSD for current playing program + 0 + 0 + 720 + 1280 + Control.HasFocus(4003) + + + + + + + + + + Background + 0 + 0 + 1280 + 720 + fade.png + 300 + + + Skip time within playback + !String.StartsWith(Control.GetLabel(4005),0 ) + 640 + 400 + 50 + 350 + center + script.module.iptvlib-font_MainMenu + white + grey + + + Channel icon + 546 + 480 + 106 + 188 + keep + default_channel.png + FFFFFFFF + + + Program start time + 640 + 600 + 15 + 400 + + script.module.iptvlib-font14 + center + center + + + Program title + -100 + -100 + 0 + 0 + + + Program title + 150 + 630 + 15 + 980 + + script.module.iptvlib-font30_title + center + center + + + Program playtime + 10 + 670 + 15 + 110 + + script.module.iptvlib-font14 + center + center + + + Program playtime progress bar + 150 + 670 + 980 + 15 + white50.png + white50.png + + + Program playtime progress bar slider + 155 + 665 + 980 + 25 + slider_bar.png + slider_bar.png + slider_bar.png + 4200 + + + Program duration + 1150 + 670 + 15 + 110 + + script.module.iptvlib-font14 + center + center + + + -100 + -100 + 1 + 1 + 4200 + + + + + + Group containing channelgroups, channels and program lists + 0 + 0 + 720 + 1300 + VisibleChange + + Control.HasFocus(4100)|Control.HasFocus(4200)|Control.HasFocus(4300) + + + Background for whole group + 720 + 1280 + black.png + A6FFFFFF + + + Channelgroup list + + List background + 720 + 300 + listitem-bg.png + CC000000 + + + Conditional + + + Conditional + + + + Channelgroups list + 4200 + vertical + false + 6 + 200 + + + Colored list item background + 0 + 0 + 78 + 20 + $INFO[ListItem.Property(icon)] + + + Overlay over colored list item background (unfocused) + 0 + 0 + 78 + 20 + white50.png + FF000000 + + + List item background + 20 + 0 + 280 + 78 + listitem-bg.png + FF000000 + + + Conditional + + + Conditional + + + + Group name + 48 + 1 + 252 + 80 + script.module.iptvlib-font14 + center + white + left + + + + Conditional + + + Conditional + + + + + + Colored list item background for selected group without focus + 0 + 1 + 20 + 77 + $INFO[ListItem.Property(icon)] + !Control.HasFocus(4100) + VisibleChange + + + Colored list item background for selected group + 0 + 0 + 300 + 78 + $INFO[ListItem.Property(icon)] + + + Conditional + + + Conditional + + + + Group name + 48 + 1 + 252 + 80 + script.module.iptvlib-font14 + center + black + left + + + + Conditional + + + Conditional + + + + + + + Container for channel and program list + 20 + 0 + 720 + 500 + VisibleChange + Conditional + + Channel list + + Shadow + separator-y.png + -20 + 0 + 720 + 20 + Control.HasFocus(4200)|Control.HasFocus(4300) + + + List background + 720 + 500 + listitem-bg.png + CC000000 + + Conditional + + Conditional + + + + Channels list + 4100 + 4300 + vertical + false + 6 + 200 + + + List item background for channel icon + 0 + 0 + 78 + 150 + listitem-bg.png + FF000000 + + + List item background for text + 150 + 0 + 78 + 350 + listitem-bg.png + FF000000 + + Conditional + + Conditional + + + + Channel icon + 5 + 10 + 60 + 140 + keep + $INFO[ListItem.Property(icon)] + FFFFFFFF + + + Vertical bar (separator) + 150 + 18 + 44 + 2 + white.png + 33FFFFFF + + Conditional + + Conditional + + + + Channel name + 170 + 1 + 80 + 330 + script.module.iptvlib-font14 + center + white + left + + + Conditional + + Conditional + + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4100) + + + + + Icon background + 0 + 0 + 78 + 150 + listitem-bg-fo.png + Conditional + + + Text background + 150 + 0 + 78 + 350 + listitem-bg-fo.png + + Conditional + + Conditional + + Conditional + + + Icon + 5 + 10 + 60 + 140 + keep + $INFO[ListItem.Property(icon)] + FFFFFFFF + + + Vertical bar + 150 + 18 + 44 + 2 + black.png + 33FFFFFF + + Conditional + + Conditional + + + + Label + 170 + 1 + 80 + 330 + script.module.iptvlib-font14 + center + black + left + + Conditional + + Conditional + + Conditional + + + Label + 170 + 1 + 80 + 330 + script.module.iptvlib-font14 + center + white + left + + Conditional + + Conditional + + Conditional + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4100) + + + -100 + -100 + 0 + 0 + - + Skin.SetString(channel_id, 481) + + + + + + Program list + 150 + 0 + 720 + 500 + + Conditional + + + [Control.IsVisible(4200)|Control.IsVisible(4300)] + +String.IsEqual(Container(4200).ListItem.Property(cid), Container(4300).ListItem.Property(cid)) + + + + + + + + + Shadow + separator-y.png + -20 + 0 + 20 + 720 + Control.HasFocus(4300) + + + List background + 720 + 500 + listitem-bg.png + CC000000 + + + 4200 + 999 + vertical + false + 6 + 5 + 200 + + + List item background + 0 + 0 + 78 + 500 + listitem-bg.png + FF000000 + + + day color bar + 0 + 0 + 78 + 2 + $INFO[Listitem.Property(day_color)] + + + Program start time + 12 + 2 + 80 + 70 + script.module.iptvlib-font14 + top + white + left + ListItem.Property(t_start) + + + Program status icon (Archive, Past, Live) + 28 + 37 + 25 + 25 + $INFO[Listitem.Property(status)] + + + Program title + 85 + 2 + 76 + 290 + script.module.iptvlib-font14 + top + white + left + true + + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4200) + + + Program image + 10 + 90 + $INFO[ListItem.Property(img_s)] + keep + + + + + List item background + 0 + 0 + 78 + 500 + listitem-bg-fo.png + Conditional + + + day color bar + 0 + 0 + 78 + 2 + $INFO[Listitem.Property(day_color)] + Conditional + + + Selected Program start time (on focus) + 12 + 2 + 80 + 70 + script.module.iptvlib-font14 + top + black + left + ListItem.Property(t_start) + Conditional + + + Program status icon (Archive, Past, Live) + 28 + 37 + 25 + 25 + $INFO[Listitem.Property(status)] + + + Selected Program start time + 12 + 2 + 80 + 70 + script.module.iptvlib-font14 + top + white + left + ListItem.Property(t_start) + Conditional + + + Selected Program title on focus + 85 + 2 + 76 + 290 + script.module.iptvlib-font14 + top + black + left + true + + Conditional + + + Selected Program title + 85 + 2 + 76 + 290 + script.module.iptvlib-font14 + top + white + left + true + + Conditional + + + Shadow + separator-y.png + 0 + 0 + 80 + 20 + Control.HasFocus(4200) + + + Program image + 10 + 90 + $INFO[ListItem.Property(img_s)] + keep + + + + + Detailed information of the selected program + 500 + 0 + 720 + 780 + Control.HasFocus(4300) + VisibleChange + + List background + 720 + 780 + listitem-bg.png + CC000000 + + + Program status icon (Archive, Past, Live) + 30 + 55 + 25 + 25 + $INFO[Container(4300).ListItem.Property(status)] + + + Program start time and date + 28 + 55 + 30 + 500 + script.module.iptvlib-font12 + + + + Program Information + 30 + 100 + 215 + 500 + script.module.iptvlib-font14 + true + true + + + + Program image + 30 + 320 + 500 + $INFO[Container(4300).ListItem.Property(img_l)] + keep + + + + + + + + + + diff --git a/resources/skins/Default/fonts/noto_license.txt b/resources/skins/Default/fonts/noto_license.txt new file mode 100644 index 0000000..f00e6a2 --- /dev/null +++ b/resources/skins/Default/fonts/noto_license.txt @@ -0,0 +1,48 @@ +Copyright (c) 2010-2014, Łukasz Dziedzic (dziedzic@typoland.com), +with Reserved Font Name Lato. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/resources/skins/Default/fonts/script.module.iptvlib-NotoSans-Bold.ttf b/resources/skins/Default/fonts/script.module.iptvlib-NotoSans-Bold.ttf new file mode 100644 index 0000000..21dddde Binary files /dev/null and b/resources/skins/Default/fonts/script.module.iptvlib-NotoSans-Bold.ttf differ diff --git a/resources/skins/Default/fonts/script.module.iptvlib-NotoSans-Regular.ttf b/resources/skins/Default/fonts/script.module.iptvlib-NotoSans-Regular.ttf new file mode 100644 index 0000000..04be6f5 Binary files /dev/null and b/resources/skins/Default/fonts/script.module.iptvlib-NotoSans-Regular.ttf differ diff --git a/resources/skins/Default/media/1.png b/resources/skins/Default/media/1.png new file mode 100755 index 0000000..c256d67 Binary files /dev/null and b/resources/skins/Default/media/1.png differ diff --git a/resources/skins/Default/media/10.png b/resources/skins/Default/media/10.png new file mode 100755 index 0000000..dab37c9 Binary files /dev/null and b/resources/skins/Default/media/10.png differ diff --git a/resources/skins/Default/media/11.png b/resources/skins/Default/media/11.png new file mode 100755 index 0000000..c256d67 Binary files /dev/null and b/resources/skins/Default/media/11.png differ diff --git a/resources/skins/Default/media/12.png b/resources/skins/Default/media/12.png new file mode 100755 index 0000000..cccf20e Binary files /dev/null and b/resources/skins/Default/media/12.png differ diff --git a/resources/skins/Default/media/13.png b/resources/skins/Default/media/13.png new file mode 100755 index 0000000..d0f49c7 Binary files /dev/null and b/resources/skins/Default/media/13.png differ diff --git a/resources/skins/Default/media/14.png b/resources/skins/Default/media/14.png new file mode 100755 index 0000000..51f57bc Binary files /dev/null and b/resources/skins/Default/media/14.png differ diff --git a/resources/skins/Default/media/15.png b/resources/skins/Default/media/15.png new file mode 100755 index 0000000..b7f3ce6 Binary files /dev/null and b/resources/skins/Default/media/15.png differ diff --git a/resources/skins/Default/media/16.png b/resources/skins/Default/media/16.png new file mode 100755 index 0000000..03a3009 Binary files /dev/null and b/resources/skins/Default/media/16.png differ diff --git a/resources/skins/Default/media/17.png b/resources/skins/Default/media/17.png new file mode 100755 index 0000000..cccf20e Binary files /dev/null and b/resources/skins/Default/media/17.png differ diff --git a/resources/skins/Default/media/18.png b/resources/skins/Default/media/18.png new file mode 100755 index 0000000..8b7f748 Binary files /dev/null and b/resources/skins/Default/media/18.png differ diff --git a/resources/skins/Default/media/19.png b/resources/skins/Default/media/19.png new file mode 100755 index 0000000..4109f83 Binary files /dev/null and b/resources/skins/Default/media/19.png differ diff --git a/resources/skins/Default/media/2.png b/resources/skins/Default/media/2.png new file mode 100755 index 0000000..cccf20e Binary files /dev/null and b/resources/skins/Default/media/2.png differ diff --git a/resources/skins/Default/media/20.png b/resources/skins/Default/media/20.png new file mode 100755 index 0000000..dab37c9 Binary files /dev/null and b/resources/skins/Default/media/20.png differ diff --git a/resources/skins/Default/media/3.png b/resources/skins/Default/media/3.png new file mode 100755 index 0000000..d0f49c7 Binary files /dev/null and b/resources/skins/Default/media/3.png differ diff --git a/resources/skins/Default/media/4.png b/resources/skins/Default/media/4.png new file mode 100755 index 0000000..51f57bc Binary files /dev/null and b/resources/skins/Default/media/4.png differ diff --git a/resources/skins/Default/media/5.png b/resources/skins/Default/media/5.png new file mode 100755 index 0000000..b7f3ce6 Binary files /dev/null and b/resources/skins/Default/media/5.png differ diff --git a/resources/skins/Default/media/6.png b/resources/skins/Default/media/6.png new file mode 100755 index 0000000..03a3009 Binary files /dev/null and b/resources/skins/Default/media/6.png differ diff --git a/resources/skins/Default/media/7.png b/resources/skins/Default/media/7.png new file mode 100755 index 0000000..cccf20e Binary files /dev/null and b/resources/skins/Default/media/7.png differ diff --git a/resources/skins/Default/media/8.png b/resources/skins/Default/media/8.png new file mode 100755 index 0000000..8b7f748 Binary files /dev/null and b/resources/skins/Default/media/8.png differ diff --git a/resources/skins/Default/media/9.png b/resources/skins/Default/media/9.png new file mode 100755 index 0000000..4109f83 Binary files /dev/null and b/resources/skins/Default/media/9.png differ diff --git a/resources/skins/Default/media/black.png b/resources/skins/Default/media/black.png new file mode 100755 index 0000000..493f7f8 Binary files /dev/null and b/resources/skins/Default/media/black.png differ diff --git a/resources/skins/Default/media/default_channel.png b/resources/skins/Default/media/default_channel.png new file mode 100755 index 0000000..5d7b726 Binary files /dev/null and b/resources/skins/Default/media/default_channel.png differ diff --git a/resources/skins/Default/media/fade.png b/resources/skins/Default/media/fade.png new file mode 100755 index 0000000..4978e12 Binary files /dev/null and b/resources/skins/Default/media/fade.png differ diff --git a/resources/skins/Default/media/icon.png b/resources/skins/Default/media/icon.png new file mode 100755 index 0000000..4d3f693 Binary files /dev/null and b/resources/skins/Default/media/icon.png differ diff --git a/resources/skins/Default/media/listitem-bg-fo.png b/resources/skins/Default/media/listitem-bg-fo.png new file mode 100755 index 0000000..94a4d3b Binary files /dev/null and b/resources/skins/Default/media/listitem-bg-fo.png differ diff --git a/resources/skins/Default/media/listitem-bg.png b/resources/skins/Default/media/listitem-bg.png new file mode 100755 index 0000000..e91d139 Binary files /dev/null and b/resources/skins/Default/media/listitem-bg.png differ diff --git a/resources/skins/Default/media/prg_status_archive.png b/resources/skins/Default/media/prg_status_archive.png new file mode 100755 index 0000000..f2f7691 Binary files /dev/null and b/resources/skins/Default/media/prg_status_archive.png differ diff --git a/resources/skins/Default/media/prg_status_live.png b/resources/skins/Default/media/prg_status_live.png new file mode 100755 index 0000000..7d43b9c Binary files /dev/null and b/resources/skins/Default/media/prg_status_live.png differ diff --git a/resources/skins/Default/media/prg_status_past.png b/resources/skins/Default/media/prg_status_past.png new file mode 100755 index 0000000..ff5c1c1 Binary files /dev/null and b/resources/skins/Default/media/prg_status_past.png differ diff --git a/resources/skins/Default/media/separator-y.png b/resources/skins/Default/media/separator-y.png new file mode 100755 index 0000000..6c5b54b Binary files /dev/null and b/resources/skins/Default/media/separator-y.png differ diff --git a/resources/skins/Default/media/slider_bar.png b/resources/skins/Default/media/slider_bar.png new file mode 100755 index 0000000..8619937 Binary files /dev/null and b/resources/skins/Default/media/slider_bar.png differ diff --git a/resources/skins/Default/media/white.png b/resources/skins/Default/media/white.png new file mode 100755 index 0000000..a2fa69d Binary files /dev/null and b/resources/skins/Default/media/white.png differ diff --git a/resources/skins/Default/media/white50.png b/resources/skins/Default/media/white50.png new file mode 100755 index 0000000..6794744 Binary files /dev/null and b/resources/skins/Default/media/white50.png differ