версия для Kodi 19
This commit is contained in:
1 parent
9aefb9451b
commit
5d2aff9bc5
60 files changed
+5831
No files matched your search
@@ -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.
|
||||
#
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user