-
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Dummy file to make this directory a package.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,143 @@
|
||||
import json
|
||||
from kodi_six import xbmc, xbmcvfs
|
||||
from . import utils as utils
|
||||
|
||||
|
||||
class CronSchedule:
|
||||
expression = ''
|
||||
name = 'library'
|
||||
timer_type = 'xbmc'
|
||||
command = {'method': 'VideoLibrary.Scan', 'params': {'showdialogs': True}}
|
||||
next_run = 0
|
||||
on_delay = False # used to defer processing until after player finishes
|
||||
|
||||
def executeCommand(self):
|
||||
jsonCommand = {'jsonrpc': '2.0', 'method': self.command['method'], 'params': self.command['params'], 'id': 44}
|
||||
utils.log(json.dumps(jsonCommand))
|
||||
xbmc.executeJSONRPC(json.dumps(jsonCommand))
|
||||
|
||||
def cleanLibrarySchedule(self, selectedIndex):
|
||||
if(selectedIndex == 1):
|
||||
# once per day
|
||||
return "* * *"
|
||||
elif (selectedIndex == 2):
|
||||
# once per week
|
||||
return "* * 0"
|
||||
else:
|
||||
# once per month
|
||||
return "1 * *"
|
||||
|
||||
|
||||
class CustomPathFile:
|
||||
jsonFile = xbmcvfs.translatePath(utils.data_dir() + "custom_paths.json")
|
||||
paths = None
|
||||
contentType = 'video' # all by default
|
||||
|
||||
def __init__(self, contentType):
|
||||
self.paths = []
|
||||
self.contentType = contentType
|
||||
|
||||
# try and read in the custom file
|
||||
self._readFile()
|
||||
|
||||
def getSchedules(self, showDialogs=True):
|
||||
schedules = []
|
||||
|
||||
# create schedules from the path information
|
||||
for aPath in self.paths:
|
||||
if(self.contentType == aPath['content']):
|
||||
schedules.append(self._createSchedule(aPath, showDialogs))
|
||||
|
||||
return schedules
|
||||
|
||||
def addPath(self, path):
|
||||
path['id'] = self._getNextId()
|
||||
self.paths.append(path)
|
||||
|
||||
# save the file
|
||||
self._writeFile()
|
||||
|
||||
def deletePath(self, aKey):
|
||||
# find the given key
|
||||
index = -1
|
||||
for i in range(0, len(self.paths)):
|
||||
if(self.paths[i]['id'] == aKey):
|
||||
index = i
|
||||
|
||||
# if found, delete it
|
||||
if(i != -1):
|
||||
del self.paths[index]
|
||||
|
||||
# save the file
|
||||
self._writeFile()
|
||||
|
||||
def getPaths(self):
|
||||
result = []
|
||||
|
||||
for aPath in self.paths:
|
||||
# if type matches the one we want
|
||||
if(self.contentType == 'all' or self.contentType == aPath['content']):
|
||||
result.append(aPath)
|
||||
|
||||
return result
|
||||
|
||||
def _getNextId(self):
|
||||
result = 0
|
||||
|
||||
if(len(self.paths) > 0):
|
||||
# sort ids, get highest one
|
||||
maxId = sorted(self.paths, reverse=True, key=lambda k: k['id'])
|
||||
result = maxId[0]['id']
|
||||
|
||||
return result + 1
|
||||
|
||||
def _writeFile(self):
|
||||
# sort the ids
|
||||
self.paths = sorted(self.paths, reverse=True, key=lambda k: k['id'])
|
||||
|
||||
# create the custom file
|
||||
aFile = xbmcvfs.File(self.jsonFile, 'w')
|
||||
aFile.write(json.dumps(self.paths))
|
||||
aFile.close()
|
||||
|
||||
def _readFile(self):
|
||||
|
||||
if(xbmcvfs.exists(self.jsonFile)):
|
||||
|
||||
# read in the custom file
|
||||
aFile = xbmcvfs.File(self.jsonFile)
|
||||
|
||||
# load paths in the format {path:path,expression:expression,content:type}
|
||||
tempPaths = json.loads(aFile.read())
|
||||
|
||||
# update values in path
|
||||
for aPath in tempPaths:
|
||||
|
||||
# old files are only video, update
|
||||
if('content' not in aPath):
|
||||
aPath['content'] = 'video'
|
||||
|
||||
if('id' not in aPath):
|
||||
aPath['id'] = self._getNextId()
|
||||
|
||||
self.paths.append(aPath)
|
||||
|
||||
aFile.close()
|
||||
else:
|
||||
# write a blank file
|
||||
self._writeFile()
|
||||
|
||||
def _createSchedule(self, aPath, showDialogs):
|
||||
|
||||
aSchedule = CronSchedule()
|
||||
aSchedule.name = aPath['path']
|
||||
|
||||
# command depends on content type
|
||||
if(aPath['content'] == 'video'):
|
||||
aSchedule.command = {'method': 'VideoLibrary.Scan', 'params': {'directory': aPath['path'], 'showdialogs': showDialogs}}
|
||||
else:
|
||||
aSchedule.command = {'method': 'AudioLibrary.Scan', 'params': {'directory': aPath['path'], 'showdialogs': showDialogs}}
|
||||
|
||||
aSchedule.expression = aPath['expression']
|
||||
|
||||
return aSchedule
|
||||
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
from time import time, mktime
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
search_re = re.compile(r'^([^-]+)-([^-/]+)(/(.*))?$')
|
||||
only_int_re = re.compile(r'^\d+$')
|
||||
any_int_re = re.compile(r'^\d+')
|
||||
star_or_int_re = re.compile(r'^(\d+|\*)$')
|
||||
|
||||
__all__ = ('croniter',)
|
||||
|
||||
|
||||
class croniter(object):
|
||||
RANGES = (
|
||||
(0, 59),
|
||||
(0, 23),
|
||||
(1, 31),
|
||||
(1, 12),
|
||||
(0, 6),
|
||||
(0, 59)
|
||||
)
|
||||
DAYS = (
|
||||
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||||
)
|
||||
|
||||
ALPHACONV = (
|
||||
{ },
|
||||
{ },
|
||||
{ },
|
||||
{ 'jan':1, 'feb':2, 'mar':3, 'apr':4, 'may':5, 'jun':6,
|
||||
'jul':7, 'aug':8, 'sep':9, 'oct':10, 'nov':11, 'dec':12 },
|
||||
{ 'sun':0, 'mon':1, 'tue':2, 'wed':3, 'thu':4, 'fri':5, 'sat':6 },
|
||||
{ }
|
||||
)
|
||||
|
||||
LOWMAP = (
|
||||
{},
|
||||
{},
|
||||
{0: 1},
|
||||
{0: 1},
|
||||
{7: 0},
|
||||
{},
|
||||
)
|
||||
|
||||
bad_length = 'Exactly 5 or 6 columns has to be specified for iterator' \
|
||||
'expression.'
|
||||
|
||||
def __init__(self, expr_format, start_time=time()):
|
||||
if isinstance(start_time, datetime):
|
||||
start_time = mktime(start_time.timetuple())
|
||||
|
||||
self.cur = start_time
|
||||
self.exprs = expr_format.split()
|
||||
|
||||
if len(self.exprs) != 5 and len(self.exprs) != 6:
|
||||
raise ValueError(self.bad_length)
|
||||
|
||||
expanded = []
|
||||
|
||||
for i, expr in enumerate(self.exprs):
|
||||
e_list = expr.split(',')
|
||||
res = []
|
||||
|
||||
while len(e_list) > 0:
|
||||
e = e_list.pop()
|
||||
t = re.sub(r'^\*(/.+)$', r'%d-%d\1' % (self.RANGES[i][0],
|
||||
self.RANGES[i][1]),
|
||||
str(e))
|
||||
m = search_re.search(t)
|
||||
|
||||
if m:
|
||||
(low, high, step) = m.group(1), m.group(2), m.group(4) or 1
|
||||
|
||||
if not any_int_re.search(low):
|
||||
low = self.ALPHACONV[i][low.lower()]
|
||||
|
||||
if not any_int_re.search(high):
|
||||
high = self.ALPHACONV[i][high.lower()]
|
||||
|
||||
if (not low or not high or int(low) > int(high)
|
||||
or not only_int_re.search(str(step))):
|
||||
raise ValueError("[%s] is not acceptable" %expr_format)
|
||||
|
||||
for j in range(int(low), int(high)+1):
|
||||
if j % int(step) == 0:
|
||||
e_list.append(j)
|
||||
else:
|
||||
if not star_or_int_re.search(t):
|
||||
t = self.ALPHACONV[i][t.lower()]
|
||||
|
||||
try:
|
||||
t = int(t)
|
||||
except:
|
||||
pass
|
||||
|
||||
if t in self.LOWMAP[i]:
|
||||
t = self.LOWMAP[i][t]
|
||||
|
||||
if t != '*' and (int(t) < self.RANGES[i][0] or
|
||||
int(t) > self.RANGES[i][1]):
|
||||
raise ValueError("[%s] is not acceptable, out of range" % expr_format)
|
||||
|
||||
res.append(t)
|
||||
|
||||
res.sort()
|
||||
expanded.append(['*'] if (len(res) == 1 and res[0] == '*') else res)
|
||||
self.expanded = expanded
|
||||
|
||||
def get_next(self, ret_type=float):
|
||||
return self._get_next(ret_type, is_prev=False)
|
||||
|
||||
def get_prev(self, ret_type=float):
|
||||
return self._get_next(ret_type, is_prev=True)
|
||||
|
||||
def _get_next(self, ret_type=float, is_prev=False):
|
||||
expanded = self.expanded[:]
|
||||
|
||||
if ret_type not in (float, datetime):
|
||||
raise TypeError("Invalid ret_type, only 'float' or 'datetime' " \
|
||||
"is acceptable.")
|
||||
|
||||
if expanded[2][0] != '*' and expanded[4][0] != '*':
|
||||
bak = expanded[4]
|
||||
expanded[4] = ['*']
|
||||
t1 = self._calc(self.cur, expanded, is_prev)
|
||||
expanded[4] = bak
|
||||
expanded[2] = ['*']
|
||||
|
||||
t2 = self._calc(self.cur, expanded, is_prev)
|
||||
if not is_prev:
|
||||
result = t1 if t1 < t2 else t2
|
||||
else:
|
||||
result = t1 if t1 > t2 else t2
|
||||
else:
|
||||
result = self._calc(self.cur, expanded, is_prev)
|
||||
self.cur = result
|
||||
|
||||
if ret_type == datetime:
|
||||
result = datetime.fromtimestamp(result)
|
||||
return result
|
||||
|
||||
def _calc(self, now, expanded, is_prev):
|
||||
if is_prev:
|
||||
nearest_method = self._get_prev_nearest
|
||||
nearest_diff_method = self._get_prev_nearest_diff
|
||||
sign = -1
|
||||
else:
|
||||
nearest_method = self._get_next_nearest
|
||||
nearest_diff_method = self._get_next_nearest_diff
|
||||
sign = 1
|
||||
|
||||
offset = len(expanded) == 6 and 1 or 60
|
||||
dst = now = datetime.fromtimestamp(now + sign * offset)
|
||||
|
||||
day, month, year = dst.day, dst.month, dst.year
|
||||
current_year = now.year
|
||||
DAYS = self.DAYS
|
||||
|
||||
def proc_month(d):
|
||||
if expanded[3][0] != '*':
|
||||
diff_month = nearest_diff_method(month, expanded[3], 12)
|
||||
days = DAYS[month - 1]
|
||||
if month == 2 and self.is_leap(year) == True:
|
||||
days += 1
|
||||
|
||||
reset_day = days if is_prev else 1
|
||||
|
||||
if diff_month != None and diff_month != 0:
|
||||
if is_prev:
|
||||
d += relativedelta(months=diff_month)
|
||||
else:
|
||||
d += relativedelta(months=diff_month, day=reset_day,
|
||||
hour=0, minute=0, second=0)
|
||||
return True, d
|
||||
return False, d
|
||||
|
||||
def proc_day_of_month(d):
|
||||
if expanded[2][0] != '*':
|
||||
days = DAYS[month - 1]
|
||||
if month == 2 and self.is_leap(year) == True:
|
||||
days += 1
|
||||
|
||||
diff_day = nearest_diff_method(d.day, expanded[2], days)
|
||||
|
||||
if diff_day != None and diff_day != 0:
|
||||
if is_prev:
|
||||
d += relativedelta(days=diff_day)
|
||||
else:
|
||||
d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
|
||||
return True, d
|
||||
return False, d
|
||||
|
||||
def proc_day_of_week(d):
|
||||
if expanded[4][0] != '*':
|
||||
diff_day_of_week = nearest_diff_method(d.isoweekday() % 7, expanded[4], 7)
|
||||
if diff_day_of_week != None and diff_day_of_week != 0:
|
||||
if is_prev:
|
||||
d += relativedelta(days=diff_day_of_week)
|
||||
else:
|
||||
d += relativedelta(days=diff_day_of_week, hour=0, minute=0, second=0)
|
||||
return True, d
|
||||
return False, d
|
||||
|
||||
def proc_hour(d):
|
||||
if expanded[1][0] != '*':
|
||||
diff_hour = nearest_diff_method(d.hour, expanded[1], 24)
|
||||
if diff_hour != None and diff_hour != 0:
|
||||
if is_prev:
|
||||
d += relativedelta(hours = diff_hour)
|
||||
else:
|
||||
d += relativedelta(hours = diff_hour, minute=0, second=0)
|
||||
return True, d
|
||||
return False, d
|
||||
|
||||
def proc_minute(d):
|
||||
if expanded[0][0] != '*':
|
||||
diff_min = nearest_diff_method(d.minute, expanded[0], 60)
|
||||
if diff_min != None and diff_min != 0:
|
||||
if is_prev:
|
||||
d += relativedelta(minutes = diff_min)
|
||||
else:
|
||||
d += relativedelta(minutes = diff_min, second=0)
|
||||
return True, d
|
||||
return False, d
|
||||
|
||||
def proc_second(d):
|
||||
if len(expanded) == 6:
|
||||
if expanded[5][0] != '*':
|
||||
diff_sec = nearest_diff_method(d.second, expanded[5], 60)
|
||||
if diff_sec != None and diff_sec != 0:
|
||||
dst += relativedelta(seconds = diff_sec)
|
||||
return True, d
|
||||
else:
|
||||
d += relativedelta(second = 0)
|
||||
return False, d
|
||||
|
||||
if is_prev:
|
||||
procs = [proc_second,
|
||||
proc_minute,
|
||||
proc_hour,
|
||||
proc_day_of_week,
|
||||
proc_day_of_month,
|
||||
proc_month]
|
||||
else:
|
||||
procs = [proc_month,
|
||||
proc_day_of_month,
|
||||
proc_day_of_week,
|
||||
proc_hour,
|
||||
proc_minute,
|
||||
proc_second]
|
||||
|
||||
while abs(year - current_year) <= 1:
|
||||
next = False
|
||||
for proc in procs:
|
||||
(changed, dst) = proc(dst)
|
||||
if changed:
|
||||
next = True
|
||||
break
|
||||
if next:
|
||||
continue
|
||||
return mktime(dst.timetuple())
|
||||
|
||||
raise "failed to find prev date"
|
||||
|
||||
def _get_next_nearest(self, x, to_check):
|
||||
small = [item for item in to_check if item < x]
|
||||
large = [item for item in to_check if item >= x]
|
||||
large.extend(small)
|
||||
return large[0]
|
||||
|
||||
def _get_prev_nearest(self, x, to_check):
|
||||
small = [item for item in to_check if item <= x]
|
||||
large = [item for item in to_check if item > x]
|
||||
small.reverse()
|
||||
large.reverse()
|
||||
small.extend(large)
|
||||
return small[0]
|
||||
|
||||
def _get_next_nearest_diff(self, x, to_check, range_val):
|
||||
for i, d in enumerate(to_check):
|
||||
if d >= x:
|
||||
return d - x
|
||||
return to_check[0] - x + range_val
|
||||
|
||||
def _get_prev_nearest_diff(self, x, to_check, range_val):
|
||||
candidates = to_check[:]
|
||||
candidates.reverse()
|
||||
for d in candidates:
|
||||
if d <= x:
|
||||
return d - x
|
||||
return (candidates[0]) - x - range_val
|
||||
|
||||
def is_leap(self, year):
|
||||
if year % 400 == 0 or (year % 4 == 0 and year % 100 != 0):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -0,0 +1,424 @@
|
||||
# -*- coding: cp1252 -*-
|
||||
import time
|
||||
from datetime import datetime
|
||||
from kodi_six import xbmc, xbmcgui, xbmcvfs
|
||||
from future.moves.urllib.request import urlopen, unquote
|
||||
import json
|
||||
import resources.lib.utils as utils
|
||||
from resources.lib.croniter import croniter
|
||||
from resources.lib.cronclasses import CronSchedule, CustomPathFile
|
||||
|
||||
UPGRADE_INT = 1 # to keep track of any upgrade notifications
|
||||
|
||||
|
||||
class AutoUpdater:
|
||||
last_run = 0
|
||||
schedules = []
|
||||
lock = False
|
||||
|
||||
monitor = None
|
||||
|
||||
def __init__(self):
|
||||
utils.check_data_dir() # in case this directory does not exist yet
|
||||
self.readLastRun()
|
||||
|
||||
# force and update on startup to create the array
|
||||
self.createSchedules(True)
|
||||
|
||||
def runProgram(self):
|
||||
self.monitor = UpdateMonitor(update_settings=self.createSchedules, after_scan=self.databaseUpdated)
|
||||
# a one-time catch for the startup delay
|
||||
if(utils.getSettingInt("startup_delay") != 0):
|
||||
count = 0
|
||||
while count < len(self.schedules):
|
||||
if(time.time() > self.schedules[count].next_run):
|
||||
# we missed at least one update, fix this
|
||||
self.schedules[count].next_run = time.time() + utils.getSettingInt("startup_delay") * 60
|
||||
count = count + 1
|
||||
|
||||
utils.log(str(utils.getSettingInt('startup_delay')))
|
||||
# display upgrade messages if they exist
|
||||
if(utils.getSettingInt('upgrade_notes') < UPGRADE_INT):
|
||||
xbmcgui.Dialog().ok(utils.getString(30000), utils.getString(30030))
|
||||
utils.setSetting('upgrade_notes', str(UPGRADE_INT))
|
||||
|
||||
# program has started, check if we should show a notification
|
||||
self.showNotify()
|
||||
|
||||
while(True):
|
||||
|
||||
# don't check unless new minute
|
||||
if(time.time() > self.last_run + 60):
|
||||
self.readLastRun()
|
||||
|
||||
self.evalSchedules()
|
||||
|
||||
# calculate the sleep time (next minute)
|
||||
now = datetime.now()
|
||||
if(self.monitor.waitForAbort(60 - now.second)):
|
||||
break
|
||||
|
||||
# clean up monitor on exit
|
||||
del self.monitor
|
||||
|
||||
def evalSchedules(self, manual=False):
|
||||
if(not self.lock):
|
||||
now = time.time()
|
||||
|
||||
count = 0
|
||||
player = xbmc.Player()
|
||||
while count < len(self.schedules):
|
||||
cronJob = self.schedules[count]
|
||||
|
||||
if(cronJob.next_run <= now or manual):
|
||||
if(not player.isPlaying() or utils.getSetting("run_during_playback") == "true"):
|
||||
|
||||
# check if run on idle is checked and screen is idle - disable this on manual run
|
||||
if(not utils.getSettingBool('run_on_idle') or (utils.getSettingBool('run_on_idle') and (self.monitor.screensaver_running or manual))):
|
||||
|
||||
# check for valid network connection - check sources if setting enabled
|
||||
if(self._networkUp() and (not utils.getSettingBool('check_sources') or (utils.getSettingBool('check_sources') and self._checkSources(cronJob)))):
|
||||
|
||||
# check if this scan was delayed due to playback
|
||||
if(cronJob.on_delay):
|
||||
# add another minute to the delay
|
||||
self.schedules[count].next_run = now + 60
|
||||
self.schedules[count].on_delay = False
|
||||
utils.log(cronJob.name + " paused due to playback")
|
||||
|
||||
elif(not self.scanRunning()):
|
||||
# run the command for this job
|
||||
utils.log(cronJob.name)
|
||||
|
||||
if(cronJob.timer_type == 'xbmc'):
|
||||
cronJob.executeCommand()
|
||||
else:
|
||||
self.cleanLibrary(cronJob)
|
||||
|
||||
# find the next run time
|
||||
cronJob.next_run = self.calcNextRun(cronJob.expression, now)
|
||||
self.schedules[count] = cronJob
|
||||
|
||||
elif(self.scanRunning()):
|
||||
self.schedules[count].next_run = now + 60
|
||||
utils.log("Waiting for other scan to finish")
|
||||
else:
|
||||
utils.log("Network down, not running")
|
||||
else:
|
||||
utils.log("Skipping scan, only run when idle")
|
||||
else:
|
||||
self.schedules[count].on_delay = True
|
||||
utils.log("Player is running, wait until finished")
|
||||
|
||||
count = count + 1
|
||||
|
||||
# write last run time
|
||||
now = time.time()
|
||||
self.last_run = now - (now % 60)
|
||||
|
||||
def createSchedules(self, forceUpdate=False):
|
||||
utils.log("update timers")
|
||||
self.lock = True # lock so the eval portion does not run
|
||||
self.schedules = []
|
||||
showDialogs = utils.getSettingBool('notify_next_run') # if the user has selected to show dialogs for library operations
|
||||
|
||||
if(utils.getSettingBool('clean_libraries')):
|
||||
# create clean schedule (if needed)
|
||||
if(utils.getSettingInt("clean_timer") != 0):
|
||||
|
||||
if(utils.getSettingInt('library_to_clean') == 0 or utils.getSettingInt('library_to_clean') == 1):
|
||||
# video clean schedule starts at 12am by default
|
||||
aSchedule = CronSchedule()
|
||||
aSchedule.name = utils.getString(30048)
|
||||
aSchedule.timer_type = utils.__addon_id__
|
||||
aSchedule.command = {'method': 'VideoLibrary.Clean', 'params': {'showdialogs': showDialogs}}
|
||||
if(utils.getSettingInt("clean_timer") == 4):
|
||||
aSchedule.expression = utils.getSetting("clean_video_cron_expression")
|
||||
else:
|
||||
aSchedule.expression = "0 0 " + aSchedule.cleanLibrarySchedule(utils.getSettingInt("clean_timer"))
|
||||
aSchedule.next_run = self.calcNextRun(aSchedule.expression, time.time())
|
||||
|
||||
self.schedules.append(aSchedule)
|
||||
|
||||
if(utils.getSettingInt('library_to_clean') == 2 or utils.getSettingInt('library_to_clean') == 0):
|
||||
# music clean schedule starts at 2am by default
|
||||
aSchedule = CronSchedule()
|
||||
aSchedule.name = utils.getString(30049)
|
||||
aSchedule.timer_type = utils.__addon_id__
|
||||
aSchedule.command = {'method': 'AudioLibrary.Clean', 'params': {'showdialogs': showDialogs}}
|
||||
if(utils.getSettingInt("clean_timer") == 4):
|
||||
aSchedule.expression = utils.getSetting("clean_music_cron_expression")
|
||||
else:
|
||||
aSchedule.expression = "0 2 " + aSchedule.cleanLibrarySchedule(utils.getSettingInt("clean_timer"))
|
||||
aSchedule.next_run = self.calcNextRun(aSchedule.expression, time.time())
|
||||
|
||||
self.schedules.append(aSchedule)
|
||||
|
||||
if(utils.getSettingBool('update_video')):
|
||||
utils.log("Creating timer for Video Library")
|
||||
# create the video schedule
|
||||
aSchedule = CronSchedule()
|
||||
aSchedule.name = utils.getString(30012)
|
||||
aSchedule.command = {'method': 'VideoLibrary.Scan', 'params': {'showdialogs': showDialogs}}
|
||||
aSchedule.expression = self.checkTimer('video')
|
||||
aSchedule.next_run = self.calcNextRun(aSchedule.expression, self.last_run)
|
||||
self.schedules.append(aSchedule)
|
||||
|
||||
# add custom video paths (separate timers)
|
||||
customPaths = CustomPathFile('video')
|
||||
for aJob in customPaths.getSchedules(showDialogs):
|
||||
utils.log("Creating timer " + aJob.name)
|
||||
aJob.next_run = self.calcNextRun(aJob.expression, self.last_run)
|
||||
self.schedules.append(aJob)
|
||||
|
||||
if(utils.getSettingBool('update_music')):
|
||||
utils.log("Creating timer for Music Library")
|
||||
# create the music schedule
|
||||
aSchedule = CronSchedule()
|
||||
aSchedule.name = utils.getString(30013)
|
||||
aSchedule.command = {'method': 'AudioLibrary.Scan', 'params': {'showdialogs': showDialogs}}
|
||||
aSchedule.expression = self.checkTimer('music')
|
||||
aSchedule.next_run = self.calcNextRun(aSchedule.expression, self.last_run)
|
||||
|
||||
self.schedules.append(aSchedule)
|
||||
|
||||
# add custom music paths (separate timers)
|
||||
customPaths = CustomPathFile('music')
|
||||
for aJob in customPaths.getSchedules(showDialogs):
|
||||
utils.log("Creating timer " + aJob.name)
|
||||
aJob.next_run = self.calcNextRun(aJob.expression, self.last_run)
|
||||
self.schedules.append(aJob)
|
||||
|
||||
# release the lock
|
||||
self.lock = False
|
||||
|
||||
utils.log("Created " + str(len(self.schedules)) + " schedules", xbmc.LOGDEBUG)
|
||||
|
||||
# show any notifications
|
||||
self.showNotify(not forceUpdate)
|
||||
|
||||
def checkTimer(self, settingName):
|
||||
result = ''
|
||||
utils.log(utils.getSetting(settingName + "_timer"))
|
||||
# figure out if using standard or advanced timer
|
||||
if(utils.getSettingBool(settingName + '_advanced_timer')):
|
||||
# copy the expression
|
||||
result = utils.getSetting(settingName + "_cron_expression")
|
||||
else:
|
||||
result = '0 */' + str(utils.getSetting(settingName + "_timer")) + ' * * *'
|
||||
|
||||
return result
|
||||
|
||||
def calcNextRun(self, cronExp, startTime):
|
||||
nextRun = -1
|
||||
|
||||
try:
|
||||
# create croniter for this expression
|
||||
cron = croniter(cronExp, startTime)
|
||||
nextRun = cron.get_next(float)
|
||||
except ValueError:
|
||||
# error in syntax
|
||||
xbmcgui.Dialog().ok(utils.getString(30000), utils.getString(30016) % cronExp)
|
||||
utils.log('Cron syntax error %s' % cronExp, xbmc.LOGDEBUG)
|
||||
|
||||
# rerun with a valid syntax
|
||||
nextRun = self.calcNextRun('0 */2 * * *', startTime)
|
||||
|
||||
return nextRun
|
||||
|
||||
def showNotify(self, displayToScreen=True):
|
||||
# go through and find the next schedule to run
|
||||
next_run_time = CronSchedule()
|
||||
for cronJob in self.schedules:
|
||||
if(cronJob.next_run < next_run_time.next_run or next_run_time.next_run == 0):
|
||||
next_run_time = cronJob
|
||||
|
||||
inWords = self.nextRunCountdown(next_run_time.next_run)
|
||||
# show the notification (if applicable)
|
||||
if(next_run_time.next_run > time.time() and utils.getSettingBool('notify_next_run') and displayToScreen):
|
||||
utils.showNotification(utils.getString(30000), inWords + " - " + next_run_time.name)
|
||||
|
||||
return inWords
|
||||
|
||||
def nextRunCountdown(self, nextRun):
|
||||
# compare now with next date
|
||||
cronDiff = nextRun - time.time()
|
||||
|
||||
if cronDiff < 0:
|
||||
return ""
|
||||
|
||||
hours = int((cronDiff / 60) / 60)
|
||||
minutes = int(round(cronDiff / 60.0 - hours * 60))
|
||||
|
||||
# we always have at least one minute
|
||||
if minutes == 0:
|
||||
minutes = 1
|
||||
|
||||
result = str(hours) + " h " + str(minutes) + " m"
|
||||
if hours == 0:
|
||||
result = str(minutes) + " m"
|
||||
elif hours > 36:
|
||||
# just show the date instead
|
||||
result = datetime.fromtimestamp(nextRun).strftime('%m/%d %I:%M%p')
|
||||
elif hours > 24:
|
||||
days = int(hours / 24)
|
||||
hours = hours - days * 24
|
||||
result = str(days) + " d " + str(hours) + " h " + str(minutes) + " m"
|
||||
|
||||
return result
|
||||
|
||||
def cleanLibrary(self, cronJob):
|
||||
# check if we should verify with user first unless we're on 'clean after update'
|
||||
if(utils.getSettingBool('user_confirm_clean') and utils.getSettingInt('clean_timer') != 0):
|
||||
# user can decide 'no' here and exit this
|
||||
runClean = xbmcgui.Dialog().yesno(utils.getString(30000), utils.getString(30052), line2=utils.getString(30053), autoclose=15000)
|
||||
if(not runClean):
|
||||
return
|
||||
|
||||
# run the clean operation
|
||||
utils.log("Cleaning Database")
|
||||
cronJob.executeCommand()
|
||||
|
||||
# write last run time, will trigger notifications
|
||||
self.writeLastRun()
|
||||
|
||||
def readLastRun(self):
|
||||
if(self.last_run == 0):
|
||||
# read it in from the settings
|
||||
if(xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + "last_run.txt"))):
|
||||
|
||||
runFile = xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "last_run.txt"))
|
||||
|
||||
try:
|
||||
# there may be an issue with this file, we'll get it the next time through
|
||||
self.last_run = float(runFile.read())
|
||||
except ValueError:
|
||||
self.last_run = 0
|
||||
|
||||
runFile.close()
|
||||
else:
|
||||
self.last_run = 0
|
||||
|
||||
def writeLastRun(self):
|
||||
runFile = xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "last_run.txt"), 'w')
|
||||
runFile.write(str(self.last_run))
|
||||
runFile.close()
|
||||
|
||||
self.showNotify(True)
|
||||
|
||||
def scanRunning(self):
|
||||
# check if any type of scan is currently running
|
||||
if(xbmc.getCondVisibility('Library.IsScanningVideo') or xbmc.getCondVisibility('Library.IsScanningMusic')):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def databaseUpdated(self, database):
|
||||
showDialogs = utils.getSettingBool('notify_next_run') # if the user has selected to show dialogs for library operations
|
||||
# check if we should clean the library
|
||||
if(utils.getSettingBool('clean_libraries')):
|
||||
# check if should update while playing media
|
||||
if(not xbmc.Player().isPlaying() or utils.getSettingBool("run_during_playback")):
|
||||
if(utils.getSettingInt("clean_timer") == 0):
|
||||
# check if we should clean music, or video
|
||||
aJob = CronSchedule()
|
||||
aJob.name = utils.getString(30048)
|
||||
aJob.timer_type = utils.__addon_id__
|
||||
if((utils.getSettingInt('library_to_clean') == 0 or utils.getSettingInt('library_to_clean') == 1) and database == 'video'):
|
||||
# create the clean job schedule
|
||||
aJob.command = {'method': 'VideoLibrary.Clean', 'params': {'showdialogs': showDialogs}}
|
||||
if((utils.getSettingInt('library_to_clean') == 2 or utils.getSettingInt('library_to_clean') == 0) and database == 'music'):
|
||||
aJob.command = {'method': 'AudioLibrary.Clean', 'params': {'showdialogs': showDialogs}}
|
||||
|
||||
self.cleanLibrary(aJob)
|
||||
|
||||
# writeLastRun will trigger notifications
|
||||
self.writeLastRun()
|
||||
|
||||
def _networkUp(self):
|
||||
try:
|
||||
urlopen('http://connectivitycheck.gstatic.com/generate_204', timeout=1)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def _checkSources(self, aJob):
|
||||
result = False
|
||||
mediaType = 'video'
|
||||
|
||||
if(aJob.command['method'] == 'VideoLibrary.Scan' or aJob.command['method'] == 'AudioLibrary.Scan'):
|
||||
|
||||
# set the media type
|
||||
if(aJob.command['method'] != 'VideoLibrary.Scan'):
|
||||
mediaType = 'music'
|
||||
|
||||
if('directory' in aJob.command['params']):
|
||||
# we have a specific path to check
|
||||
result = self._sourceExists(aJob.command['params']['directory'])
|
||||
else:
|
||||
# check every video path
|
||||
response = json.loads(xbmc.executeJSONRPC(json.dumps({'jsonrpc': '2.0', 'method': 'Files.GetSources', 'params': {'media': mediaType}, 'id': 44})))
|
||||
|
||||
# make sure we got something
|
||||
if('result' in response):
|
||||
for source in response['result']['sources']:
|
||||
if(not self._sourceExists(source['file'])):
|
||||
# one failure fails the whole thing
|
||||
return False
|
||||
# if we make it this far we got them all
|
||||
result = True
|
||||
else:
|
||||
# must be a cleaning, skip this check since Kodi will do it
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
def _sourceExists(self, source):
|
||||
utils.log("checking: " + source)
|
||||
# check if this is a multipath source
|
||||
if(source.startswith('multipath://')):
|
||||
# code adapted from xbmc source MultiPathDirectory.cpp
|
||||
source = source[12:]
|
||||
|
||||
if(source[-1:] == "/"):
|
||||
source = source[:-1]
|
||||
splitSource = source.split('/')
|
||||
if(len(splitSource) > 0):
|
||||
for aSource in splitSource:
|
||||
if not xbmcvfs.exists(unquote(aSource)):
|
||||
# if one source in the multi does not exist, return false
|
||||
return False
|
||||
# if we make it here they all exist
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return xbmcvfs.exists(source)
|
||||
|
||||
|
||||
class UpdateMonitor(xbmc.Monitor):
|
||||
update_settings = None
|
||||
after_scan = None
|
||||
screensaver_running = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmc.Monitor.__init__(self)
|
||||
self.update_settings = kwargs['update_settings']
|
||||
self.after_scan = kwargs['after_scan']
|
||||
|
||||
def onSettingsChanged(self):
|
||||
xbmc.sleep(1000) # slight delay for notifications
|
||||
self.update_settings()
|
||||
|
||||
def onScanFinished(self, database):
|
||||
self.after_scan(database)
|
||||
|
||||
def onScreensaverActivated(self):
|
||||
utils.log("screen saver on", xbmc.LOGDEBUG)
|
||||
self.screensaver_running = True
|
||||
|
||||
def onScreensaverDeactivated(self):
|
||||
utils.log("screen saver off", xbmc.LOGDEBUG)
|
||||
self.screensaver_running = False
|
||||
@@ -0,0 +1,45 @@
|
||||
from kodi_six import xbmc, xbmcgui, xbmcaddon, xbmcvfs
|
||||
|
||||
__addon_id__ = 'service.libraryautoupdate'
|
||||
__Addon = xbmcaddon.Addon(__addon_id__)
|
||||
|
||||
|
||||
def check_data_dir():
|
||||
if(not xbmcvfs.exists(xbmcvfs.translatePath(data_dir()))):
|
||||
xbmcvfs.mkdir(xbmcvfs.translatePath(data_dir()))
|
||||
|
||||
|
||||
def data_dir():
|
||||
return __Addon.getAddonInfo('profile')
|
||||
|
||||
|
||||
def addon_dir():
|
||||
return __Addon.getAddonInfo('path')
|
||||
|
||||
|
||||
def log(message, loglevel=xbmc.LOGDEBUG):
|
||||
xbmc.log(__addon_id__ + "-" + __Addon.getAddonInfo('version') + " : " + message, level=loglevel)
|
||||
|
||||
|
||||
def showNotification(title, message):
|
||||
xbmcgui.Dialog().notification(getString(30000), message, time=5000, icon=xbmcvfs.translatePath(__Addon.getAddonInfo('path') + "/resources/media/icon.png"), sound=False)
|
||||
|
||||
|
||||
def setSetting(name, value):
|
||||
__Addon.setSettingString(name, value)
|
||||
|
||||
|
||||
def getSetting(name):
|
||||
return __Addon.getSetting(name)
|
||||
|
||||
|
||||
def getSettingBool(name):
|
||||
return bool(__Addon.getSettingBool(name))
|
||||
|
||||
|
||||
def getSettingInt(name):
|
||||
return __Addon.getSettingInt(name)
|
||||
|
||||
|
||||
def getString(string_id):
|
||||
return __Addon.getLocalizedString(string_id)
|
||||
Reference in New Issue
Block a user