# 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)