1+ import threading
12import xbmcgui
23import xbmcvfs
4+ import xbmc
35
46import glob
57import hashlib
68import json
79import math
810import os
911import time
12+ import random
1013
1114import 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-
6252def 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-
9766def 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
13094def 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