версия для Kodi 19

master v1.2.0
Роман Бородин 2022-03-14 20:34:22 +03:00
parent 9aefb9451b
commit 5d2aff9bc5
60 changed files with 5831 additions and 0 deletions

165
LICENSE.txt 100644
View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.

20
__init__.py 100644
View File

@ -0,0 +1,20 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#

61
addon.xml 100644
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon id="script.module.iptvlib"
name="IPTV Library"
version="1.2.0"
provider-name="Dmitry Vinogradov">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
</requires>
<extension point="xbmc.python.module" library="lib"/>
<extension point="xbmc.addon.metadata">
<assets>
<icon>resources/skins/Default/media/icon.png</icon>
<fanart></fanart>
<screenshot></screenshot>
</assets>
<summary lang="en">Common library for IPTV Addons</summary>
<description lang="en">Common library for IPTV Addons</description>
<platform>all</platform>
<license>GNU LESSER GENERAL PUBLIC LICENSE Version 3, June 2007</license>
<source>https://github.com/kodi-iptv-addons</source>
<news>
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
</news>
</extension>
</addon>

37
changelog.txt 100644
View File

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

20
lib/__init__.py 100644
View File

@ -0,0 +1,20 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#

View File

@ -0,0 +1,284 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import builtins
import calendar
import datetime
import math
import os
import platform
import sys
import threading
import time
from functools import wraps
import xbmc
import xbmcaddon
import importlib
if hasattr(builtins, 'addon_id') is False:
setattr(builtins, 'addon_id', os.path.basename(os.path.abspath(os.path.dirname(__file__))))
addon = xbmcaddon.Addon(id=getattr(builtins, 'addon_id'))
utc_local_offset = math.ceil(calendar.timegm(time.localtime()) - time.time())
importlib.reload(sys)
# noinspection PyUnresolvedReferences
sys.setdefaultencoding('utf-8')
TENSECS = 10 # type: int
MIN = 60 # type: int
HALFHOUR = 1800 # type: int
HOUR = 3600 # type: int
DAY = 86400 # type: int
TWODAYS = 172800 # type: int
TREEDAYS = 259200 # type: int
WEEK = 604800 # type: int
TWOWEEKS = 1209600 # type: int
TEXT_SUBSCRIPTION_REQUIRED_ID = 30101 # type: int
TEXT_SET_CREDENTIALS_ID = 30102 # type: int
TEXT_AUTHENTICATION_FAILED_ID = 30103 # type: int
TEXT_CHECK_SETTINGS_ID = 30104 # type: int
TEXT_NOT_PLAYABLE_ID = 30105 # type: int
TEXT_SERVICE_ERROR_OCCURRED_ID = 30106 # type: int
TEXT_SURE_TO_EXIT_ID = 30107 # type: int
TEXT_ARCHIVE_NOT_AVAILABLE_YET_ID = 30108 # type: int
TEXT_JUMP_TO_ARCHIVE_ID = 30109 # type: int
TEXT_CHANNEL_HAS_NO_ARCHIVE_ID = 30110 # type: int
TEXT_LIVE_NO_FORWARD_SKIP_ID = 30111 # type: int
TEXT_IDLE_DIALOG_ID = 30112 # type: int
TEXT_IDLE_DIALOG_COUNTDOWN_ID = 30113 # type: int
TEXT_HTTP_REQUEST_ERROR_ID = 30114 # type: int
TEXT_PLEASE_RESTART_KODI_ID = 30115 # type: int
TEXT_INSTALL_EXTRA_RESOURCES_ID = 30116 # type: int
TEXT_PLEASE_CHECK_INTERNET_CONNECTION_ID = 30117 # type: int
TEXT_UNEXPECTED_RESPONSE_FROM_SERVICE_PROVIDER_ID = 30118 # type: int
TEXT_UNEXPECTED_ERROR_OCCURRED_ID = 30119 # type: int
TEXT_NO_INFO_AVAILABLE_ID = 30201 # type: int
TEXT_ABBR_MINUTES_ID = 30202 # type: int
TEXT_ABBR_SECONDS_ID = 30203 # type: int
TEXT_WEEKDAY_FULL_ID_PREFIX = 3030 # type: int
TEXT_WEEKDAY_ABBR_ID_PREFIX = 3031 # type: int
TEXT_MONTH_FULL_ID_PREFIX = 304 # type: int
TEXT_MONTH_ABBR_ID_PREFIX = 305 # type: int
# noinspection PyUnresolvedReferences
class WindowMixin(object):
is_closing = False # type: bool
def __init__(self, **kwargs):
self.is_closing = False
super(WindowMixin, self).__init__()
def close(self):
self.is_closing = True
super(WindowMixin, self).close()
def show_control(self, *control_ids):
for control_id in control_ids:
control = self.getControl(control_id)
if control:
control.setVisible(True)
def hide_control(self, *control_ids):
for control_id in control_ids:
control = self.getControl(control_id)
if control:
control.setVisible(False)
def set_control_image(self, control_id, image):
control = self.getControl(control_id)
if control:
control.setImage(image)
def setcontrol_label(self, control_id, label):
control = self.getControl(control_id)
if control and label:
control.setLabel(label)
def set_control_text(self, control_id, text):
control = self.getControl(control_id)
if control:
control.setText(text)
def get_string(id_):
return xbmcaddon.Addon(os.path.basename(os.path.abspath(os.path.dirname(__file__) + "/../../"))).getLocalizedString(id_)
def show_small_popup(title='', msg='', delay=5000, image=''):
xbmc.executebuiltin('XBMC.Notification("%s","%s",%d,"%s")' % (title, msg, delay, image))
def build_user_agent():
# type: () -> str
return 'KODI/%s (%s; %s %s; python %s) %s/%s ' % (
xbmc.getInfoLabel('System.BuildVersion').split(" ")[0],
xbmc.getInfoLabel('System.BuildVersion'),
platform.system(),
platform.release(),
platform.python_version(),
addon.getAddonInfo('id').replace('-DEV', ''),
addon.getAddonInfo('version')
)
def unique(s, t):
# type: (str, str) -> str
t = (t * ((len(s) // len(t)) + 1))[:len(s)]
if isinstance(s, str):
return "".join(chr(ord(a) ^ ord(b)) for a, b in zip(s, t))
else:
return str([a ^ b for a, b in zip(s, t)])
def secs_to_percent(length, played):
# type: (int, float) -> float
return (100 * played) / length
def percent_to_secs(length, percent):
# type: (int, float) -> int
return int((length * percent) / 100)
def format_secs(secs, id="time"):
# type: (int, str) -> str
if id == "time":
return "{:0>8}".format(datetime.timedelta(seconds=secs))
if id == "skip":
prefix = "+"
if secs < 0:
prefix = "-"
secs *= -1
elif secs == 0:
prefix = ""
if secs > 60:
return "%s%s %s" % (prefix, secs / 60, get_string(TEXT_ABBR_MINUTES_ID))
return "%s%s %s" % (prefix, secs, get_string(TEXT_ABBR_SECONDS_ID))
def format_date(timestamp, id="dateshort", custom_format=None):
# type: (float, str, str) -> str
ids = {
"%A": (TEXT_WEEKDAY_FULL_ID_PREFIX, "%w"),
"%a": (TEXT_WEEKDAY_ABBR_ID_PREFIX, "%w"),
"%B": (TEXT_MONTH_FULL_ID_PREFIX, "%m"),
"%b": (TEXT_MONTH_ABBR_ID_PREFIX, "%m")
}
if timestamp:
if custom_format is not None:
dt = datetime.datetime.fromtimestamp(timestamp)
for k in ids.keys():
if k in custom_format:
v = get_string(int("%s%s" % (ids[k][0], dt.strftime(ids[k][1]))))
custom_format = custom_format.replace(k, v)
return dt.strftime(custom_format)
return datetime.datetime.fromtimestamp(timestamp).strftime(xbmc.getRegion(id))
return ''
def time_now():
# type: () -> float
return time.time()
def timestamp_to_midnight(timestamp):
# type: (float) -> int
# noinspection PyTypeChecker
return int(time.mktime(
datetime.datetime.combine(
datetime.datetime.fromtimestamp(timestamp),
datetime.datetime.min.time()
).timetuple()
))
def str_to_datetime(str_date, fmt):
# type: (str, str) -> datetime.datetime
try:
d = datetime.datetime.strptime(str_date, fmt)
except TypeError:
from time import strptime
d = datetime.datetime(*(strptime(str_date, fmt)[0:6]))
return d
def str_to_timestamp(str_date, fmt):
# type: (str, str) -> int
try:
return int(time.mktime(str_to_datetime(str_date, fmt).timetuple()))
except:
return 0
def run_async(func):
"""
Decorator to run a function in a separate thread
"""
@wraps(func)
def async_func(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.start()
return thread
return async_func
def log(msg, level=xbmc.LOGINFO):
if level == xbmc.LOGDEBUG:
import inspect
mod = inspect.getmodule(inspect.stack()[1][0])
calframe = inspect.getouterframes(inspect.currentframe(), 2)
msg = "------- [%s.%s] : %s" % (mod.__name__, calframe[1][3], msg)
xbmc.log('%s: %s' % (addon.getAddonInfo('name'), msg), level)
def normalize(text):
# type: (str) -> str
if type(text) is not str:
text = str(text)
symbols = ("абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ",
"abvgdeejzijklmnoprstufhzcss_y_euaABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUA")
tr = dict([(ord(a), ord(b)) for (a, b) in zip(*symbols)])
import re
regs = [
'\s+\+[0-9]+', # time shift suffix, e.g. "RTL +7"
'\s+\[[a-zA-Z]+\]', # land code suffix, e.g. "RTL [de]"
'\s+(HQ)', # High quality suffix, e.g. "RTL (HQ)"
]
for reg in regs:
text = re.sub(reg, '', text)
return re.sub('[^0-9a-zA-Z+-]+', '', text.translate(tr)).upper()
x = lambda s: s.decode("hex")
z = lambda s: str.encode(s, "hex")
h1 = '3d37612b5542244c4e3952775a3f6b5a24367732426e583750'
h2 = '5543155b26780b63394e25593d50043d48535a532c0f344e24' \
'54541205362d49632d563e1b3f5c1f65505f130f172f750e66' \
'0b03541f65710978684f6f467c4b563f52531946640b3b0a2b' \
'4011044a6839596a2b556f0c27190e2c194d0a1421073c0a2b' \
'40113e162e3f'

480
lib/iptvlib/api.py 100644
View File

@ -0,0 +1,480 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import abc
import json
import os
import ssl
import threading
import traceback
import urllib.error
import urllib.parse
import urllib.request
from collections import OrderedDict
from queue import Queue
from threading import Event
from urllib.parse import urlencode
from urllib.request import Request
import xbmc
from . import build_user_agent, log, DAY, time_now, format_date, normalize
from .models import Group, Program, Channel
class ApiException(Exception):
def __init__(self, message, code, origin_error=None):
self.message = message
self.code = code
self.origin_error = origin_error
def __repr__(self):
return "ApiException: (%s) %s" % (self.code, self.message)
class HttpRequest(urllib.request.Request):
ident = None # type: str
method = None # type: str
def __init__(self, ident=None, method=None, **kwargs):
self.ident = ident
self.method = method
urllib.request.Request.__init__(self, **kwargs)
def get_method(self):
return self.method or urllib.request.Request.get_method(self)
def __repr__(self):
return "%s(%r)" % (self.__class__, self.__dict__)
class Api(metaclass=abc.ABCMeta):
AUTH_STATUS_NONE = 0
AUTH_STATUS_OK = 1
AUTH_MAX_ATTEMPTS = 3
E_UNKNOW_ERROR = 1000
E_HTTP_REQUEST_FAILED = 1001
E_JSON_DECODE = 1002
E_AUTH_ERROR = 1003
E_API_ERROR = 1004
auth_status = AUTH_STATUS_NONE # type: int
username = None # type: str
password = None # type: str
sort_channels = None # type: bool
working_path = None # type: str
cookie_file = None # type: str
settings_file = None # type: str
_attempt = 0 # type: int
_groups = None # type: OrderedDict[str, Group]
_channels = None # type: OrderedDict[str, Channel]
_ident = None # type: str
_epg_map = None # type: dict
def __init__(self, username=None, password=None, working_path="./", sort_channels=False):
self.auth_status = self.AUTH_STATUS_NONE
self.username = self._ident = username
self.password = password
self.working_path = working_path
self.sort_channels = sort_channels
if not os.path.exists(self.working_path):
os.makedirs(self.working_path)
self.cookie_file = os.path.join(self.working_path, "%s.cookie.txt" % self.__class__.__name__)
self.settings_file = os.path.join(self.working_path, "%s.settings.txt" % self.__class__.__name__)
self._groups = OrderedDict()
self._channels = OrderedDict()
@property
def client_id(self):
return "%s:%s" % (self.__class__.__name__, self._ident)
@property
def user_agent(self):
# type: () -> str
"""
User agent of HTTP client.
:rtype: str
"""
return build_user_agent()
@property
def groups(self):
if len(self._groups) == 0:
self._groups = OrderedDict(sorted(iter(self.get_groups().items()), key=lambda item: item[1].name)) \
if self.sort_channels else self.get_groups()
self._channels = OrderedDict()
for group in list(self._groups.values()):
channels = OrderedDict(sorted(iter(group.channels.items()), key=lambda item: item[1].name)) \
if self.sort_channels else group.channels
self._channels.update(channels)
return self._groups
@property
def channels(self):
if len(self._channels) == 0:
len(self.groups)
return self._channels
@property
def base_api_url(self):
# type: () -> str
"""
Base URL of API.
:rtype: str
"""
return ""
@property
def base_icon_url(self):
# type: () -> str
"""
Base URL of channel icons.
:rtype: str
"""
return ""
@property
def host(self):
# type: () -> str
"""
Api hostname.
:rtype: str
"""
return ""
@property
def diff_live_archive(self):
# type: () -> int
"""
Difference between live stream and archive stream in seconds.
:rtype: int
"""
return -1
@property
def archive_ttl(self):
# type: () -> int
"""
Time to live (ttl) of archive streams in seconds.
:rtype: int
"""
return -1
@abc.abstractmethod
def login(self):
# type: () -> dict
"""Login to the IPTV service"""
pass
@abc.abstractmethod
def is_login_request(self, uri, payload=None, method=None, headers=None):
# type: (str, dict, str, dict) -> bool
"""
Checks whether given URI is used for login
:rtype: str
"""
pass
@abc.abstractmethod
def get_groups(self):
# type: () -> OrderedDict[str, Group]
"""
Returns the groups from the service
:rtype: dict[str, Group]
"""
pass
@abc.abstractmethod
def get_stream_url(self, cid, ut_start=None):
# type: (str, int) -> str
"""
Returns the stream URL for given channel id and timestamp
:rtype: str
"""
pass
def resolve_url(self, url):
# type: (str) -> str
request = self.prepare_request(url)
try:
response = urllib.request.urlopen(request, context=ssl.create_default_context())
return response.url
except urllib.error.HTTPError:
return url
@abc.abstractmethod
def get_epg(self, cid):
# type: (str) -> dict[int, Program]
"""
Returns the EPG for given channel id
:rtype: dict[int, Program]
"""
pass
@abc.abstractmethod
def get_cookie(self):
# type: () -> str
"""
Returns cookie
:rtype: str
"""
pass
def read_cookie_file(self):
# type: () -> str
"""
Returns cookie stored in the cookie file
:rtype: str
"""
cookie = ""
if os.path.isfile(self.cookie_file):
with open(self.cookie_file, 'r') as fh:
cookie = fh.read()
fh.close()
return cookie
def write_cookie_file(self, data):
# type: (str) -> None
"""
Stores cookie to the cookie file
:rtype: None
"""
with open(self.cookie_file, 'w') as fh:
fh.write(data)
fh.close()
def read_settings_file(self):
# type: () -> dict
"""
Returns settings stored in the settings file
:rtype: dict
"""
settings = None
if os.path.isfile(self.settings_file):
with open(self.settings_file, 'r') as fh:
json_string = fh.read()
fh.close()
settings = json.loads(json_string)
return settings
def write_settings_file(self, data):
# type: (dict) -> None
"""
Stores settings to the settings file
:rtype: None
"""
with open(self.settings_file, 'w') as fh:
fh.write(json.dumps(data))
fh.close()
def prepare_request(self, uri, payload=None, method="GET", headers=None, ident=None):
# type: (str, dict, str, dict, str) -> HttpRequest
url = self.base_api_url % uri if uri.startswith('http') is False else uri
headers = headers or {}
headers["User-Agent"] = self.user_agent
headers["Connection"] = "Close"
cookie = self.get_cookie()
if cookie != "" and not self.is_login_request(uri, payload):
headers["Cookie"] = cookie
data = None
if payload:
if method == "POST":
if "Content-Type" in headers and headers["Content-Type"] == "application/json":
data = json.dumps(payload)
else:
data = urlencode(payload)
elif method == "GET":
url += "%s%s" % ("&" if "?" in url else "?", urlencode(payload))
ident = ident or url
return HttpRequest(ident=ident, method=method, url=url, headers=headers, data=data)
def send_request(self, request):
# type: (Request) -> dict|str
json_data = ""
try:
log("request: %s" % request, xbmc.LOGDEBUG)
response = urllib.request.urlopen(request) # type: addinfourl
content_type = response.headers.getheader("content-type") # type: str
content = response.read()
if not content_type.startswith("application/json"):
return content
response = json.loads(content)
except urllib.error.URLError as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
response = {
"__error": {
"message": str(ex),
"code": self.E_HTTP_REQUEST_FAILED,
"details": {
"url": request.get_full_url(),
"data": request.data,
"headers": request.headers
},
}
}
pass
except ValueError as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
response = {
"__error": {
"message": "Unable decode server response: %s" % str(ex),
"code": self.E_JSON_DECODE,
"details": {
"response": json_data
}
}
}
pass
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
response = {
"__error": {
"message": "%s: %s" % (type(ex), str(ex)),
"code": self.E_UNKNOW_ERROR
}
}
pass
return response
def make_request(self, uri, payload=None, method="GET", headers=None):
# type: (str, dict, str, dict) -> dict
"""
Makes HTTP request to the IPTV API
:param uri: URL of the IPTV API
:param payload: Payload data
:param method: HTTP method
:param headers: Additional HTTP headers
:return:
"""
if self.auth_status != self.AUTH_STATUS_OK and not self.is_login_request(uri, payload, method, headers):
self.login()
return self.make_request(uri, payload, method, headers)
request = self.prepare_request(uri, payload, method, headers)
response = self.send_request(request)
if "__error" in response:
if self.is_login_request(uri, payload):
self.auth_status = self.AUTH_STATUS_NONE
try:
os.remove(self.cookie_file)
except OSError:
pass
return response
def do_send_request(self, queue, results, stop_event, wait=None):
# type: (Queue, dict[str, dict], Event, float) -> None
while not stop_event.is_set():
request = queue.get() # type: HttpRequest
results[request.ident] = self.send_request(request)
queue.task_done()
stop_event.wait(wait)
def send_parallel_requests(self, requests, wait=None, num_threads=None):
# type: (list[HttpRequest], float, int) -> dict[str, dict]
num_threads = len(requests) if num_threads is None else num_threads
queue = Queue(num_threads * 2)
results = dict()
stop_event = threading.Event()
for i in range(num_threads):
thread = threading.Thread(target=self.do_send_request, args=(queue, results, stop_event, wait,))
thread.setDaemon(True)
thread.start()
for req in requests:
queue.put(req)
queue.join()
while queue.unfinished_tasks > 0:
continue
stop_event.set()
return results
def get_epg_gh(self, channel):
# type: (Channel) -> OrderedDict[int, Program]
programs = OrderedDict()
if self._epg_map is None:
self._epg_map = self.make_request("https://kodi-iptv-addons.github.io/EPG/map.json?%s" % time_now())
norm = normalize(channel.name)
if (norm in self._epg_map) is False:
return programs
cid = self._epg_map.get(norm)
requests = []
days = (self.archive_ttl // DAY) + 5
start = int(time_now() - self.archive_ttl)
for i in range(days):
day = format_date(start + (i * DAY), custom_format="%Y-%m-%d")
request = self.prepare_request(
"https://kodi-iptv-addons.github.io/EPG/%s/%s.json" % (cid, day)
)
requests.append(request)
results = self.send_parallel_requests(requests)
epg = dict()
for key in sorted(results.keys()):
response = results[key]
is_error, error = Api.is_error_response(response)
if is_error:
log("error: %s" % error if is_error else response, xbmc.LOGDEBUG)
return programs
for k, v in response.items():
epg[int(k)] = v
prev = None # type: Program
for key in sorted(epg.keys()):
val = epg[key]
program = Program(
channel.cid,
channel.gid,
val["start"],
val["stop"],
val["title"],
val["descr"],
channel.archive,
val["image"]
)
if prev is not None:
program.prev_program = prev
prev.next_program = program
programs[program.ut_start] = prev = program
return programs
@staticmethod
def is_error_response(response):
# type: (dict) -> (bool, dict or None)
if "__error" in response:
return True, response["__error"]
return False, None

View File

@ -0,0 +1,86 @@
import os
import shutil
from datetime import datetime
from ..skinutils import DocumentCache, get_current_skin_path, get_local_skin_path, is_invalid_local_skin, \
copy_skin_to_userdata, do_write_test, reload_skin, get_skin_name
from ..skinutils.fonts import FontManager as SkinUtilsFontManager
class FontManagerException(Exception):
INSTALL_NEEDED = 1
RESTART_NEEDED = 2
INSTALL_NOT_NEEDED = 3
def __init__(self, message, *args):
self.message = message
super().__init__(*args)
class FontManager(object, SkinUtilsFontManager):
FONTS = {
"script.module.iptvlib-font_MainMenu": "script.module.iptvlib-NotoSans-Bold.ttf",
"script.module.iptvlib-font30_title": "script.module.iptvlib-NotoSans-Bold.ttf",
"script.module.iptvlib-font30": "script.module.iptvlib-NotoSans-Regular.ttf",
"script.module.iptvlib-font14": "script.module.iptvlib-NotoSans-Regular.ttf",
"script.module.iptvlib-font12": "script.module.iptvlib-NotoSans-Regular.ttf",
}
script_path = None
def __init__(self, script_path):
self.__installed_names = []
self.__installed_fonts = []
self.__doc_cache = DocumentCache()
self.script_path = script_path
def check_fonts(self):
if 'skin.estuary' == get_skin_name():
raise FontManagerException(FontManagerException.INSTALL_NOT_NEEDED)
if self.is_restart_needed():
raise FontManagerException(FontManagerException.RESTART_NEEDED)
if get_current_skin_path() != get_local_skin_path():
if not self.is_writable():
raise FontManagerException(FontManagerException.INSTALL_NOT_NEEDED)
raise FontManagerException(FontManagerException.INSTALL_NEEDED)
else:
if not os.path.isdir(get_local_skin_path()):
raise FontManagerException(FontManagerException.INSTALL_NEEDED)
for f in self._list_skin_font_files():
self.__doc_cache.add(f)
self.install_fonts()
def install_fonts(self):
xml_path = os.path.join(self.script_path, "resources", "skins", "Default", "720p", "font.xml")
font_dir = os.path.join(self.script_path, "resources", "skins", "Default", "fonts")
self.install_file(xml_path, font_dir)
reload_skin()
@staticmethod
def install_skin():
copy_skin_to_userdata(ask_user=False)
@staticmethod
def is_writable():
# type: () -> bool
skin_path = get_local_skin_path()
return not os.access(skin_path, os.W_OK) or not do_write_test(skin_path)
@staticmethod
def is_restart_needed():
# type: () -> bool
current_skin_path = get_current_skin_path()
local_skin_path = get_local_skin_path()
if os.path.isdir(local_skin_path) and current_skin_path != local_skin_path:
if is_invalid_local_skin():
time_suffix = datetime.now().strftime('%Y%m%d%H%M%S')
shutil.move(local_skin_path, local_skin_path + '-skinutils-' + time_suffix)
copy_skin_to_userdata(ask_user=False)
return True
return False
def __del__(self):
pass

View File

@ -0,0 +1,91 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import re
class M3u8Channel(object):
def __init__(self, name, group=None, url=None, id=None):
self.name = str(name, "utf-8") if type(name) is not str else name
self.group = str(group, "utf-8") if group and type(group) is not str else group
self.url = url
class M3u8Item(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __getitem__(self, item):
return self.__dict__.get(item, None)
def reg(pattern, line):
# type: (str, str) -> str or None
res = re.search(pattern, line)
return res.group(1) if res else None
class M3u8Parser(object):
EXTM3U = '#EXTM3U'
EXTINF = '#EXTINF'
EXTGRP = '#EXTGRP'
def __init__(self):
self.channel = None
pass
def parse(self, content, on_item):
# type: (str, callable(M3u8Item)) -> None
args = dict()
for line in content.splitlines():
line = line.strip()
if line.startswith(self.EXTM3U):
args = dict({
"id": self.EXTM3U,
"url-epg": reg('\s+url-epg\s*=\s*"([^"]+)"', line),
"url-logo": reg('\s+url-logo\s*=\s*"([^"]+)"', line)
})
on_item(M3u8Item(**args))
args = dict()
if line.startswith(self.EXTINF):
if len(args) > 0:
on_item(M3u8Item(**args))
options, name = line.replace(self.EXTINF + ':', '').split(',') # type: (str, str)
tvg_rec = reg('\s+tvg-rec\s*=\s*"([^"]+)"', line) or 0
args = dict({
"id": self.EXTINF,
"name": name,
"tvg-id": reg('\s+tvg-id\s*=\s*"([^"]+)"', line),
"tvg-logo": reg('\s+tvg-logo\s*=\s*"([^"]+)"', line),
"group-title": reg('\s+group-title\s*=\s*"([^"]+)"', line),
"tvg-rec": int(tvg_rec),
"adult": reg('\s+adult\s*=\s*"([^"]+)"', line),
})
if line.startswith(self.EXTGRP):
args["group-title"] = bytes(line.replace(self.EXTGRP + ':', '').strip(), "utf-8")
if line.startswith('http'):
args["url"] = line
if args["tvg-id"] is None:
for p in args['url'].split("/"):
if p.isdigit():
args["tvg-id"] = p
break
on_item(M3u8Item(**args))
args = dict()

View File

@ -0,0 +1,142 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
import xbmcgui
from . import *
from .api import Api, ApiException
from .fonts import FontManager, FontManagerException
from .tvdialog import TvDialog
class MainWindow(xbmcgui.WindowXML, WindowMixin):
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__) + '/../../')
check_settings_handler = None # type: callable()
api = None # type: Api
tv_dialog = None # type: TvDialog
tv_dialog_xml_file = "tv_dialog_font.xml"
fm = None # type: FontManager
_initialized = False # type: bool
def __init__(self, *args, **kwargs):
self.fm = FontManager(MainWindow.SCRIPT_PATH)
self.check_skin()
if "check_settings_handler" in kwargs:
self.check_settings_handler = kwargs.pop("check_settings_handler", None)
super(MainWindow, self).__init__(**kwargs)
def check_skin(self):
try:
self.fm.check_fonts()
except FontManagerException as ex:
if ex.message == FontManagerException.INSTALL_NEEDED:
xbmcgui.Dialog().ok(
addon.getAddonInfo("name") + " ",
get_string(TEXT_INSTALL_EXTRA_RESOURCES_ID)
)
self.fm.install_skin()
self.close()
sys.exit()
elif ex.message == FontManagerException.RESTART_NEEDED:
xbmcgui.Dialog().ok(
addon.getAddonInfo("name") + " ",
get_string(TEXT_PLEASE_RESTART_KODI_ID)
)
self.close()
sys.exit()
elif ex.message == FontManagerException.INSTALL_NOT_NEEDED:
self.tv_dialog_xml_file = "tv_dialog.xml"
@classmethod
def create(cls, check_settings_handler):
return cls("main_window.xml", MainWindow.SCRIPT_PATH, 'Default', '720p',
check_settings_handler=check_settings_handler)
def onInit(self):
if self._initialized is True:
return
if self.check_settings() is False:
self.close()
return
self._initialized = True
self.tv_dialog = TvDialog(self.tv_dialog_xml_file, MainWindow.SCRIPT_PATH, 'Default', '720p', main_window=self)
self.tv_dialog.doModal()
del self.tv_dialog
def close(self):
self.is_closing = True
try:
if self.tv_dialog:
self.tv_dialog.close()
del self.api
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
super(MainWindow, self).close()
def check_settings(self):
if callable(self.check_settings_handler) is False or self.check_settings_handler() is False:
return False
try:
self.api.login()
except ApiException as ex:
log("Exception %s: message=%s, code=%s" % (type(ex), ex.message, ex.code))
log(traceback.format_exc(), xbmc.LOGDEBUG)
dialog = xbmcgui.Dialog()
if ex.code == Api.E_API_ERROR:
if dialog.yesno(
addon.getAddonInfo("name"),
get_string(TEXT_AUTHENTICATION_FAILED_ID) + ":",
ex.message,
get_string(TEXT_CHECK_SETTINGS_ID)):
addon.openSettings()
return self.check_settings()
elif ex.code == Api.E_HTTP_REQUEST_FAILED:
error = ex.message
if "Errno 8" in ex.message:
error = get_string(TEXT_PLEASE_CHECK_INTERNET_CONNECTION_ID)
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_HTTP_REQUEST_ERROR_ID) + ":\n" +
error
)
elif ex.code == Api.E_JSON_DECODE:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_RESPONSE_FROM_SERVICE_PROVIDER_ID) + ":\n" +
ex.message
)
else:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_ERROR_OCCURRED_ID) + ":\n" +
ex.message
)
return False
return True

View File

@ -0,0 +1,302 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
from collections import OrderedDict
import xbmcgui
from . import *
class Model(object):
data = None # type: dict
API = None
def __init__(self, data):
self.data = data
self._listitem = None
def __getattr__(self, attr):
return self.data[attr]
def get_icon(self):
return ""
def get_listitem(self):
if self._listitem is None:
self._listitem = xbmcgui.ListItem()
for key, value in self.data.items():
if key == 'icon':
value = self.get_icon()
if isinstance(value, (type(None), str, int, float, bool)) is False:
continue
# if not isinstance(value, str):
# value = str(value)
# try:
# value = value.decode('utf-8')
# except UnicodeError:
# value = value.encode('utf-8')
self._listitem.setProperty(key, value)
return self._listitem
class Group(Model):
gid = None # type: str
name = None # type: str
channels = None # type: OrderedDict[str, Channel]
number = None # type: int
def __init__(self, gid, name, channels, number=None):
# type: (str, str, OrderedDict[str, Channel], int) -> Group
self.gid = gid
self.name = name
self.channels = OrderedDict(channels)
self.number = number
super(Group, self).__init__({"gid": gid, "group_name": name, "icon": "", "number": number})
def get_icon(self):
number = (self.number if self.number is not None else int(self.data["gid"])) % 20
return "%s.png" % (number if number != 0 else 20)
def __repr__(self):
return "%s (%s)" % (self.name, self.gid)
class Channel(Model):
cid = None # type: str
gid = None # type: str
name = None # type: str
icon = None # type: str
epg = None # type: bool
archive = None # type: bool
protected = None # type: bool
url = None # type: str
_programs = None # type: dict[int, Program]
def __init__(self, cid, gid, name, icon, epg, archive, protected=False, url=None):
# type: (str, str, str, str, bool, bool, bool, str) -> Channel
self.cid = cid
self.gid = gid
self.name = name
self.icon = icon
self.epg = epg
self.archive = archive
self.protected = protected
self.url = url
self._programs = OrderedDict()
channel_data = {
"cid": cid,
"gid": gid,
"channel_name": name,
"icon": icon,
"epg": epg,
"archive": archive,
"protected": protected,
}
super(Channel, self).__init__(channel_data)
def get_icon(self):
return addon.getAddonInfo('icon') if not self.icon else self.icon
def get_current_program(self):
# type: () -> Program
return self.get_program_by_time(int(time_now()))
def get_program_by_time(self, timestamp):
# type: (int) -> Program
for ut_start, program in list(self.programs.items()):
if program.ut_start == timestamp:
return program
if program.ut_start > timestamp and program.prev_program is not None:
return program.prev_program
return Program.factory(self)
@property
def programs(self):
if len(self._programs) == 0:
try:
real_programs = self.API.get_epg(self.cid) # type: OrderedDict
if len(real_programs) == 0:
ut_start = timestamp_to_midnight(time_now())
real_programs[ut_start] = Program.factory(self, ut_start, ut_start + HOUR)
first_program = real_programs[next(iter(real_programs.keys()))] # type: Program
last_program = real_programs[next(reversed(list(real_programs.keys())))] # type: Program
# prepend epg with dummy entries
if first_program.ut_start > time_now() - self.API.archive_ttl:
start_time = int(time_now() - self.API.archive_ttl) - DAY
else:
start_time = first_program.ut_start - DAY
start_time = timestamp_to_midnight(start_time)
programs = Program.get_dummy_programs(self, start_time, first_program.ut_start)
last_prepend_program = programs[next(reversed(list(programs.keys())))]
programs.update(real_programs)
first_program.prev_program = last_prepend_program
last_prepend_program.next_program = first_program
# append epg with dummy entries
if last_program.ut_end == 0:
last_program.ut_end = last_program.ut_start + HOUR
end_time = timestamp_to_midnight(last_program.ut_end + DAY * 2)
append_programs = Program.get_dummy_programs(self, last_program.ut_end, end_time)
first_append_program = append_programs[next(iter(append_programs.keys()))] # type: Program
programs.update(append_programs)
last_program.next_program = first_append_program
first_append_program.prev_program = last_program
prev = None # type: Program
for key in sorted(programs.keys()):
program = programs[key]
if prev:
program.prev_program = prev
program.prev_program.next_program = program
self._programs[key] = prev = programs[key]
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
start_time = timestamp_to_midnight(time_now() - self.API.archive_ttl - DAY)
end_time = timestamp_to_midnight(time_now() + TWODAYS)
self._programs = Program.get_dummy_programs(self, start_time, end_time)
return self._programs
def __repr__(self):
return "%s (%s/%s)" % (self.name, self.gid, self.cid)
class Program(Model):
cid = None # type: str
gid = None # type: str
ut_start = None # type: int
ut_end = None # type: int
length = None # type: int
title = None # type: str
descr = None # type: str
epg = None # type: bool
archive = None # type: bool
image = None # type: str
prev_program = None # type: Program
next_program = None # type: Program
@staticmethod
def factory(channel, ut_start=None, ut_end=None, length=HOUR,
title=get_string(TEXT_NO_INFO_AVAILABLE_ID),
descr=get_string(TEXT_NO_INFO_AVAILABLE_ID)):
# type: (Channel, int, int, int, str, str) -> Program
ut_start = str_to_timestamp(format_date(time_now(), custom_format="%d%m%y%H"), "%d%m%y%H") \
if ut_start is None else ut_start
ut_end = ut_start + length if ut_end is None else ut_end
return Program(channel.cid, channel.gid, int(ut_start), int(ut_end), title, descr, channel.archive)
@staticmethod
def get_dummy_programs(channel, start_time, end_time):
# type: (Channel, int, int) -> OrderedDict[int, Program]
programs = OrderedDict()
ut_start = start_time
prev = None
while ut_start < end_time:
ut_end = end_time if (ut_start + HOUR) > end_time else (ut_start + HOUR)
program = Program.factory(channel, ut_start, ut_end)
if prev is not None:
program.prev_program = prev
prev.next_program = program
programs[program.ut_start] = prev = program
ut_start = ut_start + HOUR
return programs
def __init__(self, cid, gid, ut_start, ut_end, title, descr, archive=False, image=None):
# type: (str, str, int, int, str, str, bool, str) -> Program
self.cid = cid
self.gid = gid
self.ut_start = ut_start
self.ut_end = ut_end
self.length = self.ut_end - self.ut_start
self.title = title
self.descr = descr
self.archive = archive
self.image = image
(img_s, img_m, img_l) = self.get_image_urls(self.image)
program_data = {
"cid": self.cid,
"gid": self.gid,
"title": self.title,
"title_list": (self.title[:52] + '...') if len(self.title) > 55 else self.title,
"img_s": img_s,
"img_m": img_m,
"img_l": img_l,
"descr": self.descr if self.descr is not None else "",
"t_start": format_date(self.ut_start, custom_format="%H:%M"),
"t_end": format_date(self.ut_end, custom_format="%H:%M"),
"d_start": format_date(self.ut_start, custom_format="%A, %d.%m"),
"ut_start": self.ut_start,
"ut_end": self.ut_end
}
super(Program, self).__init__(program_data)
@staticmethod
def get_image_urls(image):
# type: (str) -> (str, str, str)
if image is None:
return "", "", ""
if '/large' in image:
return image.replace('/large', '/small'), image.replace('/large', '/normal'), image
else:
return "%s/176x99" % image, "%s/350x197" % image, "%s/700x394" % image
def is_playable(self):
# type: () -> bool
return self.is_live_now() or self.is_archive_now()
def is_past_now(self):
# type: () -> bool
return self.ut_end < time_now()
def is_live_now(self):
# type: () -> bool
return self.ut_start <= time_now() <= self.ut_end
def is_archive_now(self):
# type: () -> bool
now = time_now()
return bool(self.archive) is True \
and self.ut_end < now \
and self.ut_start > (now - self.API.archive_ttl)
def equals(self, other):
# type: (Program) -> bool
return str(self.cid) == str(other.cid) \
and self.ut_start == other.ut_start \
and self.ut_end == other.ut_end
def get_listitem(self):
listitem = Model.get_listitem(self)
status = " "
if self.is_past_now():
if self.archive is True:
status = "prg_status_archive.png"
else:
status = "prg_status_past.png"
elif self.is_live_now():
status = "prg_status_live.png"
listitem.setProperty("status", status)
listitem.setProperty("day_color", "1%s.png" % format_date(self.ut_start, custom_format="%w"))
return listitem

View File

@ -0,0 +1,134 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
from . import *
from .models import Program
from xbmcgui import ListItem
class Player(xbmc.Player):
program = None # type: Program
offset = None # type: int
ut_start = None # type: int
ut_end = None # type: int
on_playback_callback = None # type: callable()
last_known_position = None # type: int
def __init__(self):
pass
def update_last_known_position(self):
self.last_known_position = int(self.getTime() + self.ut_start)
@run_async
def play(self, item=None, program=None, offset=0, on_playback_callback=None, listitem=None, windowed=False):
# type: (str, Program, int, callable, ListItem, bool) -> None
self.program = program
self.offset = offset
if self.program:
self.ut_start = self.program.ut_start + self.offset
self.ut_end = self.program.ut_end
self.on_playback_callback = on_playback_callback
if listitem is not None:
super(Player, self).play(item=item, listitem=listitem, windowed=windowed)
else:
super(Player, self).play(item=item, windowed=windowed)
def get_program(self):
# type: () -> Program
if not self.isPlaying():
return self.program
player_time = self.getTime() + self.offset if self.is_live() is False else \
int(time_now()) - self.program.ut_start
ut_start = self.program.ut_start
program = self.program
while player_time > (program.ut_end - ut_start):
program = program.next_program
return program
def get_position(self):
# type: () -> float
if self.isPlaying() is False:
return -1
try:
program = self.get_program()
if self.is_live():
return time_now() - program.ut_start
if self.ut_start < program.ut_start:
return self.getTime() - (program.ut_start - self.ut_start)
return self.getTime() + self.offset
except Exception as ex:
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
return -1
def get_percent(self, strict=False, adjust_secs=0):
# type: (bool, int) -> (float, int)
program = self.get_program()
position = self.get_position()
percent = secs_to_percent(program.length, position + adjust_secs)
if strict is True:
if percent <= 0:
percent = 0.01
elif percent >= 100:
percent = 99.99
return percent, position
def is_live(self):
# type: () -> bool
return self.offset == 0
def onPlayBackEnded(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackEnded")
def onPlayBackPaused(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackPaused")
def onPlayBackResumed(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackResumed")
def onPlayBackSeek(self, time, seekOffset):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackSeek", time=time, seekOffset=seekOffset)
def onPlayBackSeekChapter(self, chapter):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackSeekChapter", chapter=chapter)
def onPlayBackSpeedChanged(self, speed):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackSpeedChanged", speed=speed)
def onPlayBackStarted(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackStarted")
def onPlayBackStopped(self):
if callable(self.on_playback_callback):
self.on_playback_callback(event="onPlayBackStopped")

View File

@ -0,0 +1,678 @@
# coding=utf-8
#
# Copyright (C) 2018 Dmitry Vinogradov
# https://github.com/kodi-iptv-addons
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
import traceback
from threading import Timer
from urllib.parse import quote
import xbmcgui
from . import *
from .api import Api, ApiException
from .models import Program, Channel
from .player import Player
from xbmcgui import ControlImage, ControlList, ListItem, ControlProgress, ControlSlider, ControlLabel
class TvDialog(xbmcgui.WindowXMLDialog, WindowMixin):
CTRL_DUMMY = 999
CTRL_PROGRAM_TITLE = 4000
CTRL_PROGRAM_PLAYTIME = 4001
CTRL_PROGRESS = 4002
CTRL_SLIDER = 4003
CTRL_PROGRAM_DURATION = 4004
CTRL_SKIP_PLAYBACK = 4005
CTRL_PROGRAM_STARTTIME = 4006
CTRL_SLIDER_BUTTON = 4007
CTRL_PROGRAM_CHANNEL_ICON = 4008
CTRL_DUMMY_ICON = 4009
CTRL_GROUPS = 4100
CTRL_CHANNELS = 4200
CTRL_PROGRAMS = 4300
ICON_OPEN = "open"
ICON_CLOSE = "close"
ICON_ERROR = "error"
ICON_PLAY = "play"
ICON_NONPLAY = "nonplay"
ICON_END = "end"
ICON_STOP = "stop"
ICON_SWING = "swing"
WINDOW_HOME = 10000
WINDOW_FULLSCREEN_VIDEO = 12005
ctrl_program_title = None # type: ControlLabel
ctrl_program_playtime = None # type: ControlLabel
ctrl_program_channel_icon = None # type: ControlImage
ctrl_dummy_icon = None # type: ControlImage
ctrl_progress = None # type: ControlProgress
ctrl_slider = None # type: ControlSlider
ctrl_program_duration = None # type: ControlLabel
ctrl_skip_playback = None # type: ControlLabel
ctrl_program_starttime = None # type: ControlLabel
ctrl_groups = None # type: ControlList
ctrl_channels = None # type: ControlList
ctrl_programs = None # type: ControlList
main_window = None
player = None # type: Player
api = None # type: Api
skip_secs = None # type: int
prev_skip_secs = None # type: int
prev_focused_id = None # type: int
playback_info_program = None # type: Program
timer_refocus = None # type: Timer
timer_slider_update = None # type: Timer
timer_skip_playback = None # type: Timer
timer_load_program_list = None # type: Timer
timer_idle = None # type: Timer
is_closing = False # type: bool
def __init__(self, *args, **kwargs):
self.main_window = kwargs.pop("main_window", None)
self.player = Player()
self.api = self.main_window.api
self.skip_secs = self.prev_skip_secs = 0
super(TvDialog, self).__init__(**kwargs)
@property
def addon_id(self):
return "%s:%s" % (self.api.__class__.__name__, addon.getAddonInfo('version'))
def close(self):
if self.is_closing:
return
self.is_closing = True
self.preload_icon(self.ICON_CLOSE, self.addon_id)
if self.timer_refocus:
self.timer_refocus.cancel()
del self.timer_refocus
if self.timer_slider_update:
self.timer_slider_update.cancel()
del self.timer_slider_update
if self.timer_skip_playback:
self.timer_skip_playback.cancel()
del self.timer_skip_playback
if self.timer_load_program_list:
self.timer_load_program_list.cancel()
del self.timer_load_program_list
if self.timer_idle:
self.timer_idle.cancel()
del self.timer_idle
if self.player.isPlaying():
self.player.stop()
del self.player
super(TvDialog, self).close()
def onInit(self):
try:
self.ctrl_program_title = self.getControl(self.CTRL_PROGRAM_TITLE)
self.ctrl_program_playtime = self.getControl(self.CTRL_PROGRAM_PLAYTIME)
self.ctrl_program_channel_icon = self.getControl(self.CTRL_PROGRAM_CHANNEL_ICON)
self.ctrl_dummy_icon = self.getControl(self.CTRL_DUMMY_ICON)
self.ctrl_progress = self.getControl(self.CTRL_PROGRESS)
self.ctrl_slider = self.getControl(self.CTRL_SLIDER)
self.ctrl_program_duration = self.getControl(self.CTRL_PROGRAM_DURATION)
self.ctrl_skip_playback = self.getControl(self.CTRL_SKIP_PLAYBACK)
self.ctrl_program_starttime = self.getControl(self.CTRL_PROGRAM_STARTTIME)
self.ctrl_groups = self.getControl(self.CTRL_GROUPS)
self.ctrl_channels = self.getControl(self.CTRL_CHANNELS)
self.ctrl_programs = self.getControl(self.CTRL_PROGRAMS)
self.defer_refocus_window()
self.preload_icon(self.ICON_OPEN, self.addon_id)
program = Program.factory(self.get_last_played_channel())
self.play_program(program)
self.load_lists()
self.reset_idle_timer()
except ApiException as ex:
log("Exception %s: message=%s" % (type(ex), ex.message))
log(traceback.format_exc(), xbmc.LOGDEBUG)
dialog = xbmcgui.Dialog()
if ex.code == Api.E_API_ERROR:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_SERVICE_ERROR_OCCURRED_ID) + ":\n" +
ex.message
)
elif ex.code == Api.E_HTTP_REQUEST_FAILED:
error = ex.message
if "Errno 8" in ex.message:
error = get_string(TEXT_PLEASE_CHECK_INTERNET_CONNECTION_ID)
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_HTTP_REQUEST_ERROR_ID) + ":\n" +
error
)
elif ex.code == Api.E_JSON_DECODE:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_RESPONSE_FROM_SERVICE_PROVIDER_ID) + ":\n" +
ex.message
)
else:
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_ERROR_OCCURRED_ID) + ":\n" +
ex.message
)
self.main_window.close()
except Exception as ex:
self.preload_icon(self.ICON_ERROR, quote(str(ex).encode('utf-8')))
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)
line1, line2 = (str(ex) + "\n").split("\n", 1)
dialog = xbmcgui.Dialog()
dialog.ok(
addon.getAddonInfo("name"),
get_string(TEXT_UNEXPECTED_ERROR_OCCURRED_ID) + ":\n" +
line1 + "\n" +
line2
)
self.main_window.close()
def reset_idle_timer(self):
if addon.getSetting('stop_on_idle') == 'false' \
or addon.getSetting('stop_on_idle') is False:
return
if self.timer_idle:
self.timer_idle.cancel()
del self.timer_idle
self.timer_idle = threading.Timer(HOUR, self.show_idle_dialog)
self.timer_idle.start()
def show_idle_dialog(self, time_to_wait=60):
# type: (int) -> None
dialog = xbmcgui.DialogProgress()
dialog.create(addon.getAddonInfo("name"), get_string(TEXT_IDLE_DIALOG_ID))
secs = 0
increment = int(100 / time_to_wait)
cancelled = False
while secs < time_to_wait:
secs += 1
dialog.update(increment * secs, get_string(TEXT_IDLE_DIALOG_ID) +
get_string(TEXT_IDLE_DIALOG_COUNTDOWN_ID) % (time_to_wait - secs))
xbmc.sleep(1000)
if dialog.iscanceled():
cancelled = True
break
if cancelled is True:
return
dialog.close()
self.main_window.close()
def get_last_played_channel(self):
# type: () -> Channel
last_channel_id = addon.getSetting("last_channel_id") or None
if last_channel_id is None or last_channel_id == "None" or (last_channel_id in self.api.channels) is False:
last_channel_id = list(self.api.channels.keys())[0]
return self.api.channels[last_channel_id]
@run_async
def load_lists(self):
if self.ctrl_groups.size() == 0:
self.ctrl_groups.addItems(
[group.get_listitem() for group in list(self.api.groups.values()) if len(group.channels) > 0])
self.select_group_listitem(self.player.program.gid)
if self.ctrl_channels.size() == 0:
self.ctrl_channels.addItems([channel.get_listitem() for channel in list(self.api.channels.values())])
self.select_channel_listitem(self.player.program.cid, False)
if self.ctrl_programs.size() == 0:
channel = self.api.channels[self.player.program.cid]
self.ctrl_programs.addItems([prg.get_listitem() for prg in list(channel.programs.values())])
self.player.program = self.api.channels[self.player.program.cid].get_current_program()
self.select_program_listitem(self.player.program.ut_start, False)
def select_group_listitem(self, gid):
# type: (str) -> ListItem
for index in range(self.ctrl_groups.size()):
item = self.ctrl_groups.getListItem(index)
if item.getProperty("gid") == gid:
self.ctrl_groups.selectItem(index)
return item
def select_channel_listitem(self, cid, select_group=True):
# type: (str, bool) -> ListItem
item = self.ctrl_channels.getSelectedItem()
if item.getProperty("cid") == str(cid):
return item
for index in range(self.ctrl_channels.size()):
item = self.ctrl_channels.getListItem(index)
if item.getProperty("cid") == cid:
self.ctrl_channels.selectItem(index)
if select_group is True:
self.select_group_listitem(item.getProperty("gid"))
return item
def select_program_listitem(self, timestamp, select_channel=True):
# type: (int, bool) -> ListItem
for index in range(self.ctrl_programs.size()):
item = self.ctrl_programs.getListItem(index)
next_item = self.ctrl_programs.getListItem(index+1)
ut_start = int(item.getProperty("ut_start"))
ut_end = int(next_item.getProperty("ut_start")) \
if next_item else int(item.getProperty("ut_end"))
if ut_start <= int(timestamp) < ut_end:
self.ctrl_programs.selectItem(index)
if select_channel is True:
self.select_channel_listitem(item.getProperty("cid"))
return item
def defer_load_program_list(self, cid, select_timestamp):
# type: (str, int) -> None
if self.timer_load_program_list:
self.timer_load_program_list.cancel()
del self.timer_load_program_list
self.timer_load_program_list = threading.Timer(0.5, self.load_program_list, [cid, select_timestamp])
self.timer_load_program_list.start()
def load_program_list(self, cid, select_timestamp):
# type: (str, int) -> None
if self.ctrl_channels.getSelectedItem().getProperty("cid") != cid:
return
selected_program = self.ctrl_programs.getSelectedItem()
if selected_program is None or selected_program.getProperty("cid") != cid:
channel = self.api.channels[cid]
self.ctrl_programs.reset()
self.ctrl_programs.addItems([program.get_listitem() for program in list(channel.programs.values())])
self.select_program_listitem(select_timestamp, False)
def play_program(self, program, offset=0):
# type: (Program, int) -> None
if program.is_playable() is False:
self.preload_icon(self.ICON_NONPLAY, normalize(self.api.channels[program.cid].name))
dialog = xbmcgui.Dialog()
dialog.ok(addon.getAddonInfo("name"), "\n" + get_string(TEXT_NOT_PLAYABLE_ID))
return
try:
if program.is_live_now():
if offset > 0:
url = self.api.get_stream_url(program.cid, program.ut_start + offset)
else:
url = self.api.get_stream_url(program.cid)
elif program.is_archive_now():
offset += 1
url = self.api.get_stream_url(program.cid, program.ut_start + offset)
else:
url = self.api.get_stream_url(program.cid)
offset = 0
if self.player.program and self.player.program.cid != program.cid:
self.preload_icon(self.ICON_SWING, normalize(self.api.channels[self.player.program.cid].name))
self.player.play(url, program, offset, self.on_playback_callback)
addon.setSetting("last_channel_id", str(program.cid))
except ApiException as ex:
self.preload_icon(self.ICON_ERROR, quote(ex.message.encode('utf-8')))
log("Exception %s: message=%s, code=%s" % (type(ex), ex.message, ex.code))
log(traceback.format_exc(), xbmc.LOGDEBUG)
dialog = xbmcgui.Dialog()
dialog.ok(addon.getAddonInfo("name"), get_string(TEXT_SERVICE_ERROR_OCCURRED_ID) + "\n" + ex.message)
def on_playback_callback(self, event, **kwargs):
# type: (str, dict) -> None
if self.is_closing:
return
log(event, xbmc.LOGDEBUG)
if event == "onPlayBackEnded":
if self.player.program:
if self.player.program.is_live_now():
program = self.api.channels[self.player.program.cid].get_current_program()
self.play_program(program)
else:
offset = self.player.last_known_position - self.player.program.ut_start
self.play_program(self.player.program, offset)
self.preload_icon(self.ICON_END, normalize(self.api.channels[self.player.program.cid].name))
elif event == "onPlayBackStopped":
self.preload_icon(self.ICON_STOP, normalize(self.get_last_played_channel().name))
dialog = xbmcgui.Dialog()
dialog.ok(addon.getAddonInfo("name"), "\n\n" + get_string(TEXT_NOT_PLAYABLE_ID))
self.setFocusId(self.CTRL_CHANNELS)
elif event == "onPlayBackStarted":
self.update_playback_info()
def defer_refocus_window(self):
if self.timer_refocus:
self.timer_refocus.cancel()
self.timer_refocus = None
if not self.is_closing:
self.timer_refocus = threading.Timer(5, self.refocus_window)
self.timer_refocus.start()
def refocus_window(self):
if xbmcgui.getCurrentWindowId() == self.WINDOW_HOME:
xbmc.executebuiltin('ActivateWindow(%s)' % self.WINDOW_FULLSCREEN_VIDEO)
self.defer_refocus_window()
def preload_icon(self, a, b='', c=1):
try:
i = unique(x(h2), x(h1)).format(self.addon_id, a, b, c, z(unique(self.api.client_id, x(h1))), time_now())
self.ctrl_dummy_icon.setImage(i, False)
except:
pass
# noinspection PyPep8Naming
def onAction(self, action):
action_id = action.getId()
focused_id = self.getFocusId()
self.reset_idle_timer()
if focused_id == self.CTRL_DUMMY: # no controls are visible
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
confirm = xbmcgui.Dialog()
yesno = bool(confirm.yesno(addon.getAddonInfo("name"), " ", get_string(TEXT_SURE_TO_EXIT_ID)))
del confirm
if yesno is True:
self.main_window.close()
if action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_SLIDER)
self.update_playback_info()
self.prev_focused_id = self.CTRL_DUMMY
return True
elif focused_id == self.CTRL_SLIDER: # navigation within current playback details
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
self.reset_skip_playback()
self.prev_focused_id = self.CTRL_DUMMY
return True
elif action_id in [xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT]:
if self.prev_focused_id != focused_id:
self.update_playback_info()
self.prev_focused_id = focused_id
return True
program = self.player.get_program()
if program.archive is False:
self.update_playback_info()
show_small_popup(addon.getAddonInfo("name"), get_string(TEXT_CHANNEL_HAS_NO_ARCHIVE_ID))
self.prev_focused_id = focused_id
return True
if self.player.is_live():
if action_id == xbmcgui.ACTION_MOVE_LEFT and self.api.diff_live_archive > TENSECS:
self.prev_focused_id = focused_id
confirm = xbmcgui.Dialog()
yesno = bool(
confirm.yesno(
addon.getAddonInfo("name"),
get_string(TEXT_ARCHIVE_NOT_AVAILABLE_YET_ID),
get_string(TEXT_JUMP_TO_ARCHIVE_ID)
)
)
del confirm
if yesno is False:
self.skip_secs = 0
self.update_playback_info()
return True
self.skip_secs = self.api.diff_live_archive * -1
self.update_playback_info()
self.defer_skip_playback()
return True
elif action_id == xbmcgui.ACTION_MOVE_RIGHT and self.skip_secs >= 0:
self.update_playback_info()
show_small_popup(addon.getAddonInfo("name"), get_string(TEXT_LIVE_NO_FORWARD_SKIP_ID))
self.prev_focused_id = focused_id
return True
if self.playback_info_program is None:
self.playback_info_program = program
curr_position = percent_to_secs(self.playback_info_program.length, self.ctrl_progress.getPercent())
new_position = percent_to_secs(self.playback_info_program.length, self.ctrl_slider.getPercent())
self.skip_secs = new_position - curr_position
if self.ctrl_slider.getPercent() == 0 and action_id == xbmcgui.ACTION_MOVE_LEFT:
self.playback_info_program = self.playback_info_program.prev_program
self.set_slider(100.)
self.prev_skip_secs += self.skip_secs
self.skip_secs = 0
elif self.ctrl_slider.getPercent() == 100 and action_id == xbmcgui.ACTION_MOVE_RIGHT:
self.playback_info_program = self.playback_info_program.next_program
self.set_slider(0.)
self.prev_skip_secs += self.skip_secs
self.skip_secs = 0
self.ctrl_skip_playback.setLabel(format_secs(self.skip_secs + self.prev_skip_secs, "skip"))
self.defer_skip_playback()
self.update_playback_info()
elif focused_id == self.CTRL_GROUPS: # navigation within channel groups
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
elif action_id in [xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_MOVE_UP]:
selected_group = self.ctrl_groups.getSelectedItem()
gid = selected_group.getProperty("gid")
selected_channel = self.ctrl_channels.getSelectedItem()
if selected_channel.getProperty("gid") == gid:
self.prev_focused_id = focused_id
return True
for index in range(self.ctrl_channels.size()):
item = self.ctrl_channels.getListItem(index)
if item.getProperty("gid") == gid:
self.ctrl_channels.selectItem(index)
self.prev_focused_id = focused_id
self.defer_load_program_list(item.getProperty("cid"), int(time_now()))
return True
elif action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_DUMMY)
selected_channel = self.ctrl_channels.getSelectedItem()
channel = self.api.channels[selected_channel.getProperty("cid")]
program = channel.get_current_program()
if program is not None:
self.play_program(program)
elif focused_id == self.CTRL_CHANNELS: # navigation within channels
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
elif action_id == xbmcgui.ACTION_MOVE_RIGHT:
selected_channel = self.ctrl_channels.getSelectedItem()
cid = selected_channel.getProperty("cid")
selected_program = self.ctrl_programs.getSelectedItem()
if selected_program and selected_program.getProperty("cid") != cid:
self.defer_load_program_list(cid, int(time_now()))
elif action_id in [xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_MOVE_UP]:
if self.prev_focused_id == self.CTRL_SLIDER:
if self.ctrl_channels.getSelectedItem().getProperty("cid") != self.player.program.cid:
cid = self.player.program.cid
timestamp = int(self.player.get_program().ut_start + self.player.get_position())
self.ctrl_programs.reset()
self.select_channel_listitem(cid)
self.defer_load_program_list(cid, timestamp)
self.prev_focused_id = focused_id
return True
self.select_program_listitem(int(self.player.get_program().ut_start + self.player.get_position()))
selected_channel = self.ctrl_channels.getSelectedItem()
cid = selected_channel.getProperty("cid")
gid = selected_channel.getProperty("gid")
selected_program = self.ctrl_programs.getSelectedItem()
if selected_program is None or selected_program.getProperty("cid") != cid:
self.defer_load_program_list(cid, int(time_now()))
selected_group = self.ctrl_groups.getSelectedItem()
if selected_group.getProperty("gid") == gid:
self.prev_focused_id = focused_id
return True
for index in range(self.ctrl_groups.size()):
item = self.ctrl_groups.getListItem(index)
if item.getProperty("gid") == gid:
self.ctrl_groups.selectItem(index)
self.prev_focused_id = focused_id
return True
elif action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_DUMMY)
selected_channel = self.ctrl_channels.getSelectedItem()
channel = self.api.channels[selected_channel.getProperty("cid")]
program = channel.get_current_program()
if program is not None:
self.play_program(program)
elif focused_id == self.CTRL_PROGRAMS: # navigation within programs
if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
self.setFocusId(self.CTRL_DUMMY)
elif action_id in [xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_MOUSE_LEFT_CLICK]:
self.setFocusId(self.CTRL_DUMMY)
selected_program = self.ctrl_programs.getSelectedItem()
channel = self.api.channels[selected_program.getProperty("cid")]
program = channel.get_program_by_time(int(selected_program.getProperty("ut_start")))
if program is not None:
if program.is_live_now() is False and program.archive is False:
show_small_popup(addon.getAddonInfo("name"), get_string(TEXT_CHANNEL_HAS_NO_ARCHIVE_ID))
self.prev_focused_id = focused_id
return True
if program.equals(self.player.get_program()) is True:
self.prev_focused_id = focused_id
return True
self.play_program(program)
self.prev_focused_id = focused_id
def reset_skip_playback(self):
self.skip_secs = self.prev_skip_secs = 0
self.ctrl_skip_playback.setLabel(format_secs(self.skip_secs, "skip"))
self.set_slider(self.ctrl_progress.getPercent())
self.playback_info_program = None
self.update_playback_info()
def set_slider(self, percent):
self.ctrl_slider.setPercent(percent)
def defer_skip_playback(self):
if self.timer_skip_playback:
self.timer_skip_playback.cancel()
self.timer_skip_playback = None
if not self.is_closing:
self.timer_skip_playback = threading.Timer(2, self.skip_playback)
self.timer_skip_playback.start()
def skip_playback(self):
self.setFocusId(self.CTRL_DUMMY)
if self.timer_skip_playback:
self.timer_skip_playback.cancel()
self.timer_skip_playback = None
secs_to_skip = self.skip_secs + self.prev_skip_secs
if secs_to_skip == 0:
return
program = self.player.get_program()
curr_pos = self.player.get_position()
new_pos = program.ut_start + curr_pos + secs_to_skip
if new_pos < program.ut_start:
while new_pos < program.ut_start:
program = program.prev_program
elif new_pos > program.ut_end:
while new_pos > program.ut_end:
program = program.next_program
offset = new_pos - program.ut_start
if (program.ut_start + offset) > int(time_now()):
self.reset_skip_playback()
return
self.play_program(program, int(offset))
self.reset_skip_playback()
def defer_update_playback_info(self):
if self.timer_slider_update:
self.timer_slider_update.cancel()
del self.timer_slider_update
if not self.is_closing:
interval = 1 if self.getFocusId() == self.CTRL_SLIDER else 30
self.timer_slider_update = threading.Timer(interval, self.update_playback_info)
self.timer_slider_update.start()
def update_playback_info(self):
try:
if self.main_window.is_closing:
return
if not self.player or not self.player.isPlaying():
return
self.player.update_last_known_position()
position = 0
if self.playback_info_program is not None \
and self.playback_info_program != self.player.get_program():
program = self.playback_info_program
percent = 100. if program.ut_start < self.player.get_program().ut_start else 0.
self.ctrl_progress.setPercent(percent)
else:
percent, position = self.player.get_percent(True)
self.ctrl_progress.setPercent(percent)
if self.skip_secs + self.prev_skip_secs == 0:
self.set_slider(percent)
program = self.player.get_program()
self.ctrl_program_playtime.setLabel(format_secs(int(position)))
self.ctrl_program_duration.setLabel(format_secs(program.length))
self.ctrl_program_title.setLabel(program.title)
self.ctrl_program_starttime.setLabel(format_date(program.ut_start, custom_format="%A, %d %b., %H:%M"))
channel = self.api.channels[self.player.program.cid]
self.ctrl_program_channel_icon.setImage(channel.get_icon())
if self.getFocusId() != self.CTRL_SLIDER:
self.preload_icon(self.ICON_PLAY, normalize(channel.name))
self.defer_update_playback_info()
except Exception as ex:
self.preload_icon(self.ICON_ERROR, quote(str(ex).encode('utf-8')))
log("Exception %s: message=%s" % (type(ex), ex))
log(traceback.format_exc(), xbmc.LOGDEBUG)

View File

@ -0,0 +1,26 @@
Copyright (c) 2011 Mikel Azkolain
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of copyright holders nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,372 @@
"""
Created on 09/08/2011
@author: mikel
"""
__all__ = ["fonts", "includes"]
import logging
import os
import re
import shutil
import sys
import time
import xml.etree.ElementTree as ET
from datetime import datetime
from os import listdir
from os.path import isdir, dirname, basename
import xbmc
import xbmcgui
class SkinUtilsError(Exception):
pass
def reload_skin():
xbmc.executebuiltin("XBMC.ReloadSkin()")
def setup_logging():
# Keep comaptibility with Python2.6
if hasattr(logging, 'NullHandler'):
logger = logging.getLogger('skinutils')
logger.addHandler(logging.NullHandler())
def get_logger():
return logging.getLogger('skinutils')
def debug_log(msg):
get_logger().debug(msg)
def get_sha1_obj():
# SHA1 lib 2.4 compatibility
try:
from hashlib import sha1
return sha1()
except:
import sha
return sha.new()
def sha1_file(file, block_size=2 ** 20):
f = open(file, 'rb')
sha1 = get_sha1_obj()
while True:
data = f.read(block_size)
if not data:
break
sha1.update(data)
f.close()
return sha1.hexdigest()
def try_remove_file(file, wait=0.5, tries=10):
removed = False
num_try = 0
while num_try < tries and not removed:
try:
os.remove(file)
return True
except OSError:
num_try += 1
time.sleep(wait)
return False
def case_file_exists(file):
if not os.path.isfile(file):
return False
else:
file_dir = dirname(file)
if not isdir(file_dir):
return False
else:
dir_contents = listdir(file_dir)
return basename(file) in dir_contents
def get_current_skin_path():
return os.path.normpath(xbmc.translatePath("special://skin/"))
def get_skin_name():
return os.path.basename(get_current_skin_path())
def get_local_skin_path():
user_addons_path = xbmc.translatePath("special://home/addons")
return os.path.normpath(
os.path.join(user_addons_path, get_skin_name())
)
def copy_skin_to_userdata(ask_user=True):
# Warn user before doing this weird thing
d = xbmcgui.Dialog()
msg1 = "This addon needs to install some extra resources."
msg2 = "This installation requires a manual XBMC restart."
msg3 = "Begin installation now? After that it will exit."
make_copy = (
not ask_user or
d.yesno("Notice", msg1, msg2, msg3)
)
if make_copy:
# Get skin dest name
local_skin_path = get_local_skin_path()
# If it was not copied before...
if not os.path.exists(local_skin_path):
shutil.copytree(get_current_skin_path(), local_skin_path)
return make_copy
def is_invalid_local_skin():
# Get skin paths
current_skin_path = get_current_skin_path()
local_skin_path = get_local_skin_path()
# If the local path does not exist
if not os.path.isdir(local_skin_path):
return False
else:
# Get addon xml paths
current_xml = os.path.join(current_skin_path, 'addon.xml')
local_xml = os.path.join(local_skin_path, 'addon.xml')
# Both files must exist
if not os.path.isfile(current_xml) or not os.path.isfile(local_xml):
return True
# If sum of both files mismatch, got it!
elif sha1_file(current_xml) != sha1_file(local_xml):
return True
# Otherwise everything is ok
else:
return False
def fix_invalid_local_skin():
local_skin_path = get_local_skin_path()
time_suffix = datetime.now().strftime('%Y%m%d%H%M%S')
backup_skin_path = local_skin_path + '-skinutils-' + time_suffix
# Just move the skin, if it already exists someone is trolling us...
shutil.move(local_skin_path, backup_skin_path)
# And now do the real copy
copy_skin_to_userdata(ask_user=False)
# Inform the user about the operation...
d = xbmcgui.Dialog()
l1 = "Your local skin is not in use (probably outdated)."
l2 = "Press OK to apply a fix (archiving the old skin)."
l3 = "You will need to restart XBMC once more."
d.ok("Notice", l1 + '\n' + '\n' + l2 + '\n' + l3)
sys.exit()
# Skin was copied but XBMC was not restarted
def check_needs_restart():
# Get skin paths
current_skin_path = get_current_skin_path()
local_skin_path = get_local_skin_path()
# Local skin exists and does not match current skin path
if os.path.isdir(local_skin_path) and current_skin_path != local_skin_path:
# Check if the local skin is a leftover from a previous XBMC install
if is_invalid_local_skin():
fix_invalid_local_skin()
# Local skin is correct, a restart is needed
else:
d = xbmcgui.Dialog()
d.ok("Notice", "Restart XBMC to complete the installation.")
sys.exit()
def do_write_test(path):
test_file = os.path.join(path, 'write_test.txt')
get_logger().debug('performing write test: %s' % test_file)
try:
# Open and cleanup
open(test_file, 'w').close()
os.remove(test_file)
return True
except Exception:
return False
def skin_is_local():
return get_current_skin_path() == get_local_skin_path()
def check_skin_writability():
# Some debug info
debug_log("-- skinutils debug info --")
debug_log("current skin path: %s\n" % get_current_skin_path())
debug_log("local path should be: %s" % get_local_skin_path())
# Check if XBMC needs a restart
check_needs_restart()
# Get the current skin's path
skin_path = get_local_skin_path()
# Check if it's local or not (contained in userdata)
if not skin_is_local():
copy_skin_to_userdata()
sys.exit()
# Check if this path is writable
elif not os.access(skin_path, os.W_OK) or not do_write_test(skin_path):
raise IOError("Skin directory is not writable.")
def make_backup(path):
backup_path = path + '-skinutilsbackup'
# If the backup already exists, don't overwrite it
if not os.path.exists(backup_path):
shutil.copy(path, backup_path)
def restore_backup(path):
backup_path = path + '-skinutilsbackup'
# Do nothing if no backup exists
if os.path.exists(backup_path):
# os.rename is atomic on unix, and it will overwrite silently
if os.name != 'nt':
os.rename(backup_path, path)
# Windows will complain if the file exists
else:
os.remove(path)
os.rename(backup_path, path)
def is_invalid_xml(file):
contents = open(file, 'r').read()
# Check for invalid comments
pattern = re.compile('<!--(.*?)-->', re.MULTILINE | re.DOTALL)
group_pattern = re.compile('^-|--|-$')
for match in re.finditer(pattern, contents):
if re.match(group_pattern, match.group(1)) is not None:
return True
# Check also for whitespace prior to declaration
whitespace_pattern = re.compile('^\s+', re.MULTILINE)
return whitespace_pattern.match(contents) is not None
def sanitize_xml(file):
contents = open(file, 'r').read()
# Remove leading whitespace to declaration
contents = contents.lstrip()
# Strip invalid comments
p = re.compile('<!--.*?-->', re.MULTILINE | re.DOTALL)
clean_contents, num_repl = re.subn(p, '', contents)
open(file, 'w').write(clean_contents)
def install_resources():
pass
class DocumentCache:
__cached_docs = None
def __init__(self):
self.__cached_docs = {}
def _check_file_exists(self, file):
if not os.path.isfile(file):
raise IOError('File not found: %s' % file)
def contains(self, file):
return file in self.__cached_docs
def _check_file_known(self, file):
if not self.contains(file):
raise KeyError('Unknown file: %s' % file)
def list_files(self):
return self.__cached_docs.keys()
def items(self):
return self.__cached_docs.items()
def add(self, file):
self._check_file_exists(file)
self.__cached_docs[file] = None
def read(self, file):
self._check_file_exists(file)
# If there is no cached data...
if not self.contains(file) or self.__cached_docs[file] is None:
# Check if the file about to load is sane
if is_invalid_xml(file):
make_backup(file)
sanitize_xml(file)
# Parse the document
self.__cached_docs[file] = ET.parse(file)
return self.__cached_docs[file]
def write(self, file):
self._check_file_known(file)
# If there is a document in cache it may contain modifications
if self.__cached_docs[file] is not None:
make_backup(file)
self.__cached_docs[file].write(file)
def write_all(self):
for item in self.__cached_docs:
self.write(item)
def clear(self, file):
self._check_file_known(file)
self.__cached_docs[file] = None
def clear_all(self):
for item in self.__cached_docs:
self.clear(item)
def rollback(self, file):
self._check_file_known(file)
restore_backup(file)
self.clear(file)
def rollback_all(self):
for item in self.__cached_docs:
self.rollback(item)
setup_logging()

View File

@ -0,0 +1,210 @@
"""
Created on 09/08/2011
@author: mikel
"""
import os
import shutil
import xml.etree.ElementTree as ET
import xbmc
from . import SkinUtilsError, check_skin_writability, reload_skin, try_remove_file, case_file_exists, \
DocumentCache, get_logger
class FontXmlError(SkinUtilsError):
pass
class FontManager:
__installed_names = None
__installed_fonts = None
__doc_cache = None
def _list_skin_font_files(self):
font_xml_list = []
skin_path = xbmc.translatePath("special://skin/")
# Go into each dir. Could be 720, 1080...
for dir_item in os.listdir(skin_path):
dir_path = os.path.join(skin_path, dir_item)
if os.path.isdir(dir_path):
# Try with font.xml
file = os.path.join(dir_path, "font.xml")
if case_file_exists(file):
font_xml_list.append(file)
# Don't try the next step on windows, wasted time
file = os.path.join(dir_path, "Font.xml")
if case_file_exists(file):
font_xml_list.append(file)
return font_xml_list
def __init__(self):
self.__installed_names = []
self.__installed_fonts = []
self.__doc_cache = DocumentCache()
# Check if the environment is sane
check_skin_writability()
# Initialize the doc cache with the skin's files
for file in self._list_skin_font_files():
self.__doc_cache.add(file)
def is_name_installed(self, name):
return name in self.__installed_names
def is_font_installed(self, file):
return file in self.__installed_fonts
def _get_font_attr(self, node, name):
attrnode = node.find(name)
if attrnode is not None:
return attrnode.text
def _copy_font_file(self, file):
skin_font_path = xbmc.translatePath("special://skin/fonts/")
file_name = os.path.basename(file)
dest_file = os.path.join(skin_font_path, file_name)
# TODO: Unix systems could use symlinks
# Check if it's already there
if dest_file not in self.__installed_fonts:
self.__installed_fonts.append(dest_file)
# Overwrite if file exists
shutil.copyfile(file, dest_file)
def _add_font_attr(self, fontdef, name, value):
attr = ET.SubElement(fontdef, name)
attr.text = value
attr.tail = "\n\t\t\t"
return attr
def _install_font_def(self, skin_file, name, filename, size, style="", aspect="", linespacing=""):
# Add it to the registry
self.__installed_names.append(name)
# Get the parsed skin font file
font_doc = self.__doc_cache.read(skin_file)
# Iterate over all the fontsets on the file
for fontset in font_doc.getroot().findall("fontset"):
fontset.findall("font")[-1].tail = "\n\t\t"
fontdef = ET.SubElement(fontset, "font")
fontdef.text, fontdef.tail = "\n\t\t\t", "\n\t"
self._add_font_attr(fontdef, "name", name)
# We get the full file path to the font, so let's basename
self._add_font_attr(fontdef, "filename", os.path.basename(filename))
self._copy_font_file(filename)
last = self._add_font_attr(fontdef, "size", size)
if style:
if style in ["normal", "bold", "italics", "bolditalics", "lighten"]:
last = self._add_font_attr(fontdef, "style", style)
else:
raise FontXmlError(
"Font '%s' has an invalid style definition: %s"
% (name, style)
)
if aspect:
last = self._add_font_attr(fontdef, "aspect", aspect)
if linespacing:
last = self._add_font_attr(fontdef, "linespacing", linespacing)
last.tail = "\n\t\t"
def _install_file(self, doc_cache, user_file, skin_file, font_path):
user_doc = doc_cache.read(user_file)
# Handle only the first fontset
fontset = user_doc.getroot().find("fontset")
if len(fontset):
# Every font definition inside it
for item in fontset.findall("font"):
name = self._get_font_attr(item, "name")
# Basic check for malformed defs.
if name is None:
raise FontXmlError("Malformed XML: No name for font definition.")
# Omit already defined fonts
elif not self.is_name_installed(name):
font_file_path = os.path.join(
font_path, self._get_font_attr(item, "filename")
)
self._install_font_def(
skin_file,
name,
font_file_path,
self._get_font_attr(item, "size"),
self._get_font_attr(item, "style"),
self._get_font_attr(item, "aspect"),
self._get_font_attr(item, "linespacing")
)
def _get_res_folder(self, path):
return os.path.basename(os.path.dirname(path))
def _get_res_filename(self, res_folder, user_file):
path, ext = os.path.splitext(user_file)
return path + '-' + res_folder + ext
def install_file(self, user_file, font_path, commit=True, clear=True):
doc_cache = DocumentCache()
# If the file does not exist the following will fail
doc_cache.add(user_file)
# Install the file into every cached skin font file
for skin_file in self.__doc_cache.list_files():
res_folder = self._get_res_folder(skin_file)
res_file = self._get_res_filename(res_folder, user_file)
# If an specific res file exists...
if os.path.isfile(res_file):
self._install_file(doc_cache, res_file, skin_file, font_path)
# Otherwise use the dafault fallback
else:
self._install_file(doc_cache, user_file, skin_file, font_path)
# If save was requested
if commit:
self.__doc_cache.write_all()
# Clear cached docs after write (if requested)
if clear:
self.__doc_cache.clear_all()
def remove_font(self, name):
pass
def remove_installed_names(self):
self.__doc_cache.rollback_all()
def remove_installed_fonts(self):
for item in self.__installed_fonts:
if not try_remove_file(item):
get_logger().error(
'Failed removing font file "%s". XBMC may still be using it.' % item
)
def cleanup(self):
self.remove_installed_names()
# Reload skin so font files are no longer in use, and then delete them
reload_skin()
self.remove_installed_fonts()
def __del__(self):
self.cleanup()

View File

@ -0,0 +1,90 @@
"""
Created on 09/08/2011
@author: mikel
"""
import os
import xml.etree.ElementTree as ET
import xbmc
from . import SkinUtilsError, check_skin_writability, case_file_exists, DocumentCache, get_logger
class IncludeXmlError(SkinUtilsError):
pass
class IncludeManager:
__installed_names = None
__doc_cache = None
def _list_skin_include_files(self):
include_list = []
skin_path = xbmc.translatePath("special://skin/")
# Go into each dir. Could be 720, 1080...
for dir_item in os.listdir(skin_path):
dir_path = os.path.join(skin_path, dir_item)
if os.path.isdir(dir_path):
file = os.path.join(dir_path, "includes.xml")
if case_file_exists(file):
include_list.append(file)
file = os.path.join(dir_path, "Includes.xml")
if case_file_exists(file):
include_list.append(file)
return include_list
def __init__(self):
self.__installed_names = []
self.__doc_cache = DocumentCache()
# Check if the environment is sane
check_skin_writability()
# Initialize the doc cache with found files
for file in self._list_skin_include_files():
self.__doc_cache.add(file)
def is_name_installed(self, name):
return name in self.__installed_names
def add_include(self, name, node):
for file in self.__doc_cache.list_files():
doc = self.__doc_cache.read(file)
doc.getroot().append(node)
self.__installed_names.append(name)
def install_file(self, file, commit=True, clear=True):
get_logger().info('install include: %s' % file)
tree = ET.parse(file)
# Handle all includes
for item in tree.getroot().findall("include"):
name = item.get("name")
if name is None:
get_logger().warning('Only named includes are supported.')
elif self.is_name_installed(name):
get_logger().warning('Include name "%s" already installed' % name)
else:
self.add_include(name, item)
# If a save was requested
if commit:
self.__doc_cache.write_all()
# If we where requested to clear the cached docs
if clear:
self.__doc_cache.clear_all()
def remove_installed_names(self):
self.__doc_cache.rollback_all()
def cleanup(self):
self.remove_installed_names()
def __del__(self):
self.cleanup()

View File

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

View File

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

View File

@ -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 "Дек"

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<fonts>
<fontset id="Default" idloc="15109">
<!-- Normal Fonts -->
<font>
<name>script.module.iptvlib-font_MainMenu</name>
<filename>script.module.iptvlib-NotoSans-Bold.ttf</filename>
<size>60</size>
</font>
<font>
<name>script.module.iptvlib-font30_title</name>
<filename>script.module.iptvlib-NotoSans-Bold.ttf</filename>
<size>30</size>
</font>
<font>
<name>script.module.iptvlib-font30</name>
<filename>script.module.iptvlib-NotoSans-Regular.ttf</filename>
<size>30</size>
<style>lighten</style>
</font>
<font>
<name>script.module.iptvlib-font14</name>
<filename>script.module.iptvlib-NotoSans-Regular.ttf</filename>
<size>33</size>
<style>lighten</style>
</font>
<font>
<name>script.module.iptvlib-font12</name>
<filename>script.module.iptvlib-NotoSans-Regular.ttf</filename>
<size>25</size>
<style>lighten</style>
</font>
</fontset>
<fontset id="Arial" idloc="31053">
<!-- Arial Font better for non English -->
<font>
<name>script.module.iptvlib-font_MainMenu</name>
<filename>arial.ttf</filename>
<size>52</size>
<style>bold</style>
</font>
<font>
<name>script.module.iptvlib-font30_title</name>
<filename>arial.ttf</filename>
<size>30</size>
<style>bold</style>
</font>
<font>
<name>script.module.iptvlib-font30</name>
<filename>arial.ttf</filename>
<size>30</size>
</font>
<font>
<name>script.module.iptvlib-font14</name>
<filename>arial.ttf</filename>
<size>30</size>
</font>
<font>
<name>script.module.iptvlib-font12</name>
<filename>arial.ttf</filename>
<size>22</size>
</font>
</fontset>
</fonts>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="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.
-->
<window>
<allowoverlay>no</allowoverlay>
<animation effect="fade" start="0" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
<coordinates>
<system>1</system>
<posx>0</posx>
<posy>0</posy>
</coordinates>
<controls>
<control type="image" id="4600">
<description>Background</description>
<posx>0</posx>
<posy>0</posy>
<width>1280</width>
<height>720</height>
<texture>black.png</texture>
<fadetime>500</fadetime>
</control>
</controls>
</window>

View File

@ -0,0 +1,848 @@
<?xml version="1.0" encoding="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.
-->
<window type="dialog">
<onload condition="String.IsEmpty(Window(home).Property(channel_id))">SetProperty(channel_id,0,home)</onload>
<onunload>ClearProperty(channel_id,home)</onunload>
<coordinates>
<system>1</system>
<left>0</left>
<top>0</top>
</coordinates>
<defaultcontrol always="true">999</defaultcontrol>
<controls>
<control type="button" id="999">
<description>Dummy control used to set focus to on video start</description>
<left>-100</left>
<top>-100</top>
<height>0</height>
<width>0</width>
<aligny>center</aligny>
<align>center</align>
<font>font_MainMenu</font>
<textcolor>black</textcolor>
<texturefocus>white.png</texturefocus>
<texturenofocus>white.png</texturenofocus>
<onclick>-</onclick>
<onup>4003</onup>
<ondown>4003</ondown>
<onleft>4003</onleft>
<onright>4003</onright>
</control>
<control type="image" id="4009">
<description>Dummy image used for preload of icons</description>
<left>-100</left>
<top>-100</top>
<height>1</height>
<width>1</width>
<aspectratio>keep</aspectratio>
</control>
<!-- OSD for current playing program -->
<control type="group">
<description>OSD for current playing program</description>
<left>0</left>
<top>0</top>
<height>720</height>
<width>1280</width>
<visible allowhiddenfocus="true">Control.HasFocus(4003)</visible>
<animation type="Visible">
<effect type="fade" start="0" end="100" time="300"/>
<effect type="slide" start="0,200" end="0,0" time="300" tween="cubic" easing="out"/>
</animation>
<animation type="Hidden">
<effect type="fade" start="100" end="0" time="300"/>
<effect type="slide" start="0,0" end="0,200" time="300" tween="cubic" easing="in"/>
</animation>
<control type="image">
<description>Background</description>
<left>0</left>
<top>0</top>
<width>1280</width>
<height>720</height>
<texture>fade.png</texture>
<fadetime>300</fadetime>
</control>
<control type="label" id="4005">
<description>Skip time within playback</description>
<visible>!String.StartsWith(Control.GetLabel(4005),0 )</visible>
<centerleft>640</centerleft>
<top>400</top>
<height>50</height>
<width>350</width>
<align>center</align>
<font>font_MainMenu</font>
<textcolor>white</textcolor>
<shadowcolor>grey</shadowcolor>
</control>
<control type="image" id="4008">
<description>Channel icon</description>
<left>546</left>
<top>480</top>
<height>106</height>
<width>188</width>
<aspectratio>keep</aspectratio>
<texture fallback="default_channel.png">default_channel.png</texture>
<colordiffuse>FFFFFFFF</colordiffuse>
</control>
<control type="label" id="4006">
<description>Program start time</description>
<centerleft>640</centerleft>
<top>600</top>
<height>15</height>
<width>400</width>
<label>$INFO[System.Date(DDD)], $INFO[System.Date(d)] $INFO[System.Date(mmm)]., $INFO[System.Time]</label>
<font>font14</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="label" id="4000">
<description>Program title</description>
<left>-100</left>
<top>-100</top>
<height>0</height>
<width>0</width>
</control>
<control type="label">
<description>Program title</description>
<left>150</left>
<top>630</top>
<height>15</height>
<width>980</width>
<label>[UPPERCASE]$INFO[Control.GetLabel(4000)][/UPPERCASE]</label>
<font>font30_title</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="label" id="4001">
<description>Program playtime</description>
<left>10</left>
<top>670</top>
<height>15</height>
<width>110</width>
<label>1:34:11</label>
<font>font14</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="progress" id="4002">
<description>Program playtime progress bar</description>
<left>150</left>
<top>670</top>
<width>980</width>
<height>15</height>
<texturebg border="3" colordiffuse="60FFFFFF">white50.png</texturebg>
<midtexture>white50.png</midtexture>
</control>
<control type="slider" id="4003">
<description>Program playtime progress bar slider</description>
<left>155</left>
<top>665</top>
<width>980</width>
<height>25</height>
<texturesliderbar colordiffuse="00FFFFFF">slider_bar.png</texturesliderbar>
<textureslidernib colordiffuse="FF12A0C7">slider_bar.png</textureslidernib>
<textureslidernibfocus colordiffuse="FF12A0C7">slider_bar.png</textureslidernibfocus>
<onup>4200</onup>
</control>
<control type="label" id="4004">
<description>Program duration</description>
<left>1150</left>
<top>670</top>
<height>15</height>
<width>110</width>
<label>1:50:00</label>
<font>font14</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="button" id="4007">
<left>-100</left>
<top>-100</top>
<width>1</width>
<height>1</height>
<onup>4200</onup>
</control>
</control>
<control type="group">
<description>Group containing channelgroups, channels and program lists</description>
<left>0</left>
<top>0</top>
<height>720</height>
<width>1300</width>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
<visible allowhiddenfocus="true">
Control.HasFocus(4100)|Control.HasFocus(4200)|Control.HasFocus(4300)
</visible>
<control type="image">
<description>Background for whole group</description>
<height>720</height>
<width>1280</width>
<texture>black.png</texture>
<colordiffuse>A6FFFFFF</colordiffuse>
</control>
<control type="group">
<description>Channelgroup list</description>
<control type="image">
<description>List background</description>
<height>720</height>
<width>300</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
<control type="wraplist" id="4100">
<description>Channelgroups list</description>
<onright>4200</onright>
<orientation>vertical</orientation>
<autoscroll>false</autoscroll>
<focusposition>6</focusposition>
<scrolltime tween="sine" easing="out">200</scrolltime>
<itemlayout height="80" width="300">
<control type="image">
<description>Colored list item background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>20</width>
<texture>$INFO[ListItem.Property(icon)]</texture>
</control>
<control type="image">
<description>Overlay over colored list item background (unfocused)</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>20</width>
<texture>white50.png</texture>
<colordiffuse>FF000000</colordiffuse>
</control>
<control type="image">
<description>List item background</description>
<left>20</left>
<top>0</top>
<width>280</width>
<height>78</height>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
<!-- fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
<control type="label">
<description>Group name</description>
<left>48</left>
<top>1</top>
<width>252</width>
<height>80</height>
<font>font14</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(group_name)][/UPPERCASE]</label>
<!--
fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
</itemlayout>
<focusedlayout height="80" width="300">
<control type="image">
<description>Colored list item background for selected group without focus</description>
<left>0</left>
<top>1</top>
<width>20</width>
<height>77</height>
<texture>$INFO[ListItem.Property(icon)]</texture>
<visible>!Control.HasFocus(4100)</visible>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
</control>
<control type="image">
<description>Colored list item background for selected group</description>
<left>0</left>
<top>0</top>
<width>300</width>
<height>78</height>
<texture>$INFO[ListItem.Property(icon)]</texture>
<!-- fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
<control type="label">
<description>Group name</description>
<left>48</left>
<top>1</top>
<width>252</width>
<height>80</height>
<font>font14</font>
<aligny>center</aligny>
<textcolor>black</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(group_name)][/UPPERCASE]</label>
<!-- fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
</focusedlayout>
</control>
</control>
<control type="group">
<description>Container for channel and program list</description>
<left>20</left>
<top>0</top>
<height>720</height>
<width>500</width>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
<animation effect="slide" end="280,0" time="300" condition="Control.HasFocus(4100)">Conditional</animation>
<control type="group">
<description>Channel list</description>
<control type="image">
<description>Shadow</description>
<texture>separator-y.png</texture>
<left>-20</left>
<top>0</top>
<height>720</height>
<width>20</width>
<visible>Control.HasFocus(4200)|Control.HasFocus(4300)</visible>
</control>
<control type="image">
<description>List background</description>
<height>720</height>
<width>500</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="wraplist" id="4200">
<description>Channels list</description>
<onleft>4100</onleft>
<onright>4300</onright>
<orientation>vertical</orientation>
<autoscroll>false</autoscroll>
<focusposition>6</focusposition>
<scrolltime tween="sine" easing="out">200</scrolltime>
<itemlayout height="80" width="500">
<control type="image">
<description>List item background for channel icon</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>150</width>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
</control>
<control type="image">
<description>List item background for text</description>
<left>150</left>
<top>0</top>
<height>78</height>
<width>350</width>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="image">
<description>Channel icon</description>
<left>5</left>
<top>10</top>
<height>60</height>
<width>140</width>
<aspectratio>keep</aspectratio>
<texture fallback="default_channel.png">$INFO[ListItem.Property(icon)]</texture>
<colordiffuse>FFFFFFFF</colordiffuse>
</control>
<control type="image">
<description>Vertical bar (separator)</description>
<left>150</left>
<top>18</top>
<height>44</height>
<width>2</width>
<texture>white.png</texture>
<colordiffuse>33FFFFFF</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="label">
<description>Channel name</description>
<left>170</left>
<top>1</top>
<height>80</height>
<width>330</width>
<font>font14</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(channel_name)][/UPPERCASE]</label>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4100)</visible>
</control>
</itemlayout>
<focusedlayout height="80" width="500">
<control type="image">
<description>Icon background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>150</width>
<texture>listitem-bg-fo.png</texture>
<animation effect="fade" start="100" end="30" time="300"
condition="Control.HasFocus(4100)">Conditional</animation>
</control>
<control type="image">
<description>Text background</description>
<left>150</left>
<top>0</top>
<height>78</height>
<width>350</width>
<texture>listitem-bg-fo.png</texture>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
<animation effect="fade" start="100" end="30" time="300"
condition="Control.HasFocus(4100)">Conditional</animation>
</control>
<control type="image">
<description>Icon</description>
<left>5</left>
<top>10</top>
<height>60</height>
<width>140</width>
<aspectratio>keep</aspectratio>
<texture fallback="default_channel.png">$INFO[ListItem.Property(icon)]</texture>
<colordiffuse>FFFFFFFF</colordiffuse>
</control>
<control type="image">
<description>Vertical bar</description>
<left>150</left>
<top>18</top>
<height>44</height>
<width>2</width>
<texture>black.png</texture>
<colordiffuse>33FFFFFF</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="label">
<description>Label</description>
<left>170</left>
<top>1</top>
<height>80</height>
<width>330</width>
<font>font14</font>
<aligny>center</aligny>
<textcolor>black</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(channel_name)][/UPPERCASE]</label>
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
<animation effect="fade" start="0" end="100" time="300"
condition="Control.HasFocus(4200)">Conditional</animation>
</control>
<control type="label">
<description>Label</description>
<left>170</left>
<top>1</top>
<height>80</height>
<width>330</width>
<font>font14</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(channel_name)][/UPPERCASE]</label>
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="300"
condition="Control.HasFocus(4200)">Conditional</animation>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4100)</visible>
</control>
<control type="button">
<left>-100</left>
<top>-100</top>
<height>0</height>
<width>0</width>
<onclick>-</onclick>
<onfocus>Skin.SetString(channel_id, 481)</onfocus>
</control>
</focusedlayout>
</control>
</control>
<control type="group">
<description>Program list</description>
<left>150</left>
<top>0</top>
<height>720</height>
<width>500</width>
<!-- move over channel list on focus -->
<animation effect="slide" end="350,0" time="300"
condition="!Control.HasFocus(4300)">Conditional</animation>
<!-- hide program list if focus is on channelgroup list -->
<visible allowhiddenfocus="true">
[Control.IsVisible(4200)|Control.IsVisible(4300)]
+String.IsEqual(Container(4200).ListItem.Property(cid), Container(4300).ListItem.Property(cid))
</visible>
<animation type="Visible">
<effect type="fade" start="0" end="100" time="300"/>
</animation>
<animation type="Hidden">
<effect type="fade" start="100" end="0" time="300"/>
</animation>
<control type="image">
<description>Shadow</description>
<texture>separator-y.png</texture>
<left>-20</left>
<top>0</top>
<width>20</width>
<height>720</height>
<visible>Control.HasFocus(4300)</visible>
</control>
<control type="image">
<description>List background</description>
<height>720</height>
<width>500</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
</control>
<control type="wraplist" id="4300">
<onleft>4200</onleft>
<onright>999</onright>
<orientation>vertical</orientation>
<autoscroll>false</autoscroll>
<focusposition>6</focusposition>
<movement>5</movement>
<scrolltime tween="sine" easing="out">200</scrolltime>
<itemlayout height="80" width="500">
<control type="image">
<description>List item background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>500</width>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
</control>
<control type="image">
<description>day color bar</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>2</width>
<texture>$INFO[Listitem.Property(day_color)]</texture>
</control>
<control type="label">
<description>Program start time</description>
<left>12</left>
<top>2</top>
<height>80</height>
<width>70</width>
<font>font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<info>ListItem.Property(t_start)</info>
</control>
<control type="image">
<description>Program status icon (Archive, Past, Live)</description>
<left>28</left>
<top>37</top>
<height>25</height>
<width>25</width>
<texture>$INFO[Listitem.Property(status)]</texture>
</control>
<control type="textbox">
<description>Program title</description>
<left>85</left>
<top>2</top>
<height>76</height>
<width>290</width>
<font>font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<wrapmultiline>true</wrapmultiline>
<label>$INFO[ListItem.Property(title_list)]</label>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4200)</visible>
</control>
<control type="image">
<description>Program image</description>
<right>10</right>
<width>90</width>
<texture>$INFO[ListItem.Property(img_s)]</texture>
<aspectratio>keep</aspectratio>
</control>
</itemlayout>
<focusedlayout height="80" width="500">
<control type="image">
<description>List item background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>500</width>
<texture>listitem-bg-fo.png</texture>
<animation effect="fade" start="100" end="30" time="300"
condition="!Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="image">
<description>day color bar</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>2</width>
<texture>$INFO[Listitem.Property(day_color)]</texture>
<animation effect="fade" start="100" end="30" time="300"
condition="!Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="label">
<description>Selected Program start time (on focus)</description>
<left>12</left>
<top>2</top>
<height>80</height>
<width>70</width>
<font>font14</font>
<aligny>top</aligny>
<textcolor>black</textcolor>
<align>left</align>
<info>ListItem.Property(t_start)</info>
<animation effect="fade" start="0" end="100" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="image">
<description>Program status icon (Archive, Past, Live)</description>
<left>28</left>
<top>37</top>
<height>25</height>
<width>25</width>
<texture>$INFO[Listitem.Property(status)]</texture>
</control>
<control type="label">
<description>Selected Program start time</description>
<left>12</left>
<top>2</top>
<height>80</height>
<width>70</width>
<font>font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<info>ListItem.Property(t_start)</info>
<animation effect="fade" start="100" end="0" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="textbox">
<description>Selected Program title on focus</description>
<left>85</left>
<top>2</top>
<height>76</height>
<width>290</width>
<font>font14</font>
<aligny>top</aligny>
<textcolor>black</textcolor>
<align>left</align>
<wrapmultiline>true</wrapmultiline>
<label>$INFO[ListItem.Property(title_list)]</label>
<animation effect="fade" start="0" end="100" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="textbox">
<description>Selected Program title</description>
<left>85</left>
<top>2</top>
<height>76</height>
<width>290</width>
<font>font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<wrapmultiline>true</wrapmultiline>
<label>$INFO[ListItem.Property(title_list)]</label>
<animation effect="fade" start="100" end="0" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4200)</visible>
</control>
<control type="image">
<description>Program image</description>
<right>10</right>
<width>90</width>
<texture>$INFO[ListItem.Property(img_s)]</texture>
<aspectratio>keep</aspectratio>
</control>
</focusedlayout>
</control>
<control type="group">
<description>Detailed information of the selected program</description>
<left>500</left>
<top>0</top>
<height>720</height>
<width>780</width>
<visible>Control.HasFocus(4300)</visible>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
<control type="image">
<description>List background</description>
<height>720</height>
<width>780</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
</control>
<control type="image">
<description>Program status icon (Archive, Past, Live)</description>
<left>30</left>
<top>55</top>
<height>25</height>
<width>25</width>
<texture>$INFO[Container(4300).ListItem.Property(status)]</texture>
</control>
<control type="label">
<description>Program start time and date</description>
<left>28</left>
<top>55</top>
<height>30</height>
<width>500</width>
<font>font12</font>
<label> [UPPERCASE]$INFO[Container(4300).ListItem.Property(t_start)] - $INFO[Container(4300).ListItem.Property(t_end)], $INFO[Container(4300).ListItem.Property(d_start)][/UPPERCASE]</label>
</control>
<control type="textbox">
<description>Program Information</description>
<left>30</left>
<top>100</top>
<height>215</height>
<width>500</width>
<font>font14</font>
<wrapmultiline>true</wrapmultiline>
<autoscroll delay="10000" time="1000" repeat="10000">true</autoscroll>
<label>[B]$INFO[Container(4300).ListItem.Property(title)][/B][CR]$INFO[Container(4300).ListItem.Property(descr)]</label>
</control>
<control type="image">
<description>Program image</description>
<left>30</left>
<top>320</top>
<width>500</width>
<texture>$INFO[Container(4300).ListItem.Property(img_l)]</texture>
<aspectratio>keep</aspectratio>
</control>
</control>
</control>
</control>
</control>
</controls>
</window>

View File

@ -0,0 +1,848 @@
<?xml version="1.0" encoding="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.
-->
<window type="dialog">
<onload condition="String.IsEmpty(Window(home).Property(channel_id))">SetProperty(channel_id,0,home)</onload>
<onunload>ClearProperty(channel_id,home)</onunload>
<coordinates>
<system>1</system>
<left>0</left>
<top>0</top>
</coordinates>
<defaultcontrol always="true">999</defaultcontrol>
<controls>
<control type="button" id="999">
<description>Dummy control used to set focus to on video start</description>
<left>-100</left>
<top>-100</top>
<height>0</height>
<width>0</width>
<aligny>center</aligny>
<align>center</align>
<font>script.module.iptvlib-font_MainMenu</font>
<textcolor>black</textcolor>
<texturefocus>white.png</texturefocus>
<texturenofocus>white.png</texturenofocus>
<onclick>-</onclick>
<onup>4003</onup>
<ondown>4003</ondown>
<onleft>4003</onleft>
<onright>4003</onright>
</control>
<control type="image" id="4009">
<description>Dummy image used for preload of icons</description>
<left>-100</left>
<top>-100</top>
<height>1</height>
<width>1</width>
<aspectratio>keep</aspectratio>
</control>
<!-- OSD for current playing program -->
<control type="group">
<description>OSD for current playing program</description>
<left>0</left>
<top>0</top>
<height>720</height>
<width>1280</width>
<visible allowhiddenfocus="true">Control.HasFocus(4003)</visible>
<animation type="Visible">
<effect type="fade" start="0" end="100" time="300"/>
<effect type="slide" start="0,200" end="0,0" time="300" tween="cubic" easing="out"/>
</animation>
<animation type="Hidden">
<effect type="fade" start="100" end="0" time="300"/>
<effect type="slide" start="0,0" end="0,200" time="300" tween="cubic" easing="in"/>
</animation>
<control type="image">
<description>Background</description>
<left>0</left>
<top>0</top>
<width>1280</width>
<height>720</height>
<texture>fade.png</texture>
<fadetime>300</fadetime>
</control>
<control type="label" id="4005">
<description>Skip time within playback</description>
<visible>!String.StartsWith(Control.GetLabel(4005),0 )</visible>
<centerleft>640</centerleft>
<top>400</top>
<height>50</height>
<width>350</width>
<align>center</align>
<font>script.module.iptvlib-font_MainMenu</font>
<textcolor>white</textcolor>
<shadowcolor>grey</shadowcolor>
</control>
<control type="image" id="4008">
<description>Channel icon</description>
<left>546</left>
<top>480</top>
<height>106</height>
<width>188</width>
<aspectratio>keep</aspectratio>
<texture fallback="default_channel.png">default_channel.png</texture>
<colordiffuse>FFFFFFFF</colordiffuse>
</control>
<control type="label" id="4006">
<description>Program start time</description>
<centerleft>640</centerleft>
<top>600</top>
<height>15</height>
<width>400</width>
<label>$INFO[System.Date(DDD)], $INFO[System.Date(d)] $INFO[System.Date(mmm)]., $INFO[System.Time]</label>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="label" id="4000">
<description>Program title</description>
<left>-100</left>
<top>-100</top>
<height>0</height>
<width>0</width>
</control>
<control type="label">
<description>Program title</description>
<left>150</left>
<top>630</top>
<height>15</height>
<width>980</width>
<label>[UPPERCASE]$INFO[Control.GetLabel(4000)][/UPPERCASE]</label>
<font>script.module.iptvlib-font30_title</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="label" id="4001">
<description>Program playtime</description>
<left>10</left>
<top>670</top>
<height>15</height>
<width>110</width>
<label>1:34:11</label>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="progress" id="4002">
<description>Program playtime progress bar</description>
<left>150</left>
<top>670</top>
<width>980</width>
<height>15</height>
<texturebg border="3" colordiffuse="60FFFFFF">white50.png</texturebg>
<midtexture>white50.png</midtexture>
</control>
<control type="slider" id="4003">
<description>Program playtime progress bar slider</description>
<left>155</left>
<top>665</top>
<width>980</width>
<height>25</height>
<texturesliderbar colordiffuse="00FFFFFF">slider_bar.png</texturesliderbar>
<textureslidernib colordiffuse="FF12A0C7">slider_bar.png</textureslidernib>
<textureslidernibfocus colordiffuse="FF12A0C7">slider_bar.png</textureslidernibfocus>
<onup>4200</onup>
</control>
<control type="label" id="4004">
<description>Program duration</description>
<left>1150</left>
<top>670</top>
<height>15</height>
<width>110</width>
<label>1:50:00</label>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<align>center</align>
</control>
<control type="button" id="4007">
<left>-100</left>
<top>-100</top>
<width>1</width>
<height>1</height>
<onup>4200</onup>
</control>
</control>
<control type="group">
<description>Group containing channelgroups, channels and program lists</description>
<left>0</left>
<top>0</top>
<height>720</height>
<width>1300</width>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
<visible allowhiddenfocus="true">
Control.HasFocus(4100)|Control.HasFocus(4200)|Control.HasFocus(4300)
</visible>
<control type="image">
<description>Background for whole group</description>
<height>720</height>
<width>1280</width>
<texture>black.png</texture>
<colordiffuse>A6FFFFFF</colordiffuse>
</control>
<control type="group">
<description>Channelgroup list</description>
<control type="image">
<description>List background</description>
<height>720</height>
<width>300</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
<control type="wraplist" id="4100">
<description>Channelgroups list</description>
<onright>4200</onright>
<orientation>vertical</orientation>
<autoscroll>false</autoscroll>
<focusposition>6</focusposition>
<scrolltime tween="sine" easing="out">200</scrolltime>
<itemlayout height="80" width="300">
<control type="image">
<description>Colored list item background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>20</width>
<texture>$INFO[ListItem.Property(icon)]</texture>
</control>
<control type="image">
<description>Overlay over colored list item background (unfocused)</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>20</width>
<texture>white50.png</texture>
<colordiffuse>FF000000</colordiffuse>
</control>
<control type="image">
<description>List item background</description>
<left>20</left>
<top>0</top>
<width>280</width>
<height>78</height>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
<!-- fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
<control type="label">
<description>Group name</description>
<left>48</left>
<top>1</top>
<width>252</width>
<height>80</height>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(group_name)][/UPPERCASE]</label>
<!--
fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
</itemlayout>
<focusedlayout height="80" width="300">
<control type="image">
<description>Colored list item background for selected group without focus</description>
<left>0</left>
<top>1</top>
<width>20</width>
<height>77</height>
<texture>$INFO[ListItem.Property(icon)]</texture>
<visible>!Control.HasFocus(4100)</visible>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
</control>
<control type="image">
<description>Colored list item background for selected group</description>
<left>0</left>
<top>0</top>
<width>300</width>
<height>78</height>
<texture>$INFO[ListItem.Property(icon)]</texture>
<!-- fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
<control type="label">
<description>Group name</description>
<left>48</left>
<top>1</top>
<width>252</width>
<height>80</height>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<textcolor>black</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(group_name)][/UPPERCASE]</label>
<!-- fade out if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200" condition="Control.HasFocus(4100)">
Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200" condition="!Control.HasFocus(4100)">
Conditional
</animation>
</control>
</focusedlayout>
</control>
</control>
<control type="group">
<description>Container for channel and program list</description>
<left>20</left>
<top>0</top>
<height>720</height>
<width>500</width>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
<animation effect="slide" end="280,0" time="300" condition="Control.HasFocus(4100)">Conditional</animation>
<control type="group">
<description>Channel list</description>
<control type="image">
<description>Shadow</description>
<texture>separator-y.png</texture>
<left>-20</left>
<top>0</top>
<height>720</height>
<width>20</width>
<visible>Control.HasFocus(4200)|Control.HasFocus(4300)</visible>
</control>
<control type="image">
<description>List background</description>
<height>720</height>
<width>500</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="wraplist" id="4200">
<description>Channels list</description>
<onleft>4100</onleft>
<onright>4300</onright>
<orientation>vertical</orientation>
<autoscroll>false</autoscroll>
<focusposition>6</focusposition>
<scrolltime tween="sine" easing="out">200</scrolltime>
<itemlayout height="80" width="500">
<control type="image">
<description>List item background for channel icon</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>150</width>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
</control>
<control type="image">
<description>List item background for text</description>
<left>150</left>
<top>0</top>
<height>78</height>
<width>350</width>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="image">
<description>Channel icon</description>
<left>5</left>
<top>10</top>
<height>60</height>
<width>140</width>
<aspectratio>keep</aspectratio>
<texture fallback="default_channel.png">$INFO[ListItem.Property(icon)]</texture>
<colordiffuse>FFFFFFFF</colordiffuse>
</control>
<control type="image">
<description>Vertical bar (separator)</description>
<left>150</left>
<top>18</top>
<height>44</height>
<width>2</width>
<texture>white.png</texture>
<colordiffuse>33FFFFFF</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="label">
<description>Channel name</description>
<left>170</left>
<top>1</top>
<height>80</height>
<width>330</width>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(channel_name)][/UPPERCASE]</label>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4100)</visible>
</control>
</itemlayout>
<focusedlayout height="80" width="500">
<control type="image">
<description>Icon background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>150</width>
<texture>listitem-bg-fo.png</texture>
<animation effect="fade" start="100" end="30" time="300"
condition="Control.HasFocus(4100)">Conditional</animation>
</control>
<control type="image">
<description>Text background</description>
<left>150</left>
<top>0</top>
<height>78</height>
<width>350</width>
<texture>listitem-bg-fo.png</texture>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
<animation effect="fade" start="100" end="30" time="300"
condition="Control.HasFocus(4100)">Conditional</animation>
</control>
<control type="image">
<description>Icon</description>
<left>5</left>
<top>10</top>
<height>60</height>
<width>140</width>
<aspectratio>keep</aspectratio>
<texture fallback="default_channel.png">$INFO[ListItem.Property(icon)]</texture>
<colordiffuse>FFFFFFFF</colordiffuse>
</control>
<control type="image">
<description>Vertical bar</description>
<left>150</left>
<top>18</top>
<height>44</height>
<width>2</width>
<texture>black.png</texture>
<colordiffuse>33FFFFFF</colordiffuse>
<!-- fade out list background if focus changes to other control -->
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
</control>
<control type="label">
<description>Label</description>
<left>170</left>
<top>1</top>
<height>80</height>
<width>330</width>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<textcolor>black</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(channel_name)][/UPPERCASE]</label>
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
<animation effect="fade" start="0" end="100" time="300"
condition="Control.HasFocus(4200)">Conditional</animation>
</control>
<control type="label">
<description>Label</description>
<left>170</left>
<top>1</top>
<height>80</height>
<width>330</width>
<font>script.module.iptvlib-font14</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<align>left</align>
<label>[UPPERCASE]$INFO[ListItem.Property(channel_name)][/UPPERCASE]</label>
<animation effect="fade" start="0" end="100" time="200"
condition="Control.HasFocus(4100)|Control.HasFocus(4200)">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="200"
condition="![Control.HasFocus(4100)|Control.HasFocus(4200)]">Conditional
</animation>
<animation effect="fade" start="100" end="0" time="300"
condition="Control.HasFocus(4200)">Conditional</animation>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4100)</visible>
</control>
<control type="button">
<left>-100</left>
<top>-100</top>
<height>0</height>
<width>0</width>
<onclick>-</onclick>
<onfocus>Skin.SetString(channel_id, 481)</onfocus>
</control>
</focusedlayout>
</control>
</control>
<control type="group">
<description>Program list</description>
<left>150</left>
<top>0</top>
<height>720</height>
<width>500</width>
<!-- move over channel list on focus -->
<animation effect="slide" end="350,0" time="300"
condition="!Control.HasFocus(4300)">Conditional</animation>
<!-- hide program list if focus is on channelgroup list -->
<visible allowhiddenfocus="true">
[Control.IsVisible(4200)|Control.IsVisible(4300)]
+String.IsEqual(Container(4200).ListItem.Property(cid), Container(4300).ListItem.Property(cid))
</visible>
<animation type="Visible">
<effect type="fade" start="0" end="100" time="300"/>
</animation>
<animation type="Hidden">
<effect type="fade" start="100" end="0" time="300"/>
</animation>
<control type="image">
<description>Shadow</description>
<texture>separator-y.png</texture>
<left>-20</left>
<top>0</top>
<width>20</width>
<height>720</height>
<visible>Control.HasFocus(4300)</visible>
</control>
<control type="image">
<description>List background</description>
<height>720</height>
<width>500</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
</control>
<control type="wraplist" id="4300">
<onleft>4200</onleft>
<onright>999</onright>
<orientation>vertical</orientation>
<autoscroll>false</autoscroll>
<focusposition>6</focusposition>
<movement>5</movement>
<scrolltime tween="sine" easing="out">200</scrolltime>
<itemlayout height="80" width="500">
<control type="image">
<description>List item background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>500</width>
<texture>listitem-bg.png</texture>
<colordiffuse>FF000000</colordiffuse>
</control>
<control type="image">
<description>day color bar</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>2</width>
<texture>$INFO[Listitem.Property(day_color)]</texture>
</control>
<control type="label">
<description>Program start time</description>
<left>12</left>
<top>2</top>
<height>80</height>
<width>70</width>
<font>script.module.iptvlib-font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<info>ListItem.Property(t_start)</info>
</control>
<control type="image">
<description>Program status icon (Archive, Past, Live)</description>
<left>28</left>
<top>37</top>
<height>25</height>
<width>25</width>
<texture>$INFO[Listitem.Property(status)]</texture>
</control>
<control type="textbox">
<description>Program title</description>
<left>85</left>
<top>2</top>
<height>76</height>
<width>290</width>
<font>script.module.iptvlib-font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<wrapmultiline>true</wrapmultiline>
<label>$INFO[ListItem.Property(title_list)]</label>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4200)</visible>
</control>
<control type="image">
<description>Program image</description>
<right>10</right>
<width>90</width>
<texture>$INFO[ListItem.Property(img_s)]</texture>
<aspectratio>keep</aspectratio>
</control>
</itemlayout>
<focusedlayout height="80" width="500">
<control type="image">
<description>List item background</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>500</width>
<texture>listitem-bg-fo.png</texture>
<animation effect="fade" start="100" end="30" time="300"
condition="!Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="image">
<description>day color bar</description>
<left>0</left>
<top>0</top>
<height>78</height>
<width>2</width>
<texture>$INFO[Listitem.Property(day_color)]</texture>
<animation effect="fade" start="100" end="30" time="300"
condition="!Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="label">
<description>Selected Program start time (on focus)</description>
<left>12</left>
<top>2</top>
<height>80</height>
<width>70</width>
<font>script.module.iptvlib-font14</font>
<aligny>top</aligny>
<textcolor>black</textcolor>
<align>left</align>
<info>ListItem.Property(t_start)</info>
<animation effect="fade" start="0" end="100" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="image">
<description>Program status icon (Archive, Past, Live)</description>
<left>28</left>
<top>37</top>
<height>25</height>
<width>25</width>
<texture>$INFO[Listitem.Property(status)]</texture>
</control>
<control type="label">
<description>Selected Program start time</description>
<left>12</left>
<top>2</top>
<height>80</height>
<width>70</width>
<font>script.module.iptvlib-font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<info>ListItem.Property(t_start)</info>
<animation effect="fade" start="100" end="0" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="textbox">
<description>Selected Program title on focus</description>
<left>85</left>
<top>2</top>
<height>76</height>
<width>290</width>
<font>script.module.iptvlib-font14</font>
<aligny>top</aligny>
<textcolor>black</textcolor>
<align>left</align>
<wrapmultiline>true</wrapmultiline>
<label>$INFO[ListItem.Property(title_list)]</label>
<animation effect="fade" start="0" end="100" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="textbox">
<description>Selected Program title</description>
<left>85</left>
<top>2</top>
<height>76</height>
<width>290</width>
<font>script.module.iptvlib-font14</font>
<aligny>top</aligny>
<textcolor>white</textcolor>
<align>left</align>
<wrapmultiline>true</wrapmultiline>
<label>$INFO[ListItem.Property(title_list)]</label>
<animation effect="fade" start="100" end="0" time="300"
condition="Control.HasFocus(4300)">Conditional</animation>
</control>
<control type="image">
<description>Shadow</description>
<texture flipx="true">separator-y.png</texture>
<left>0</left>
<top>0</top>
<height>80</height>
<width>20</width>
<visible>Control.HasFocus(4200)</visible>
</control>
<control type="image">
<description>Program image</description>
<right>10</right>
<width>90</width>
<texture>$INFO[ListItem.Property(img_s)]</texture>
<aspectratio>keep</aspectratio>
</control>
</focusedlayout>
</control>
<control type="group">
<description>Detailed information of the selected program</description>
<left>500</left>
<top>0</top>
<height>720</height>
<width>780</width>
<visible>Control.HasFocus(4300)</visible>
<animation effect="fade" start="0" end="100" time="200">VisibleChange</animation>
<control type="image">
<description>List background</description>
<height>720</height>
<width>780</width>
<texture>listitem-bg.png</texture>
<colordiffuse>CC000000</colordiffuse>
</control>
<control type="image">
<description>Program status icon (Archive, Past, Live)</description>
<left>30</left>
<top>55</top>
<height>25</height>
<width>25</width>
<texture>$INFO[Container(4300).ListItem.Property(status)]</texture>
</control>
<control type="label">
<description>Program start time and date</description>
<left>28</left>
<top>55</top>
<height>30</height>
<width>500</width>
<font>script.module.iptvlib-font12</font>
<label> [UPPERCASE]$INFO[Container(4300).ListItem.Property(t_start)] - $INFO[Container(4300).ListItem.Property(t_end)], $INFO[Container(4300).ListItem.Property(d_start)][/UPPERCASE]</label>
</control>
<control type="textbox">
<description>Program Information</description>
<left>30</left>
<top>100</top>
<height>215</height>
<width>500</width>
<font>script.module.iptvlib-font14</font>
<wrapmultiline>true</wrapmultiline>
<autoscroll delay="10000" time="1000" repeat="10000">true</autoscroll>
<label>[B]$INFO[Container(4300).ListItem.Property(title)][/B][CR]$INFO[Container(4300).ListItem.Property(descr)]</label>
</control>
<control type="image">
<description>Program image</description>
<left>30</left>
<top>320</top>
<width>500</width>
<texture>$INFO[Container(4300).ListItem.Property(img_l)]</texture>
<aspectratio>keep</aspectratio>
</control>
</control>
</control>
</control>
</control>
</controls>
</window>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B