def next_path(group_id): _window = utils.get_active_window() if _window not in ['home', 'media'] and not label_warning_shown: _warn() group = manage.get_group_by_id(group_id) if not group: utils.log( '\"{}\" is missing, please repoint the widget to fix it.'.format( group_id), level=xbmc.LOGERROR) return False, 'AutoWidget' group_name = group.get('label', '') paths = manage.find_defined_paths(group_id) if len(paths) > 0: if _window == 'media': call_path(group_id, paths[0]['id']) return False, group_name else: directory.add_menu_item(title=32013, params={'mode': 'force'}, art=unpack, info={'plot': utils.get_string(32014)}, isFolder=False) return True, group_name else: directory.add_menu_item(title=32032, art=alert, isFolder=False) return False, group_name
def get_files_list(path, widget_id=None): hash = utils.path2hash(path) _, files, _ = utils.cache_expiry(hash, widget_id) if files is None: # We had no old content so have to block and get it now utils.log("Blocking cache path read: {}".format(hash[:5]), "info") files, changed = utils.cache_files(path, widget_id) new_files = [] if "error" not in files: files = files.get("result").get("files") if not files: utils.log("No items found for {}".format(path)) return for file in files: new_file = { k: v for k, v in file.items() if v not in [None, "", -1, [], {}] } if "art" in new_file: for art in new_file["art"]: new_file["art"][art] = utils.clean_artwork_url( file["art"][art]) new_files.append(new_file) return new_files
def _update_strings(widget_def): refresh = skin_string_pattern.format(widget_def['id'], 'refresh') utils.set_property(refresh, '{}'.format(time.time())) utils.log( 'Refreshing widget {} to display {}'.format(widget_def['id'], widget_def['path']), 'debug')
def backup(): choice = dialog.yesno('AutoWidget', utils.get_string(32094)) if choice: filename = dialog.input(utils.get_string(32095)) if not filename: dialog.notification('AutoWidget', utils.get_string(32096)) return if not os.path.exists(_backup_location): try: os.makedirs(_backup_location) except Exception as e: utils.log(str(e), 'error') dialog.notification('AutoWidget', utils.get_string(32097)) return files = [ x for x in os.listdir(utils._addon_path) if x.endswith('.group') ] if len(files) == 0: dialog.notification('AutoWidget', utils.get_string(32068)) return path = os.path.join(_backup_location, '{}.zip'.format(filename.replace('.zip', ''))) content = six.BytesIO() with zipfile.ZipFile(content, 'w', zipfile.ZIP_DEFLATED) as z: for file in files: with open(os.path.join(utils._addon_path, file), 'r') as f: z.writestr(file, six.ensure_text(f.read())) with open(path, 'wb') as f: f.write(content.getvalue())
def __init__(self): utils.log('+++++ STARTING AUTOWIDGET SERVICE +++++', level=xbmc.LOGNOTICE) self.player = xbmc.Player() utils.ensure_addon_data() self._update_properties() self._update_widgets()
def _update_strings(_id, path_def, setting=None, label_setting=None): if not path_def: return label = path_def['label'] action = path_def['path'] try: label = label.encode('utf-8') except: pass if setting: if label_setting: utils.log('Setting {} to {}'.format(label_setting, label)) utils.set_skin_string(label_setting, label) utils.log('Setting {} to {}'.format(setting, action)) utils.set_skin_string(setting, action) else: target = path_def['window'] label_string = skin_string_pattern.format(_id, 'label') action_string = skin_string_pattern.format(_id, 'action') target_string = skin_string_pattern.format(_id, 'target') utils.log('Setting {} to {}'.format(label_string, label)) utils.log('Setting {} to {}'.format(action_string, action)) utils.log('Setting {} to {}'.format(target_string, target)) utils.set_skin_string(label_string, label) utils.set_skin_string(action_string, action) utils.set_skin_string(target_string, target)
def edit_widget_dialog(widget_id): dialog = xbmcgui.Dialog() updated = False if advanced and not warning_shown: _warn() widget_def = manage.get_widget_by_id(widget_id) if not widget_def: return options = _get_widget_options(widget_def) remove_label = utils.get_string(32025) if widget_id else utils.get_string( 32023) options.append('[COLOR firebrick]{}[/COLOR]'.format(remove_label)) idx = dialog.select(utils.get_string(32048), options) if idx < 0: return elif idx == len(options) - 1: _remove_widget(widget_id) utils.update_container() return else: key = _clean_key(options[idx]) updated = _get_widget_value(widget_def, key) utils.log(updated, xbmc.LOGNOTICE) if updated: convert.save_path_details(widget_def, widget_id) utils.update_container() edit_widget_dialog(widget_id)
def get_files_list(path, titles=None, widget_id=None): if not titles: titles = [] hash = utils.path2hash(path) _, files, _ = utils.cache_expiry(hash, widget_id) if files is None: # We had no old content so have to block and get it now utils.log("Blocking cache path read: {}".format(hash[:5]), "info") files, changed = utils.cache_files(path, widget_id) new_files = [] if 'error' not in files: files = files.get('result').get('files') if not files: utils.log('No items found for {}'.format(path)) return filtered_files = [x for x in files if x['title'] not in titles] for file in filtered_files: new_file = { k: v for k, v in file.items() if v not in [None, '', -1, [], {}] } if 'art' in new_file: for art in new_file['art']: new_file['art'][art] = utils.clean_artwork_url( file['art'][art]) new_files.append(new_file) return new_files
def update_path(widget_id, target, path=None): widget_def = manage.get_widget_by_id(widget_id) if not widget_def: return stack = widget_def.get("stack", []) if target == "next" and path: utils.log("Next Page selected from {}".format(widget_id), "debug") path_def = manage.get_path_by_id(widget_def["path"], widget_def["group"]) if isinstance(path_def, dict): widget_def["label"] = path_def["label"] stack.append(path) widget_def["stack"] = stack elif target == "back" and widget_def.get("stack"): utils.log("Previous Page selected from {}".format(widget_id), "debug") widget_def["stack"] = widget_def["stack"][:-1] elif target == "reset": if len(stack) > 0: # simple compatibility with pre-3.3.0 widgets if isinstance(stack[0], dict): widget_def["path"] = stack[0].get("id", "") widget_def["stack"] = [] manage.save_path_details(widget_def) _update_strings(widget_def) utils.update_container(True) back_to_top(target)
def _update_strings(widget_def): refresh = skin_string_pattern.format(widget_def["id"], "refresh") utils.set_property(refresh, "{}".format(time.time())) utils.log( "Refreshing widget {} to display {}".format(widget_def["id"], widget_def["path"]), "debug", )
def __init__(self): """Starts all of the actions of AutoWidget's service.""" super(RefreshService, self).__init__() utils.log('+++++ STARTING AUTOWIDGET SERVICE +++++', 'notice') self.player = xbmc.Player() utils.ensure_addon_data() self._update_properties() self._clean_widgets() self._update_widgets()
def predict_update_frequency(history): if not history: return DEFAULT_CACHE_TIME update_count = 0 duration = 0 changes = [] last_when, last = history[0] for when, content in history[1:]: update_count += 1 if content == last: duration += when - last_when else: duration = ( +(when - last_when) / 2 ) # change could have happened any time inbetween changes.append((duration, update_count)) duration = 0 update_count = 0 last_when = when last = content if not changes and duration: # drop the last part of the history that hasn't changed yet unless we have no other history to work with # This is an underestimate as we aren't sure when in the future it will change changes.append((duration, update_count)) # TODO: the first change is potentially an underestimate too because we don't know how long it was unchanged for # before we started recording. # Now we have changes, we can do some trends on them. durations = [duration for duration, update_count in changes if update_count > 1] if not durations: return DEFAULT_CACHE_TIME med_dur = sorted(durations)[int(math.floor(len(durations) / 2)) - 1] avg_dur = sum(durations) / len(durations) # weighted by how many snapshots we took inbetween. # TODO: number of snapshots inbetween is really just increasing the confidence on the end time bot the duration as a whole. # so perhaps a better metric is the error margin of the duration? and not weighting by that completely. # ie durations with wide margin of error should be less important. e.g. times kodi was never turned on for months/weeks. weighted = sum([d * c for d, c in changes]) / sum([c for _, c in changes]) # TODO: also try exponential decay. Older durations are less important than newer ones. ones = len([c for d, c in changes if c == 1]) / float(len(changes)) # TODO: if many streaks with lots of counts then its stable and can predict utils.log( "avg_dur {:0.0f}s, med_dur {:0.0f}s, weighted {:0.0f}s, ones {:0.2f}, all {}".format( avg_dur, med_dur, weighted, ones, changes ), "debug", ) if ones > 0.9: # too unstable so no point guessing return DEFAULT_CACHE_TIME elif DEFAULT_CACHE_TIME > avg_dur / 2.0: # should not got less than 5min otherwise our updates go in a loop return DEFAULT_CACHE_TIME else: return ( avg_dur / 2.0 ) # we want to ensure we check more often than the actual predicted expiry
def group_menu(group_id): _window = utils.get_active_window() _id = uuid.uuid4() group_def = manage.get_group_by_id(group_id) if not group_def: utils.log( '"{}" is missing, please repoint the widget to fix it.'.format( group_id), "error", ) return False, "AutoWidget", None group_name = group_def["label"] group_type = group_def["type"] paths = group_def["paths"] content = group_def.get("content") if len(paths) > 0: utils.log( u"Showing {} group: {}".format(group_type, six.text_type(group_name)), "debug", ) cm = [] art = folder_shortcut if group_type == "shortcut" else folder_sync for idx, path_def in enumerate(paths): if _window == "media": cm = _create_path_context_items(group_id, path_def["id"], idx, len(paths), group_type) directory.add_menu_item( title=path_def["label"], params={ "mode": "path", "group": group_id, "path_id": path_def["id"] }, info=path_def["file"], art=path_def["file"].get("art", art), cm=cm, isFolder=False, ) if _window != "home": _create_action_items(group_def, _id) else: directory.add_menu_item( title=30019, art=utils.get_art("alert"), isFolder=False, props={"specialsort": "bottom"}, ) return True, group_name, content
def _log_params(_plugin, _handle, _params): msg = "[{}]" params = dict(parse_qsl(_params)) if params: msg = msg.format("][".join([" {}: {} ".format(p, params[p]) for p in params])) else: msg = msg.format(" root ") utils.log(msg, "info") return params
def _log_params(_plugin, _handle, _params): msg = '[{}]' params = dict(parse_qsl(_params)) if params: msg = msg.format(']['.join( [' {}: {} '.format(p, params[p]) for p in params])) else: msg = msg.format(' root ') utils.log(msg, 'info') return params
def __init__(self): """Starts all of the actions of AutoWidget's service.""" super(RefreshService, self).__init__() utils.log("+++++ STARTING AUTOWIDGET SERVICE +++++", "info") self.player = Player() utils.ensure_addon_data() self._update_properties() self._clean_widgets() self._update_widgets() # Shutting down. Close thread if _thread is not None: _thread.stop()
def get_group_by_id(group_id): if not group_id: return filename = '{}.group'.format(group_id) path = os.path.join(_addon_path, filename) try: group_def = utils.read_json(path) except ValueError: utils.log('Unable to parse: {}'.format(path)) return group_def
def chance_playback_updates_widget(history_path, plays, cutoff_time=60 * 5): cache_data = utils.read_json(history_path) history = cache_data.setdefault("history", []) # Complex version # - for each widget # - come up with chance it will update after a playback # - each pair of updates, is there a playback inbetween and updated with X min after playback # - num playback with change / num playback with no change changes, non_changes, unrelated_changes = 0, 0, 0 update = "" time_since_play = 0 for play_time, media_type in plays: while True: last_update = update if not history: break update_time, update = history.pop(0) time_since_play = update_time - play_time # log("{} {} {} {}".format(update[:5],last_update[:5], unrelated_changes, time_since_play), 'notice') if time_since_play > 0: break elif update != last_update: unrelated_changes += 1 if update == last_update: non_changes += 1 elif ( time_since_play > cutoff_time ): # update too long after playback to be releated pass else: changes += 1 # TODO: what if the previous update was a long time before playback? # There is probably a more statistically correct way of doing this but the idea is that # with few datapoints we should tend towards 0.5 probability but as we get more datapoints # then error goes down and rely on actual changes vs nonchanges # We will do a simple weighted average with 0.5 to simulate this # TODO: currently random widgets score higher than recently played widgets. need to score them lower # as they are less relevent utils.log( "changes={}, non_changes={}, unrelated_changes={}".format( changes, non_changes, unrelated_changes ), "debug", ) datapoints = float(changes + non_changes) prob = changes / float(changes + non_changes + unrelated_changes) unknown_weight = 4 prob = (prob * datapoints + 0.5 * unknown_weight) / (datapoints + unknown_weight) return prob
def _log_params(_plugin, _handle, _params): params = dict(parse_qsl(_params)) logstring = '' for param in params: logstring += '[ {0}: {1} ] '.format(param, params[param]) if not logstring: logstring = '[ Root Menu ]' utils.log(logstring, level=xbmc.LOGNOTICE) return params
def call_path(path_id): path_def = manage.get_path_by_id(path_id) if not path_def: return utils.call_builtin("Dialog.Close(busydialog)", 500) final_path = "" if path_def["target"] == "settings": final_path = "Addon.OpenSettings({})".format( path_def["file"]["file"] .replace("plugin://", "") .replace("script://", "") .split("/")[0] ) elif ( path_def["target"] == "shortcut" and path_def["file"]["filetype"] == "file" and path_def["content"] != "addons" ): if path_def["file"]["file"] == "addons://install/": final_path = "InstallFromZip" elif not path_def["content"] or path_def["content"] == "files": if path_def["file"]["file"].startswith("androidapp://sources/apps/"): final_path = "StartAndroidActivity({})".format( path_def["file"]["file"].replace("androidapp://sources/apps/", "") ) elif path_def["file"]["file"].startswith("pvr://"): final_path = "PlayMedia({})".format(path_def["file"]["file"]) else: final_path = "RunPlugin({})".format(path_def["file"]["file"]) elif ( all(i in path_def["file"]["file"] for i in ["(", ")"]) and "://" not in path_def["file"]["file"] ): final_path = path_def["file"]["file"] else: final_path = "PlayMedia({})".format(path_def["file"]["file"]) elif ( path_def["target"] == "widget" or path_def["file"]["filetype"] == "directory" or path_def["content"] == "addons" ): final_path = "ActivateWindow({},{},return)".format( path_def.get("window", "Videos"), path_def["file"]["file"] ) if final_path: utils.log("Calling path from {} using {}".format(path_id, final_path), "debug") utils.call_builtin(final_path)
def get_group_by_id(group_id): if not group_id: return {} filename = "{}.group".format(group_id) path = os.path.join(_addon_data, filename) try: group_def = utils.read_json(path) except ValueError: utils.log("Unable to parse: {}".format(path)) return return group_def
def group_menu(group_id): _window = utils.get_active_window() _id = uuid.uuid4() group_def = manage.get_group_by_id(group_id) if not group_def: utils.log( '\"{}\" is missing, please repoint the widget to fix it.'.format( group_id), 'error') return False, 'AutoWidget' group_name = group_def['label'] group_type = group_def['type'] paths = group_def['paths'] if len(paths) > 0: utils.log( u'Showing {} group: {}'.format(group_type, six.text_type(group_name)), 'debug') cm = [] art = folder_shortcut if group_type == 'shortcut' else folder_sync for idx, path_def in enumerate(paths): if _window == 'media': cm = _create_context_items(group_id, path_def['id'], idx, len(paths), group_type) directory.add_menu_item(title=path_def['label'], params={ 'mode': 'path', 'group': group_id, 'path_id': path_def['id'] }, info=path_def['file'], art=path_def['file']['art'] or art, cm=cm, isFolder=False) if _window != 'home': _create_action_items(group_def, _id) else: directory.add_menu_item(title=32032, art=utils.get_art('alert'), isFolder=False, props={'specialsort': 'bottom'}) return True, group_name
def edit_dialog(group_id, path_id=''): dialog = xbmcgui.Dialog() updated = False if advanced and not warning_shown: _warn() group_def = manage.get_group_by_id(group_id) if not group_def: return if path_id: path_def = manage.get_path_by_id(path_id, group_id) if not path_def: return edit_def = path_def if path_id else group_def options = _get_options(edit_def) remove_label = utils.get_string(32025) if path_id else utils.get_string( 32023) options.append('[COLOR firebrick]{}[/COLOR]'.format(remove_label)) idx = dialog.select(utils.get_string(32048), options) if idx < 0: return elif idx == len(options) - 1: if path_id: _remove_path(path_id, group_id) utils.update_container(group_def['type']) else: _remove_group(group_id) utils.update_container(group_def['type']) return else: key = _clean_key(options[idx]) updated = _get_value(edit_def, key) utils.log(updated, xbmc.LOGNOTICE) if updated: if path_id: manage.write_path(group_def, path_def=path_def, update=path_id) else: manage.write_path(group_def) utils.update_container(group_def['type']) edit_dialog(group_id, path_id)
def backup(): dialog = xbmcgui.Dialog() choice = dialog.yesno("AutoWidget", utils.get_string(30071)) if choice: filename = dialog.input(utils.get_string(30072)) if not filename: dialog.notification("AutoWidget", utils.get_string(30073)) del dialog return if not os.path.exists(_backup_location): try: os.makedirs(_backup_location) except Exception as e: utils.log(str(e), "error") dialog.notification("AutoWidget", utils.get_string(30074)) del dialog return files = [ x for x in os.listdir(_addon_data) if any( x.endswith(i) for i in [".group", ".widget", ".history", ".cache", ".log"] ) ] if len(files) == 0: dialog.notification("AutoWidget", utils.get_string(30046)) del dialog return path = os.path.join( _backup_location, "{}.zip".format(filename.replace(".zip", "")) ) content = six.BytesIO() with zipfile.ZipFile(content, "w", zipfile.ZIP_DEFLATED) as z: for file in files: with open(os.path.join(_addon_data, file), "r") as f: z.writestr(file, six.ensure_text(f.read())) with open(path, "wb") as f: f.write(content.getvalue()) del dialog
def cache_and_update(widget_ids): """a widget might have many paths. Ensure each path is either queued for an update or is expired and if so force it to be refreshed. When going through the queue this could mean we refresh paths that other widgets also use. These will then be skipped. """ assert widget_ids effected_widgets = set() for widget_id in widget_ids: widget_def = manage.get_widget_by_id(widget_id) if not widget_def: continue changed = False widget_path = widget_def.get("path", {}) utils.log( "trying to update {} with widget def {}".format( widget_id, widget_def), "inspect", ) if type(widget_path) != list: widget_path = [widget_path] for path in widget_path: if isinstance(path, dict): _label = path["label"] path = path["file"]["file"] hash = utils.path2hash(path) # TODO: we might be updating paths used by widgets that weren't initiall queued. # We need to return those and ensure they get refreshed also. effected_widgets = effected_widgets.union( utils.widgets_for_path(path)) if utils.is_cache_queue(hash): # we need to update this path regardless new_files, files_changed = utils.cache_files(path, widget_id) changed = changed or files_changed utils.remove_cache_queue(hash) # else: # # double check this hasn't been updated already when updating another widget # expiry, _ = utils.cache_expiry(hash, widget_id, no_queue=True) # if expiry <= time.time(): # utils.cache_files(path, widget_id) # else: # pass # Skipping this path because its already been updated # TODO: only need to do that if a path has changed which we can tell from the history if changed: _update_strings(widget_def) return effected_widgets
def find_defined_paths(group_id=None): paths = [] if group_id: filename = '{}.group'.format(group_id) path = os.path.join(_addon_path, filename) try: group_def = utils.read_json(path) except ValueError: utils.log('Unable to parse: {}'.format(path)) if group_def: return group_def.get('paths', []) else: for group in find_defined_groups(): paths.append(find_defined_paths(group_id=group.get('id'))) return paths
def call_path(path_id): path_def = manage.get_path_by_id(path_id) if not path_def: return utils.call_builtin('Dialog.Close(busydialog)', 500) final_path = '' if path_def['target'] == 'shortcut' and path_def['file']['filetype'] == 'file' \ and path_def['content'] != 'addons': if path_def['file']['file'] == 'addons://install/': final_path = 'InstallFromZip' elif not path_def['content'] or path_def['content'] == 'files': if path_def['file']['file'].startswith( 'androidapp://sources/apps/'): final_path = 'StartAndroidActivity({})'.format( path_def['file']['file'].replace( 'androidapp://sources/apps/', '')) elif path_def['file']['file'].startswith('pvr://'): final_path = 'PlayMedia({})'.format(path_def['file']['file']) else: final_path = 'RunPlugin({})'.format(path_def['file']['file']) elif all( i in path_def['file']['file'] for i in ['(', ')']) and '://' not in path_def['file']['file']: final_path = path_def['file']['file'] else: final_path = 'PlayMedia({})'.format(path_def['file']['file']) elif path_def['target'] == 'widget' or path_def['file']['filetype'] == 'directory' \ or path_def['content'] == 'addons': final_path = 'ActivateWindow({},{},return)'.format( path_def.get('window', 'Videos'), path_def['file']['file']) elif path_def['target'] == 'settings': final_path = 'Addon.OpenSettings({})'.format( path_def['file']['file'].replace('plugin://', '')) if final_path: utils.log('Calling path from {} using {}'.format(path_id, final_path), 'debug') utils.call_builtin(final_path) return False, path_def['label']
def widgets_changed_by_watching(media_type): # Predict which widgets the skin might have that could have changed based on recently finish # watching something all_cache = filter( os.path.isfile, glob.glob(os.path.join(_addon_data, "*.history")) ) # Simple version. Anything updated recently (since startup?) # priority = sorted(all_cache, key=os.path.getmtime) # Sort by chance of it updating plays = utils.read_json(_playback_history_path, default={}).setdefault("plays", []) plays_for_type = [(time, t) for time, t in plays if t == media_type] priority = sorted( [ ( chance_playback_updates_widget(path, plays_for_type), utils.read_json(path).get("path", ""), path, ) for path in all_cache ], reverse=True, ) for chance, path, history_path in priority: hash = path2hash(path) last_update = os.path.getmtime(history_path) - _startup_time if last_update < 0: utils.log( "widget not updated since startup {} {}".format(last_update, hash[:5]), "notice", ) # elif chance < 0.3: # log("chance widget changed after play {}% {}".format(chance, hash[:5]), 'notice') else: utils.log( "chance widget changed after play {}% {}".format(chance, hash[:5]), "notice", ) yield hash, path
def get_inactive_path(paths): active_widgets = sorted(clean(all=True), key=lambda x: x.get('updated', 0), reverse=True) active_paths = [] inactive_path_ids = [] if len(active_widgets) > 0: for active_widget in active_widgets: active_paths.append(active_widget.get('path', {})) for index, path in enumerate(paths, start=0): if path not in active_paths: inactive_path_ids.append(index) utils.log(path, 'info') utils.log('inactive path: ' + path['label'], 'info') if not inactive_path_ids: return [] else: return random.choice(inactive_path_ids)
def find_defined_groups(_type=''): groups = [] for filename in [ x for x in os.listdir(_addon_path) if x.endswith('.group') ]: path = os.path.join(_addon_path, filename) try: group_def = utils.read_json(path) except ValueError: utils.log('Unable to parse: {}'.format(path)) if group_def: if _type: if group_def['type'] == _type: groups.append(group_def) else: groups.append(group_def) return groups