Skip to content

Commit 81ad3db

Browse files
authored
Merge pull request #108 from drinfernoo/reduce_updates
optimise to do less background updates, more efficiently, with less corruptions and more quickly if you have more RAM
2 parents 627c795 + f06160e commit 81ad3db

4 files changed

Lines changed: 259 additions & 212 deletions

File tree

plugin.program.autowidget/resources/lib/backup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def restore():
9999
utils.get_string(30074),
100100
"",
101101
mask=".zip",
102-
defaultt=utils.translate_path(_backup_location),
102+
defaultt=utils.translatePath(_backup_location),
103103
)
104104

105105
if backup.endswith(".zip"):

plugin.program.autowidget/resources/lib/common/cache.py

Lines changed: 91 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import threading
12
import xbmcgui
23
import xbmcvfs
4+
import xbmc
35

46
import glob
57
import hashlib
68
import json
79
import math
810
import os
911
import time
12+
import random
1013

1114
import six
1215

@@ -46,57 +49,22 @@ def hash_from_cache_path(path):
4649
return os.path.splitext(base)[0]
4750

4851

49-
def iter_queue():
50-
queued = [
51-
os.path.join(_addon_data, x)
52-
for x in xbmcvfs.listdir(_addon_data)[1]
53-
if x.endswith(".queue")
54-
]
55-
# TODO: sort by path instead so load plugins at the same time
56-
57-
for path in sorted(queued, key=lambda x: xbmcvfs.Stat(x).st_mtime()):
58-
queue_data = utils.read_json(path)
59-
yield queue_data.get("path", "")
60-
61-
6252
def read_history(path, create_if_missing=True):
6353
hash = path2hash(path)
6454
history_path = os.path.join(_addon_data, "{}.history".format(hash))
6555
if not xbmcvfs.exists(history_path):
6656
if create_if_missing:
67-
cache_data = {}
68-
history = cache_data.setdefault("history", [])
69-
widgets = cache_data.setdefault("widgets", [])
57+
cache_data = dict(history=[], widgets=[])
7058
utils.write_json(history_path, cache_data)
7159
else:
7260
cache_data = None
7361
else:
74-
cache_data = utils.read_json(history_path)
62+
cache_data = utils.read_json(history_path, default=dict(history=[], widgets=[]))
7563
return cache_data
7664

7765

78-
def next_cache_queue():
79-
# Simple queue by creating a .queue file
80-
# TODO: use watchdog to use less resources
81-
for path in iter_queue():
82-
# TODO: sort by path instead so load plugins at the same time
83-
hash = path2hash(path)
84-
queue_path = os.path.join(_addon_data, "{}.queue".format(hash))
85-
if not xbmcvfs.exists(queue_path):
86-
# a widget update has already taken care of updating this path
87-
continue
88-
# We will let the update operation remove the item from the queue
89-
90-
# TODO: need to workout if a blocking write is happen while it was queued or right now.
91-
# probably need a .lock file to ensure foreground calls can get priority.
92-
cache_data = read_history(path, create_if_missing=True)
93-
widget_id = utils.read_json(queue_path).get("widget_id", None)
94-
yield path, cache_data, widget_id
95-
96-
9766
def push_cache_queue(path, widget_id=None):
9867
hash = path2hash(path)
99-
queue_path = os.path.join(_addon_data, "{}.queue".format(hash))
10068
history = read_history(path, create_if_missing=True) # Ensure its created
10169
changed = False
10270
if widget_id is not None and widget_id not in history["widgets"]:
@@ -109,22 +77,18 @@ def push_cache_queue(path, widget_id=None):
10977
history_path = os.path.join(_addon_data, "{}.history".format(hash))
11078
utils.write_json(history_path, history)
11179

112-
if xbmcvfs.exists(queue_path):
113-
pass # Leave original modification date so item is higher priority
114-
else:
115-
utils.write_json(
116-
queue_path, {"hash": hash, "path": path, "widget_id": widget_id}
117-
)
118-
119-
120-
def is_cache_queue(hash):
121-
queue_path = os.path.join(_addon_data, "{}.queue".format(hash))
122-
return xbmcvfs.exists(queue_path)
123-
124-
125-
def remove_cache_queue(hash):
126-
queue_path = os.path.join(_addon_data, "{}.queue".format(hash))
127-
utils.remove_file(queue_path)
80+
command = {'jsonrpc': '2.0', 'method': 'JSONRPC.NotifyAll',
81+
'params': {'sender': "AutoWidget",
82+
'message': "queue",
83+
'data': (hash, path, widget_id),
84+
},
85+
'id': 1,}
86+
def send():
87+
while not utils.call_jsonrpc(command):
88+
xbmc.sleep(1000) # Wait untl service starts
89+
# Don't wait in case service hasn't started
90+
# TODO: check this doesn't still block until thread finishes
91+
threading.Thread(target=send).start()
12892

12993

13094
def path2hash(path):
@@ -154,16 +118,15 @@ def cache_and_update(path, widget_id, cache_data, notify=None):
154118
assert widget_id in cache_data["widgets"]
155119

156120
hash = path2hash(path)
157-
if not is_cache_queue(hash):
158-
return []
121+
# if not is_cache_queue(hash):
122+
# return []
159123

160124
if notify is not None:
161125
widget_def = manage.get_widget_by_id(widget_id)
162126
if widget_def is not None:
163127
notify(widget_def.get("label", ""), path)
164128

165129
new_files, files_changed = cache_files(path, widget_id)
166-
remove_cache_queue(hash)
167130

168131
# TODO: this is all widgets that ever requested this path. do we
169132
# need to update all of them?
@@ -259,9 +222,6 @@ def cache_expiry(path, widget_id, add=None, background=True):
259222
)
260223
push_cache_queue(path)
261224
else:
262-
# write any updated widget_ids so we know what to update when we dequeue
263-
# Also important as wwe use last modified of .history as accessed time
264-
utils.write_json(history_path, cache_data)
265225
size = len(json.dumps(contents))
266226
if history:
267227
expiry = history[-1][0] + predict_update_frequency(history)
@@ -371,79 +331,104 @@ def widgets_changed_by_watching(media_type):
371331
# Predict which widgets the skin might have that could have changed based on recently finish
372332
# watching something
373333

374-
all_cache = [
334+
all_hist = [
375335
os.path.join(_addon_data, x)
376336
for x in xbmcvfs.listdir(_addon_data)[1]
377337
if x.endswith(".history")
378338
]
379339

340+
# Get rid of ones not read this session. These are old
341+
all_hist = [hist_path for hist_path in all_hist if (xbmcvfs.Stat(hist_path).st_mtime() - _startup_time) >= 0]
342+
380343
# Simple version. Anything updated recently (since startup?)
381344
# priority = sorted(all_cache, key=os.path.getmtime)
382345
# Sort by chance of it updating
383346
plays = utils.read_json(_playback_history_path, default={}).setdefault("plays", [])
384-
plays_for_type = [(time, t) for time, t in plays if t == media_type]
347+
plays_for_type = [(time, t) for time, t in plays if t == media_type or media_type is None]
385348
priority = sorted(
386349
[
387350
(
388-
chance_playback_updates_widget(path, plays_for_type),
389-
utils.read_json(path).get("path", ""),
390-
path,
351+
chance_playback_updates_widget(cache_data, plays_for_type),
352+
cache_data.get("path", ""),
353+
hist_path,
391354
)
392-
for path in all_cache
355+
for hist_path, cache_data in [(p, utils.read_json(p, default={})) for p in all_hist]
393356
],
394357
reverse=True,
395358
)
396-
359+
360+
count_prob_changed = 0
361+
randoms = 0
362+
i = 0
397363
for chance, path, history_path in priority:
398364
hash = path2hash(path)
399-
last_update = xbmcvfs.Stat(history_path).st_mtime() - _startup_time
400-
if last_update < 0:
401-
utils.log(
402-
"widget not updated since startup {} {}".format(last_update, hash[:5]),
403-
"notice",
404-
)
405-
# elif chance < 0.3:
406-
# log("chance widget changed after play {}% {}".format(chance, hash[:5]), 'notice')
407-
else:
365+
if "page=" in path:
366+
# HACK: must be a better way
367+
continue
368+
elif chance >= 0.3 or i < 7:
408369
utils.log(
409-
"chance widget changed after play {}% {}".format(chance, hash[:5]),
370+
"Queue {:.2f}% {} {}".format(chance * 100, hash[:5], path),
410371
"notice",
411372
)
373+
count_prob_changed += 1
412374
yield hash, path
375+
elif random.random() <= (1/len(priority)):
376+
# If widgets never get updated after playback we never get to know if they change after playback. So always pick some randomly
377+
utils.log("Queue random {:.2f}% {} {}".format(chance * 100, hash[:5], path), 'notice')
378+
randoms += 1
379+
yield hash, path
380+
else:
381+
utils.log("Prob not changes due to playback {:.2f}% {} {}".format(chance * 100, hash[:5], path), 'notice')
382+
i += 1
383+
utils.log("=== End Widget update: {} prob changed after playback {} randoms".format(count_prob_changed, randoms), 'notice')
413384

414-
415-
def chance_playback_updates_widget(history_path, plays, cutoff_time=60 * 5):
416-
cache_data = utils.read_json(history_path)
385+
def chance_playback_updates_widget(cache_data, plays, cutoff_time=60 * 60):
417386
history = cache_data.setdefault("history", [])
387+
hist_len = len(history)
388+
path = cache_data.get("path", "")
418389
# Complex version
419390
# - for each widget
420391
# - come up with chance it will update after a playback
421392
# - each pair of updates, is there a playback inbetween and updated with X min after playback
422393
# - num playback with change / num playback with no change
423-
changes, non_changes, unrelated_changes = 0, 0, 0
394+
# C C P C C
395+
changes, non_changes, unrelated_changes, too_late_changes = 0, 0, 0, 0
424396
update = ""
425-
time_since_play = 0
397+
update_time = 0
426398
for play_time, media_type in plays:
427-
while True:
399+
while (update_time - play_time) <= 0:
428400
last_update = update
401+
last_update_time = update_time
429402
if not history:
430403
break
431404
update_time, update = history.pop(0)
432-
time_since_play = update_time - play_time
433405
# log("{} {} {} {}".format(update[:5],last_update[:5], unrelated_changes, time_since_play), 'notice')
434-
if time_since_play > 0:
406+
if (update_time - play_time) > 0:
435407
break
436408
elif update != last_update:
409+
# Update that happened with no play inbetween
437410
unrelated_changes += 1
438-
439-
if update == last_update:
440-
non_changes += 1
441-
elif (
442-
time_since_play > cutoff_time
443-
): # update too long after playback to be releated
411+
# We now have a update after a playback
412+
if not update_time:
413+
break
414+
elif not last_update:
415+
# haven't got to first update yet
444416
pass
445-
else:
417+
elif update == last_update:
418+
if update_time == last_update_time:
419+
# Two playbacks without any updates
420+
pass
421+
else:
422+
# Didn't change after playback
423+
non_changes += 1
424+
elif (update_time - play_time) <= cutoff_time:
425+
# Did change after playback
446426
changes += 1
427+
else:
428+
# update too long after playback to be releated
429+
too_late_changes += 1
430+
pass
431+
447432
# TODO: what if the previous update was a long time before playback?
448433

449434
# There is probably a more statistically correct way of doing this but the idea is that
@@ -452,20 +437,27 @@ def chance_playback_updates_widget(history_path, plays, cutoff_time=60 * 5):
452437
# We will do a simple weighted average with 0.5 to simulate this
453438
# TODO: currently random widgets score higher than recently played widgets. need to score them lower
454439
# as they are less relevent
440+
# HACK: could too late changes for now until we work out why
441+
datapoints = float(changes + too_late_changes + non_changes)
442+
all_changes = float(datapoints + unrelated_changes)
443+
if all_changes == 0:
444+
# we have no data or lost it. let's get it updated
445+
prob = 1.0
446+
else:
447+
prob = (changes) / all_changes
448+
unknown_weight = 4
449+
prob = (prob * datapoints + 0.5 * unknown_weight) / (datapoints + unknown_weight)
450+
455451
utils.log(
456-
"changes={}, non_changes={}, unrelated_changes={}".format(
457-
changes, non_changes, unrelated_changes
452+
"prob:{:.2f}% changes:{} non_changes:{} non_play_changes:{} too_late:{} plays:{} hist:{}: {}".format(
453+
prob*100, changes, non_changes, unrelated_changes, too_late_changes, len(plays), hist_len, path,
458454
),
459-
"debug",
455+
"notice",
460456
)
461-
datapoints = float(changes + non_changes)
462-
prob = changes / float(changes + non_changes + unrelated_changes)
463-
unknown_weight = 4
464-
prob = (prob * datapoints + 0.5 * unknown_weight) / (datapoints + unknown_weight)
465457
return prob
466458

467459

468-
def save_playback_history(media_type, playback_percentage):
460+
def save_playback_history(media_type, playback_percentage, path):
469461
# Record in json when things got played to help predict which widgets will change after playback
470462
# if playback_percentage < 0.7:
471463
# return

0 commit comments

Comments
 (0)