Update for Leia (#117)

* updated addon.xml for Krypton

* default log level is always debug now

* added screenshots per krypton format

* started new way of defining backup directories

* reconfigured simple backup process

* added an advanced backup editor and combined settings.xml scripts into a launcher

* added strings for advanced editor

* there was a function to do this

* match excluded with regex

* updated def for the addons set

* directory has to end in slash to use exists()

* added a backup set chooser on restore

* added string for restore browser

* utilize details to show root folder and icons

* save non translated paths, better cross platform support

* revert dropbox python 2.6 changes

* start of #132

* can't have duplicate ids

* updated strings

* closes #132

* added a disclaimer for breaking changes

* split backup and restore into separate functions

* updated scripting to pass in list of sets to restore

* beta version

* added 2 min delay in startup - part of #147

* forgot to remove debug message

* change to wait for abort in case someone tries to close Kodi

* add retroplayer game saves to default file list

* display restore points with most recent on top

* remove length check, breaking change with this version means old archives are no longer compatible

* format restore list according to regional settings

* this function isn't used anymore, legacy of old file manager

* use images folder as default

* added note about compatibility

* added utils function for regional date, use for scheduler notifications as well

* add/remove include and exclude directories to a set

* paths should have / at the end

* show path relative to root

* if in advanced mode allow jumping to editor from launch screen

* check that path is within root folder of set

* cannot have duplicate set names or rules regarding folders within a set

* put strings in correct lang file

* beta version bump

* accidentally deleted string id

* change exclude criteria. Regex was not matching in complex cases

* make sure the dest folder (backup set root) exists before writing to it

* modify select display to show recursive value for included folders

* use a context menu here

* added ability to toggle recursion of sub folders

* beta 3

* added support doc

* wrong branch

* don't need this import anymore

* don't need these imports

* part of #133
This commit is contained in:
Rob 2019-08-26 15:40:15 -05:00 committed by GitHub
parent d703374792
commit 865416977d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1529 additions and 1043 deletions

View File

@ -1,5 +1,7 @@
# Backup Addon # Backup Addon
__Kodi Version Compatibility:__ Kodi 17.x (Krypton) and greater
## About ## About
I've had to recover my database, thumbnails, and source configuration enough times that I just wanted a quick easy way to back them up. That is what this addon is meant to do. I've had to recover my database, thumbnails, and source configuration enough times that I just wanted a quick easy way to back them up. That is what this addon is meant to do.
@ -17,6 +19,11 @@ For more specific information please check out the [wiki on Github](https://gith
* [FAQ](https://github.com/robweber/xbmcbackup/wiki/FAQ) * [FAQ](https://github.com/robweber/xbmcbackup/wiki/FAQ)
## Attributions
Icon files from Open Iconic — www.useiconic.com/open

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.xbmcbackup" <addon id="script.xbmcbackup"
name="Backup" version="1.1.3" provider-name="robweber"> name="Backup" version="1.5.0~beta3" provider-name="robweber">
<requires> <requires>
<!-- jarvis --> <!-- jarvis -->
<import addon="xbmc.python" version="2.24.0"/> <import addon="xbmc.python" version="2.25.0"/>
<import addon="script.module.httplib2" version="0.8.0" /> <import addon="script.module.httplib2" version="0.8.0" />
<import addon="script.module.oauth2client" version="4.1.2" /> <import addon="script.module.oauth2client" version="4.1.2" />
<import addon="script.module.uritemplate" version="0.6" /> <import addon="script.module.uritemplate" version="0.6" />
@ -89,7 +89,11 @@
<source>https://github.com/robweber/xbmcbackup</source> <source>https://github.com/robweber/xbmcbackup</source>
<email></email> <email></email>
<assets> <assets>
<icon>resources/media/icon.png</icon> <icon>resources/images/icon.png</icon>
<screenshot>resources/images/screenshot1.png</screenshot>
<screenshot>resources/images/screenshot2.png</screenshot>
<screenshot>resources/images/screenshot3.png</screenshot>
<screenshot>resources/images/screenshot4.png</screenshot>
</assets> </assets>
<news>Version 1.1.4 <news>Version 1.1.4
- added file chunk support for dropbox uploads - added file chunk support for dropbox uploads

View File

@ -1,37 +0,0 @@
import sys
import urlparse
import xbmcgui
import resources.lib.utils as utils
from resources.lib.authorizers import DropboxAuthorizer,GoogleDriveAuthorizer
def get_params():
param = {}
try:
for i in sys.argv:
args = i
if(args.startswith('?')):
args = args[1:]
param.update(dict(urlparse.parse_qsl(args)))
except:
pass
return param
params = get_params()
#drobpox
if(params['type'] == 'dropbox'):
authorizer = DropboxAuthorizer()
if(authorizer.authorize()):
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30027) + ' ' + utils.getString(30106))
else:
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30107) + ' ' + utils.getString(30027))
#google drive
elif(params['type'] == 'google_drive'):
authorizer = GoogleDriveAuthorizer()
if(authorizer.authorize()):
xbmcgui.Dialog().ok("Backup",utils.getString(30098) + ' ' + utils.getString(30106))
else:
xbmcgui.Dialog().ok("Backup",utils.getString(30107) + ' ' + utils.getString(30098))

View File

@ -1,77 +1,91 @@
import urlparse import sys, urlparse
import xbmcgui import xbmc, xbmcgui
import resources.lib.utils as utils import resources.lib.utils as utils
from resources.lib.backup import XbmcBackup from resources.lib.backup import XbmcBackup
def get_params(): def get_params():
param = {} param = {}
if(len(sys.argv) > 1): if(len(sys.argv) > 1):
for i in sys.argv: for i in sys.argv:
args = i args = i
if(args.startswith('?')): if(args.startswith('?')):
args = args[1:] args = args[1:]
param.update(dict(urlparse.parse_qsl(args))) param.update(dict(urlparse.parse_qsl(args)))
return param return param
#the program mode #the program mode
mode = -1 mode = -1
params = get_params() params = get_params()
if("mode" in params): if("mode" in params):
if(params['mode'] == 'backup'): if(params['mode'] == 'backup'):
mode = 0 mode = 0
elif(params['mode'] == 'restore'): elif(params['mode'] == 'restore'):
mode = 1 mode = 1
#if mode wasn't passed in as arg, get from user #if mode wasn't passed in as arg, get from user
if(mode == -1): if(mode == -1):
#figure out if this is a backup or a restore from the user #by default, Backup,Restore,Open Settings
mode = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30023),[utils.getString(30016),utils.getString(30017),utils.getString(30099)]) options = [utils.getString(30016),utils.getString(30017),utils.getString(30099)]
#check if program should be run #find out if we're using the advanced editor
if(mode != -1): if(int(utils.getSetting('backup_selection_type')) == 1):
#run the profile backup options.append(utils.getString(30125))
backup = XbmcBackup()
#figure out if this is a backup or a restore from the user
if(mode == 2): mode = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30023),options)
#open the settings dialog
utils.openSettings() #check if program should be run
if(mode != -1):
elif(backup.remoteConfigured()): #run the profile backup
backup = XbmcBackup()
if(mode == backup.Restore):
#get list of valid restore points if(mode == 2):
restorePoints = backup.listBackups() #open the settings dialog
pointNames = [] utils.openSettings()
folderNames = [] elif(mode == 3 and int(utils.getSetting('backup_selection_type')) == 1):
#open the advanced editor
for aDir in restorePoints: xbmc.executebuiltin('RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_editor)')
pointNames.append(aDir[1]) elif(backup.remoteConfigured()):
folderNames.append(aDir[0])
if(mode == backup.Restore):
selectedRestore = -1 #get list of valid restore points
restorePoints = backup.listBackups()
if("archive" in params): pointNames = []
#check that the user give archive exists folderNames = []
if(params['archive'] in folderNames):
#set the index for aDir in restorePoints:
selectedRestore = folderNames.index(params['archive']) pointNames.append(aDir[1])
utils.log(str(selectedRestore) + " : " + params['archive']) folderNames.append(aDir[0])
else:
utils.showNotification(utils.getString(30045)) selectedRestore = -1
utils.log(params['archive'] + ' is not a valid restore point')
else: if("archive" in params):
#allow user to select the backup to restore from #check that the user give archive exists
selectedRestore = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30021),pointNames) if(params['archive'] in folderNames):
#set the index
if(selectedRestore != -1): selectedRestore = folderNames.index(params['archive'])
backup.selectRestore(restorePoints[selectedRestore][0]) utils.log(str(selectedRestore) + " : " + params['archive'])
else:
backup.run(mode) utils.showNotification(utils.getString(30045))
else: utils.log(params['archive'] + ' is not a valid restore point')
#can't go any further else:
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045)) #allow user to select the backup to restore from
utils.openSettings() selectedRestore = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30021),pointNames)
if(selectedRestore != -1):
backup.selectRestore(restorePoints[selectedRestore][0])
if('sets' in params):
backup.restore(selectedSets=params['sets'].split('|'))
else:
backup.restore()
else:
backup.backup()
else:
#can't go any further
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045))
utils.openSettings()

64
launcher.py Normal file
View File

@ -0,0 +1,64 @@
import sys
import urlparse
import xbmc
import xbmcgui
import xbmcvfs
import resources.lib.utils as utils
from resources.lib.authorizers import DropboxAuthorizer,GoogleDriveAuthorizer
from resources.lib.advanced_editor import AdvancedBackupEditor
#launcher for various helpful functions found in the settings.xml area
def authorize_cloud(cloudProvider):
#drobpox
if(cloudProvider == 'dropbox'):
authorizer = DropboxAuthorizer()
if(authorizer.authorize()):
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30027) + ' ' + utils.getString(30106))
else:
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30107) + ' ' + utils.getString(30027))
#google drive
elif(cloudProvider == 'google_drive'):
authorizer = GoogleDriveAuthorizer()
if(authorizer.authorize()):
xbmcgui.Dialog().ok("Backup",utils.getString(30098) + ' ' + utils.getString(30106))
else:
xbmcgui.Dialog().ok("Backup",utils.getString(30107) + ' ' + utils.getString(30098))
def remove_auth():
#triggered from settings.xml - asks if user wants to delete OAuth token information
shouldDelete = xbmcgui.Dialog().yesno(utils.getString(30093),utils.getString(30094),utils.getString(30095),autoclose=7000)
if(shouldDelete):
#delete any of the known token file types
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt")) #dropbox
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "google_drive.dat")) #google drive
def get_params():
param = {}
try:
for i in sys.argv:
args = i
if(args.startswith('?')):
args = args[1:]
param.update(dict(urlparse.parse_qsl(args)))
except:
pass
return param
params = get_params()
if(params['action'] == 'authorize_cloud'):
authorize_cloud(params['provider'])
elif(params['action'] == 'remove_auth'):
remove_auth()
elif(params['action'] == 'advanced_editor'):
editor = AdvancedBackupEditor()
editor.showMainScreen()
elif(params['action'] == 'advanced_copy_config'):
editor = AdvancedBackupEditor()
editor.copySimpleConfig()

View File

@ -1,13 +0,0 @@
import xbmc
import xbmcgui
import xbmcvfs
import resources.lib.utils as utils
#triggered from settings.xml - asks if user wants to delete OAuth token information
shouldDelete = xbmcgui.Dialog().yesno(utils.getString(30093),utils.getString(30094),utils.getString(30095),autoclose=7000)
if(shouldDelete):
#delete any of the known token file types
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt")) #dropbox
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "google_drive.dat")) #google drive

View File

@ -0,0 +1,105 @@
{
"addons":{
"root":"special://home/addons/",
"dirs":[
{
"type":"include",
"path":"special://home/addons/",
"recurse":true
},
{
"type":"exclude",
"path":"special://home/addons/packages/"
},
{
"type":"exclude",
"path":"special://home/addons/temp/"
}
]
},
"addon_data":{
"root":"special://home/userdata/addon_data/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/addon_data/",
"recurse":true
}
]
},
"database":{
"root":"special://home/userdata/Database/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/Database/",
"recurse":true
}
]
},
"game_saves":{
"root":"special://home/userdata/Savestates/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/Savestates/",
"recurse":true
}
]
},
"playlists":{
"root":"special://home/userdata/playlists/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/playlists/",
"recurse":true
}
]
},
"profiles":{
"root":"special://home/userdata/profiles/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/profiles/",
"recurse":true
}
]
},
"thumbnails":{
"root":"special://home/userdata/Thumbnails/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/Thumbnails/",
"recurse":true
}
]
},
"config":{
"root":"special://home/userdata/",
"dirs":[
{
"type":"include",
"path":"special://home/userdata/",
"recurse":false
},
{
"type":"include",
"path":"special://home/userdata/keymaps/",
"recurse":true
},
{
"type":"include",
"path":"special://home/userdata/peripheral_data/",
"recurse":true
},
{
"type":"include",
"path":"special://home/userdata/library/",
"recurse":true
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -48,6 +48,14 @@ msgctxt "#30013"
msgid "Scheduling" msgid "Scheduling"
msgstr "Scheduling" msgstr "Scheduling"
msgctxt "#30014"
msgid "Simple"
msgstr "Simple"
msgctxt "#30015"
msgid "Advanced"
msgstr "Advanced"
msgctxt "#30016" msgctxt "#30016"
msgid "Backup" msgid "Backup"
msgstr "Backup" msgstr "Backup"
@ -129,12 +137,12 @@ msgid "Config Files"
msgstr "Config Files" msgstr "Config Files"
msgctxt "#30036" msgctxt "#30036"
msgid "Custom Directory 1" msgid "Disclaimer"
msgstr "Custom Directory 1" msgstr "Disclaimer"
msgctxt "#30037" msgctxt "#30037"
msgid "Custom Directory 2" msgid "Canceling this menu will close and save changes"
msgstr "Custom Directory 2" msgstr "Canceling this menu will close and save changes"
msgctxt "#30038" msgctxt "#30038"
msgid "Advanced Settings Detected" msgid "Advanced Settings Detected"
@ -420,3 +428,131 @@ msgstr "Visit https://console.developers.google.com/"
msgctxt "#30109" msgctxt "#30109"
msgid "Run on startup if missed" msgid "Run on startup if missed"
msgstr "Run on startup if missed" msgstr "Run on startup if missed"
msgctxt "#30110"
msgid "Set Name"
msgstr ""
msgctxt "#30111"
msgid "Root folder selection"
msgstr ""
msgctxt "#30112"
msgid "Browse Folder"
msgstr ""
msgctxt "#30113"
msgid "Enter Own"
msgstr ""
msgctxt "#30114"
msgid "starts in Kodi home"
msgstr ""
msgctxt "#30115"
msgid "enter path to start there"
msgstr ""
msgctxt "#30116"
msgid "Enter root path"
msgstr ""
msgctxt "#30117"
msgid "Path Error"
msgstr ""
msgctxt "#30118"
msgid "Path does not exist"
msgstr ""
msgctxt "#30119"
msgid "Select root"
msgstr ""
msgctxt "#30120"
msgid "Add Exclude Folder"
msgstr ""
msgctxt "#30121"
msgid "Root Folder"
msgstr ""
msgctxt "#30122"
msgid "Edit"
msgstr ""
msgctxt "#30123"
msgid "Delete"
msgstr ""
msgctxt "#30124"
msgid "Choose Action"
msgstr ""
msgctxt "#30125"
msgid "Advanced Editor"
msgstr ""
msgctxt "#30126"
msgid "Add Set"
msgstr ""
msgctxt "#30127"
msgid "Delete Set"
msgstr ""
msgctxt "#30128"
msgid "Are you sure you want to delete?"
msgstr ""
msgctxt "#30129"
msgid "Exclude"
msgstr ""
msgctxt "#30130"
msgid "The root folder cannot be changed"
msgstr ""
msgctxt "#30131"
msgid "Choose Sets to Restore"
msgstr ""
msgctxt "#30132"
msgid "Version 1.1.4 requires you to setup your file selections again - this is a breaking change"
msgstr ""
msgctxt "#30133"
msgid "Game Saves"
msgstr ""
msgctxt "#30134"
msgid "Include"
msgstr ""
msgctxt "#30135"
msgid "Add Include Folder"
msgstr ""
msgctxt "#30136"
msgid "Path must be within root folder"
msgstr ""
msgctxt "#30137"
msgid "This path is part of a rule already"
msgstr ""
msgctxt "#30138"
msgid "Set Name exists already"
msgstr ""
msgctxt "#30139"
msgid "Copy Simple Config"
msgstr ""
msgctxt "#30140"
msgid "This will copy the default Simple file selection to the Advanced Editor"
msgstr ""
msgctxt "#30141"
msgid "This will erase any current Advanced Editor settings"
msgstr ""

View File

@ -129,14 +129,6 @@ msgctxt "#30035"
msgid "Config Files" msgid "Config Files"
msgstr "Config Files" msgstr "Config Files"
msgctxt "#30036"
msgid "Custom Directory 1"
msgstr "Custom Directory 1"
msgctxt "#30037"
msgid "Custom Directory 2"
msgstr "Custom Directory 2"
msgctxt "#30038" msgctxt "#30038"
msgid "Advanced Settings Detected" msgid "Advanced Settings Detected"
msgstr "Advanced Settings Detected" msgstr "Advanced Settings Detected"

View File

@ -0,0 +1,229 @@
import json
import utils as utils
import xbmcvfs
import xbmc
import xbmcgui
class BackupSetManager:
jsonFile = xbmc.translatePath(utils.data_dir() + "custom_paths.json")
paths = None
def __init__(self):
self.paths = {}
#try and read in the custom file
self._readFile()
def addSet(self,aSet):
self.paths[aSet['name']] = {'root':aSet['root'],'dirs':[{"type":"include","path":aSet['root'],'recurse':True}]}
#save the file
self._writeFile()
def updateSet(self,name,aSet):
self.paths[name] = aSet
#save the file
self._writeFile()
def deleteSet(self,index):
#match the index to a key
keys = self.getSets()
#delete this set
del self.paths[keys[index]]
#save the file
self._writeFile()
def getSets(self):
#list all current sets by name
keys = self.paths.keys()
keys.sort()
return keys
def getSet(self,index):
keys = self.getSets();
#return the set at this index
return {'name':keys[index],'set':self.paths[keys[index]]}
def validateSetName(self,name):
return (name not in self.getSets())
def _writeFile(self):
#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 custom dirs
self.paths = json.loads(aFile.read())
aFile.close()
else:
#write a blank file
self._writeFile()
class AdvancedBackupEditor:
dialog = None
def __init__(self):
self.dialog = xbmcgui.Dialog()
def _cleanPath(self,root,path):
return path[len(root)-1:]
def _validatePath(self,root,path):
return path.startswith(root)
def createSet(self):
backupSet = None
name = self.dialog.input(utils.getString(30110),defaultt='Backup Set')
if(name != None):
#give a choice to start in home or enter a root path
enterHome = self.dialog.yesno(utils.getString(30111),line1=utils.getString(30112) + " - " + utils.getString(30114),line2=utils.getString(30113) + " - " + utils.getString(30115),nolabel=utils.getString(30112),yeslabel=utils.getString(30113))
rootFolder = 'special://home'
if(enterHome):
rootFolder = self.dialog.input(utils.getString(30116),defaultt=rootFolder)
#direcotry has to end in slash
if(rootFolder[:-1] != '/'):
rootFolder = rootFolder + '/'
#check that this path even exists
if(not xbmcvfs.exists(xbmc.translatePath(rootFolder))):
self.dialog.ok(utils.getString(30117),utils.getString(30118),rootFolder)
return None
else:
#select path to start set
rootFolder = self.dialog.browse(type=0,heading=utils.getString(30119),shares='files',defaultt=rootFolder)
backupSet = {'name':name,'root':rootFolder}
return backupSet
def editSet(self,name,backupSet):
optionSelected = ''
rootPath = backupSet['root']
utils.log(rootPath)
while(optionSelected != -1):
options = [xbmcgui.ListItem(utils.getString(30120),"Exclude a specific folder from this backup set"),xbmcgui.ListItem(utils.getString(30135),"Include a specific folder to this backup set"),xbmcgui.ListItem(rootPath,utils.getString(30121))]
for aDir in backupSet['dirs']:
if(aDir['type'] == 'exclude'):
options.append(xbmcgui.ListItem(self._cleanPath(rootPath,aDir['path']),"%s: %s" % ("Type",utils.getString(30129))))
elif(aDir['type'] == 'include'):
options.append(xbmcgui.ListItem(self._cleanPath(rootPath,aDir['path']),"%s: %s | %s: %s" % ("Type",utils.getString(30134),"Include Sub Folders",str(aDir['recurse']))))
optionSelected = self.dialog.select(utils.getString(30122) + ' ' + name,options,useDetails=True)
if(optionSelected == 0 or optionSelected == 1):
#add a folder, will equal root if cancel is hit
addFolder = self.dialog.browse(type=0,heading=utils.getString(30120),shares='files',defaultt=backupSet['root'])
if(addFolder.startswith(rootPath)):
if(not any(addFolder == aDir['path'] for aDir in backupSet['dirs'])):
#cannot add root as an exclusion
if(optionSelected == 0 and addFolder != backupSet['root']):
backupSet['dirs'].append({"path":addFolder,"type":"exclude"})
elif(optionSelected == 1):
#can add root as inclusion
backupSet['dirs'].append({"path":addFolder,"type":"include","recurse":True})
else:
#this path is already part of another include/exclude rule
self.dialog.ok(utils.getString(30117),utils.getString(30137),addFolder)
else:
#folder must be under root folder
self.dialog.ok(utils.getString(30117), utils.getString(30136),rootPath)
elif(optionSelected == 2):
self.dialog.ok(utils.getString(30121),utils.getString(30130),backupSet['root'])
elif(optionSelected > 2):
cOptions = ['Delete']
if(backupSet['dirs'][optionSelected - 3]['type'] == 'include'):
cOptions.append('Toggle Sub Folders')
contextOption = self.dialog.contextmenu(cOptions)
if(contextOption == 0):
if(self.dialog.yesno(heading=utils.getString(30123),line1=utils.getString(30128))):
#remove folder
del backupSet['dirs'][optionSelected - 3]
elif(contextOption == 1 and backupSet['dirs'][optionSelected - 3]['type'] == 'include'):
#toggle if this folder should be recursive
backupSet['dirs'][optionSelected - 3]['recurse'] = not backupSet['dirs'][optionSelected - 3]['recurse']
return backupSet
def showMainScreen(self):
exitCondition = ""
customPaths = BackupSetManager()
#show this every time
self.dialog.ok(utils.getString(30036),utils.getString(30037))
while(exitCondition != -1):
#load the custom paths
options = [xbmcgui.ListItem(utils.getString(30126),'',utils.addon_dir() + '/resources/images/plus-icon.png')]
for index in range(0,len(customPaths.getSets())):
aSet = customPaths.getSet(index)
options.append(xbmcgui.ListItem(aSet['name'],utils.getString(30121) + ': ' + aSet['set']['root'],utils.addon_dir() + '/resources/images/folder-icon.png'))
#show the gui
exitCondition = self.dialog.select(utils.getString(30125),options,useDetails=True)
if(exitCondition >= 0):
if(exitCondition == 0):
newSet = self.createSet()
#check that the name is unique
if(customPaths.validateSetName(newSet['name'])):
customPaths.addSet(newSet)
else:
self.dialog.ok(utils.getString(30117), utils.getString(30138),newSet['name'])
else:
#bring up a context menu
menuOption = self.dialog.contextmenu([utils.getString(30122),utils.getString(30123)])
if(menuOption == 0):
#get the set
aSet = customPaths.getSet(exitCondition -1)
#edit the set
updatedSet = self.editSet(aSet['name'],aSet['set'])
#save it
customPaths.updateSet(aSet['name'],updatedSet)
elif(menuOption == 1):
if(self.dialog.yesno(heading=utils.getString(30127),line1=utils.getString(30128))):
#delete this path - subtract one because of "add" item
customPaths.deleteSet(exitCondition -1)
def copySimpleConfig(self):
#disclaimer in case the user hit this on accident
shouldContinue = self.dialog.yesno(utils.getString(30139),utils.getString(30140),utils.getString(30141))
if(shouldContinue):
source = xbmc.translatePath(utils.addon_dir() + "/resources/data/default_files.json")
dest = xbmc.translatePath(utils.data_dir() + "/custom_paths.json")
xbmcvfs.copy(source,dest)

View File

@ -3,9 +3,18 @@ import xbmcgui
import xbmcvfs import xbmcvfs
import resources.lib.tinyurl as tinyurl import resources.lib.tinyurl as tinyurl
import resources.lib.utils as utils import resources.lib.utils as utils
import dropbox
from resources.lib.pydrive.auth import GoogleAuth #don't die on import error yet, these might not even get used
from resources.lib.pydrive.drive import GoogleDrive try:
import dropbox
except ImportError:
pass
try:
from resources.lib.pydrive.auth import GoogleAuth
from resources.lib.pydrive.drive import GoogleDrive
except ImportError:
pass
class DropboxAuthorizer: class DropboxAuthorizer:
APP_KEY = "" APP_KEY = ""

File diff suppressed because it is too large Load Diff

View File

@ -511,9 +511,5 @@ def _params_to_urlencoded(params):
else: else:
return str(o).encode('utf-8') return str(o).encode('utf-8')
#fix for python 2.6 utf8_params = {encode(k): encode(v) for k, v in six.iteritems(params)}
utf8_params = {}
for k,v in six.iteritems(params):
utf8_params[encode(k)] = encode(v)
return url_encode(utf8_params) return url_encode(utf8_params)

View File

@ -237,12 +237,11 @@ class StoneToPythonPrimitiveSerializer(StoneSerializerBase):
def encode_map(self, validator, value): def encode_map(self, validator, value):
validated_value = validator.validate(value) validated_value = validator.validate(value)
#fix for python 2.6 return {
result = {} self.encode_sub(validator.key_validator, key):
for key, value in validated_value.items(): self.encode_sub(validator.value_validator, value) for
result[self.encode_sub(validator.key_validator,key)] = self.encode_sub(validator.value_validator, value) key, value in validated_value.items()
}
return result
def encode_nullable(self, validator, value): def encode_nullable(self, validator, value):
if value is None: if value is None:
@ -831,12 +830,11 @@ def _decode_list(
if not isinstance(obj, list): if not isinstance(obj, list):
raise bv.ValidationError( raise bv.ValidationError(
'expected list, got %s' % bv.generic_type_name(obj)) 'expected list, got %s' % bv.generic_type_name(obj))
return [
result = [] _json_compat_obj_decode_helper(
for item in obj: data_type.item_validator, item, alias_validators, strict,
result.append(_json_compat_obj_decode_helper(data_type.item_validator, item, alias_validators, strict,old_style, for_msgpack)) old_style, for_msgpack)
for item in obj]
return result
def _decode_map( def _decode_map(
@ -848,12 +846,15 @@ def _decode_map(
if not isinstance(obj, dict): if not isinstance(obj, dict):
raise bv.ValidationError( raise bv.ValidationError(
'expected dict, got %s' % bv.generic_type_name(obj)) 'expected dict, got %s' % bv.generic_type_name(obj))
return {
result = {} _json_compat_obj_decode_helper(
for key, value in obj.items(): data_type.key_validator, key, alias_validators, strict,
result[_json_compat_obj_decode_helper(data_type.key_validator, key, alias_validators, strict,old_style, for_msgpack)] = _json_compat_obj_decode_helper(data_type.value_validator, value, alias_validators, strict,old_style, for_msgpack) old_style, for_msgpack):
_json_compat_obj_decode_helper(
return result data_type.value_validator, value, alias_validators, strict,
old_style, for_msgpack)
for key, value in obj.items()
}
def _decode_nullable( def _decode_nullable(

View File

@ -422,13 +422,10 @@ class Map(Composite):
def validate(self, val): def validate(self, val):
if not isinstance(val, dict): if not isinstance(val, dict):
raise ValidationError('%r is not a valid dict' % val) raise ValidationError('%r is not a valid dict' % val)
return {
#fix for python 2.6 self.key_validator.validate(key):
result = {} self.value_validator.validate(value) for key, value in val.items()
for key, value in val.items(): }
result[self.key_validator.validate(key)] = self.value_validator.validate(value)
return result
class Struct(Composite): class Struct(Composite):

View File

@ -14,11 +14,11 @@ def addon_dir():
def openSettings(): def openSettings():
__Addon.openSettings() __Addon.openSettings()
def log(message,loglevel=xbmc.LOGNOTICE): def log(message,loglevel=xbmc.LOGDEBUG):
xbmc.log(encode(__addon_id__ + "-" + __Addon.getAddonInfo('version') + ": " + message),level=loglevel) xbmc.log(encode(__addon_id__ + "-" + __Addon.getAddonInfo('version') + ": " + message),level=loglevel)
def showNotification(message): def showNotification(message):
xbmcgui.Dialog().notification(encode(getString(30010)),encode(message),time=4000,icon=xbmc.translatePath(__Addon.getAddonInfo('path') + "/resources/media/icon.png")) xbmcgui.Dialog().notification(encode(getString(30010)),encode(message),time=4000,icon=xbmc.translatePath(__Addon.getAddonInfo('path') + "/resources/images/icon.png"))
def getSetting(name): def getSetting(name):
return __Addon.getSetting(name) return __Addon.getSetting(name)
@ -29,6 +29,14 @@ def setSetting(name,value):
def getString(string_id): def getString(string_id):
return __Addon.getLocalizedString(string_id) return __Addon.getLocalizedString(string_id)
def getRegionalTimestamp(date_time,dateformat=['dateshort']):
result = ''
for aFormat in dateformat:
result = result + ("%s " % date_time.strftime(xbmc.getRegion(aFormat)))
return result.strip()
def encode(string): def encode(string):
result = '' result = ''

View File

@ -1,16 +1,12 @@
import utils as utils import utils as utils
import tinyurl as tinyurl
import xbmc import xbmc
import xbmcvfs import xbmcvfs
import xbmcgui import xbmcgui
import zipfile import zipfile
import zlib
import os
import os.path import os.path
import sys import sys
import dropbox import dropbox
from dropbox.files import WriteMode,CommitInfo,UploadSessionCursor from dropbox.files import WriteMode,CommitInfo,UploadSessionCursor
from pydrive.drive import GoogleDrive
from authorizers import DropboxAuthorizer,GoogleDriveAuthorizer from authorizers import DropboxAuthorizer,GoogleDriveAuthorizer
class Vfs: class Vfs:
@ -232,7 +228,7 @@ class DropboxFileSystem(Vfs):
self.client.files_upload_session_append_v2(f.read(self.MAX_CHUNK),upload_cursor) self.client.files_upload_session_append_v2(f.read(self.MAX_CHUNK),upload_cursor)
upload_cursor.offset = f.tell() upload_cursor.offset = f.tell()
#if no errors we're good! #if no errors we're good!
return True return True
except Exception as anError: except Exception as anError:
utils.log(str(anError)) utils.log(str(anError))

View File

@ -4,6 +4,7 @@
<setting id="compress_backups" type="bool" label="30087" default="false" /> <setting id="compress_backups" type="bool" label="30087" default="false" />
<setting id="backup_rotation" type="number" label="30026" default="0" /> <setting id="backup_rotation" type="number" label="30026" default="0" />
<setting id="progress_mode" type="enum" label="30022" lvalues="30082|30083|30084" default="0" /> <setting id="progress_mode" type="enum" label="30022" lvalues="30082|30083|30084" default="0" />
<setting id="upgrade_notes" type="number" label="upgrade_notes" visible="false" default="0" />
</category> </category>
<category id="backup_path" label="30048"> <category id="backup_path" label="30048">
<setting id="remote_selection" type="enum" lvalues="30018|30019|30027|30098" default="0" label="30025"/> <setting id="remote_selection" type="enum" lvalues="30018|30019|30027|30098" default="0" label="30025"/>
@ -13,22 +14,22 @@
<setting id="dropbox_secret" type="text" label="30029" visible="eq(-4,2)" default="" /> <setting id="dropbox_secret" type="text" label="30029" visible="eq(-4,2)" default="" />
<setting id="google_drive_id" type="text" label="Client ID" visible="eq(-5,3)" default="" /> <setting id="google_drive_id" type="text" label="Client ID" visible="eq(-5,3)" default="" />
<setting id="google_drive_secret" type="text" label="Client Secret" visible="eq(-6,3)" default="" /> <setting id="google_drive_secret" type="text" label="Client Secret" visible="eq(-6,3)" default="" />
<setting id="auth_dropbox_button" type="action" label="30104" action="RunScript(special://home/addons/script.xbmcbackup/authorize_cloud.py,type=dropbox)" visible="eq(-7,2)"/> <setting id="auth_dropbox_button" type="action" label="30104" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=authorize_cloud&provider=dropbox)" visible="eq(-7,2)"/>
<setting id="auth_google_button" type="action" label="30104" action="RunScript(special://home/addons/script.xbmcbackup/authorize_cloud.py,type=google_drive)" visible="eq(-8,3)"/> <setting id="auth_google_button" type="action" label="30104" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=authorize_cloud&provider=google_drive)" visible="eq(-8,3)"/>
<setting id="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/remove_auth.py)" visible="gt(-9,1)"/> <setting id="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=remove_auth)" visible="gt(-9,1)"/>
</category> </category>
<category id="selection" label="30012"> <category id="selection" label="30012">
<setting id="backup_addons" type="bool" label="30030" default="true" /> <setting id="backup_selection_type" type="enum" lvalues="30014|30015" default="0" label="30023" />
<setting id="backup_addon_data" type="bool" label="30031" default="false" /> <setting id="backup_addon_data" type="bool" label="30031" default="false" visible="eq(-1,0)"/>
<setting id="backup_database" type="bool" label="30032" default="true" /> <setting id="backup_config" type="bool" label="30035" default="true" visible="eq(-2,0)"/>
<setting id="backup_playlists" type="bool" label="30033" default="true" /> <setting id="backup_database" type="bool" label="30032" default="true" visible="eq(-3,0)"/>
<setting id="backup_profiles" type="bool" label="30080" default="false" /> <setting id="backup_game_saves" type="bool" label="30133" default="false" visible="eq(-4,0)" />
<setting id="backup_thumbnails" type="bool" label="30034" default="true" /> <setting id="backup_playlists" type="bool" label="30033" default="true" visible="eq(-5,0)"/>
<setting id="backup_config" type="bool" label="30035" default="true" /> <setting id="backup_profiles" type="bool" label="30080" default="false" visible="eq(-6,0)"/>
<setting id="custom_dir_1_enable" type="bool" label="30036" default="false" /> <setting id="backup_thumbnails" type="bool" label="30034" default="true" visible="eq(-7,0)"/>
<setting id="backup_custom_dir_1" type="folder" label="30018" default="" visible="eq(-1,true)"/> <setting id="backup_addons" type="bool" label="30030" default="true" visible="eq(-8,0)" />
<setting id="custom_dir_2_enable" type="bool" label="30037" default="false" /> <setting id="advanced_button" type="action" label="30125" visible="eq(-9,1)" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_editor)" />
<setting id="backup_custom_dir_2" type="folder" label="30018" default="" visible="eq(-1,true)"/> <setting id="advanced_defaults" type="action" label="30139" visible="eq(-10,1)" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_copy_config)" />
</category> </category>
<category id="scheduling" label="30013"> <category id="scheduling" label="30013">
<setting id="enable_scheduler" type="bool" label="30060" default="false" /> <setting id="enable_scheduler" type="bool" label="30060" default="false" />

View File

@ -1,191 +1,198 @@
import xbmc import xbmc
import xbmcvfs import xbmcvfs
import xbmcgui import xbmcgui
import datetime from datetime import datetime
import time import time
import resources.lib.utils as utils import resources.lib.utils as utils
from resources.lib.croniter import croniter from resources.lib.croniter import croniter
from resources.lib.backup import XbmcBackup from resources.lib.backup import XbmcBackup
class BackupScheduler: UPGRADE_INT = 1 #to keep track of any upgrade notifications
monitor = None
enabled = "false" class BackupScheduler:
next_run = 0 monitor = None
next_run_path = None enabled = "false"
restore_point = None next_run = 0
next_run_path = None
def __init__(self): restore_point = None
self.monitor = UpdateMonitor(update_method = self.settingsChanged)
self.enabled = utils.getSetting("enable_scheduler") def __init__(self):
self.next_run_path = xbmc.translatePath(utils.data_dir()) + 'next_run.txt' self.monitor = UpdateMonitor(update_method = self.settingsChanged)
self.enabled = utils.getSetting("enable_scheduler")
if(self.enabled == "true"): self.next_run_path = xbmc.translatePath(utils.data_dir()) + 'next_run.txt'
#sleep for 2 minutes so Kodi can start and time can update correctly if(self.enabled == "true"):
xbmc.Monitor().waitForAbort(120)
#sleep for 2 minutes so Kodi can start and time can update correctly
nr = 0 xbmc.Monitor().waitForAbort(120)
if(xbmcvfs.exists(self.next_run_path)):
nr = 0
fh = xbmcvfs.File(self.next_run_path) if(xbmcvfs.exists(self.next_run_path)):
try:
#check if we saved a run time from the last run fh = xbmcvfs.File(self.next_run_path)
nr = float(fh.read()) try:
except ValueError: #check if we saved a run time from the last run
nr = 0 nr = float(fh.read())
except ValueError:
fh.close() nr = 0
#if we missed and the user wants to play catch-up fh.close()
if(0 < nr <= time.time() and utils.getSetting('schedule_miss') == 'true'):
utils.log("scheduled backup was missed, doing it now...") #if we missed and the user wants to play catch-up
progress_mode = int(utils.getSetting('progress_mode')) if(0 < nr <= time.time() and utils.getSetting('schedule_miss') == 'true'):
utils.log("scheduled backup was missed, doing it now...")
if(progress_mode == 0): progress_mode = int(utils.getSetting('progress_mode'))
progress_mode = 1 # Kodi just started, don't block it with a foreground progress bar
if(progress_mode == 0):
self.doScheduledBackup(progress_mode) progress_mode = 1 # Kodi just started, don't block it with a foreground progress bar
self.setup() self.doScheduledBackup(progress_mode)
def setup(self): self.setup()
#scheduler was turned on, find next run time
utils.log("scheduler enabled, finding next run time") def setup(self):
self.findNextRun(time.time()) #scheduler was turned on, find next run time
utils.log("scheduler enabled, finding next run time")
def start(self): self.findNextRun(time.time())
#check if a backup should be resumed def start(self):
resumeRestore = self._resumeCheck()
#display upgrade messages if they exist
if(resumeRestore): if(int(utils.getSetting('upgrade_notes')) < UPGRADE_INT):
restore = XbmcBackup() xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30132))
restore.selectRestore(self.restore_point) utils.setSetting('upgrade_notes',str(UPGRADE_INT))
#skip the advanced settings check
restore.skipAdvanced() #check if a backup should be resumed
restore.run(XbmcBackup.Restore) resumeRestore = self._resumeCheck()
while(not self.monitor.abortRequested()): if(resumeRestore):
restore = XbmcBackup()
if(self.enabled == "true"): restore.selectRestore(self.restore_point)
#scheduler is still on #skip the advanced settings check
now = time.time() restore.skipAdvanced()
restore.restore()
if(self.next_run <= now):
progress_mode = int(utils.getSetting('progress_mode')) while(not self.monitor.abortRequested()):
self.doScheduledBackup(progress_mode)
if(self.enabled == "true"):
#check if we should shut the computer down #scheduler is still on
if(utils.getSetting("cron_shutdown") == 'true'): now = time.time()
#wait 10 seconds to make sure all backup processes and files are completed
time.sleep(10) if(self.next_run <= now):
xbmc.executebuiltin('ShutDown()') progress_mode = int(utils.getSetting('progress_mode'))
else: self.doScheduledBackup(progress_mode)
#find the next run time like normal
self.findNextRun(now) #check if we should shut the computer down
if(utils.getSetting("cron_shutdown") == 'true'):
xbmc.sleep(500) #wait 10 seconds to make sure all backup processes and files are completed
time.sleep(10)
#delete monitor to free up memory xbmc.executebuiltin('ShutDown()')
del self.monitor else:
#find the next run time like normal
def doScheduledBackup(self,progress_mode): self.findNextRun(now)
if(progress_mode != 2):
utils.showNotification(utils.getString(30053)) xbmc.sleep(500)
backup = XbmcBackup() #delete monitor to free up memory
del self.monitor
if(backup.remoteConfigured()):
def doScheduledBackup(self,progress_mode):
if(int(utils.getSetting('progress_mode')) in [0,1]): if(progress_mode != 2):
backup.run(XbmcBackup.Backup,True) utils.showNotification(utils.getString(30053))
else:
backup.run(XbmcBackup.Backup,False) backup = XbmcBackup()
#check if this is a "one-off" if(backup.remoteConfigured()):
if(int(utils.getSetting("schedule_interval")) == 0):
#disable the scheduler after this run if(int(utils.getSetting('progress_mode')) in [0,1]):
self.enabled = "false" backup.backup(True)
utils.setSetting('enable_scheduler','false') else:
else: backup.backup(False)
utils.showNotification(utils.getString(30045))
#check if this is a "one-off"
def findNextRun(self,now): if(int(utils.getSetting("schedule_interval")) == 0):
progress_mode = int(utils.getSetting('progress_mode')) #disable the scheduler after this run
self.enabled = "false"
#find the cron expression and get the next run time utils.setSetting('enable_scheduler','false')
cron_exp = self.parseSchedule() else:
utils.showNotification(utils.getString(30045))
cron_ob = croniter(cron_exp,datetime.datetime.fromtimestamp(now))
new_run_time = cron_ob.get_next(float) def findNextRun(self,now):
progress_mode = int(utils.getSetting('progress_mode'))
if(new_run_time != self.next_run):
self.next_run = new_run_time #find the cron expression and get the next run time
utils.log("scheduler will run again on " + datetime.datetime.fromtimestamp(self.next_run).strftime('%m-%d-%Y %H:%M')) cron_exp = self.parseSchedule()
#write the next time to a file cron_ob = croniter(cron_exp,datetime.fromtimestamp(now))
fh = xbmcvfs.File(self.next_run_path, 'w') new_run_time = cron_ob.get_next(float)
fh.write(str(self.next_run))
fh.close() if(new_run_time != self.next_run):
self.next_run = new_run_time
#only show when not in silent mode utils.log("scheduler will run again on " + utils.getRegionalTimestamp(datetime.fromtimestamp(self.next_run),['dateshort','time']))
if(progress_mode != 2):
utils.showNotification(utils.getString(30081) + " " + datetime.datetime.fromtimestamp(self.next_run).strftime('%m-%d-%Y %H:%M')) #write the next time to a file
fh = xbmcvfs.File(self.next_run_path, 'w')
def settingsChanged(self): fh.write(str(self.next_run))
current_enabled = utils.getSetting("enable_scheduler") fh.close()
if(current_enabled == "true" and self.enabled == "false"): #only show when not in silent mode
#scheduler was just turned on if(progress_mode != 2):
self.enabled = current_enabled utils.showNotification(utils.getString(30081) + " " + utils.getRegionalTimestamp(datetime.fromtimestamp(self.next_run),['dateshort','time']))
self.setup()
elif (current_enabled == "false" and self.enabled == "true"): def settingsChanged(self):
#schedule was turn off current_enabled = utils.getSetting("enable_scheduler")
self.enabled = current_enabled
if(current_enabled == "true" and self.enabled == "false"):
if(self.enabled == "true"): #scheduler was just turned on
#always recheck the next run time after an update self.enabled = current_enabled
self.findNextRun(time.time()) self.setup()
elif (current_enabled == "false" and self.enabled == "true"):
def parseSchedule(self): #schedule was turn off
schedule_type = int(utils.getSetting("schedule_interval")) self.enabled = current_enabled
cron_exp = utils.getSetting("cron_schedule")
if(self.enabled == "true"):
hour_of_day = utils.getSetting("schedule_time") #always recheck the next run time after an update
hour_of_day = int(hour_of_day[0:2]) self.findNextRun(time.time())
if(schedule_type == 0 or schedule_type == 1):
#every day def parseSchedule(self):
cron_exp = "0 " + str(hour_of_day) + " * * *" schedule_type = int(utils.getSetting("schedule_interval"))
elif(schedule_type == 2): cron_exp = utils.getSetting("cron_schedule")
#once a week
day_of_week = utils.getSetting("day_of_week") hour_of_day = utils.getSetting("schedule_time")
cron_exp = "0 " + str(hour_of_day) + " * * " + day_of_week hour_of_day = int(hour_of_day[0:2])
elif(schedule_type == 3): if(schedule_type == 0 or schedule_type == 1):
#first day of month #every day
cron_exp = "0 " + str(hour_of_day) + " 1 * *" cron_exp = "0 " + str(hour_of_day) + " * * *"
elif(schedule_type == 2):
return cron_exp #once a week
day_of_week = utils.getSetting("day_of_week")
def _resumeCheck(self): cron_exp = "0 " + str(hour_of_day) + " * * " + day_of_week
shouldContinue = False elif(schedule_type == 3):
if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "resume.txt"))): #first day of month
rFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"),'r') cron_exp = "0 " + str(hour_of_day) + " 1 * *"
self.restore_point = rFile.read()
rFile.close() return cron_exp
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "resume.txt"))
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30042),utils.getString(30043),utils.getString(30044)) def _resumeCheck(self):
shouldContinue = False
return shouldContinue if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "resume.txt"))):
rFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"),'r')
self.restore_point = rFile.read()
class UpdateMonitor(xbmc.Monitor): rFile.close()
update_method = None xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "resume.txt"))
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30042),utils.getString(30043),utils.getString(30044))
def __init__(self,*args, **kwargs):
xbmc.Monitor.__init__(self) return shouldContinue
self.update_method = kwargs['update_method']
def onSettingsChanged(self): class UpdateMonitor(xbmc.Monitor):
self.update_method() update_method = None
BackupScheduler().start() def __init__(self,*args, **kwargs):
xbmc.Monitor.__init__(self)
self.update_method = kwargs['update_method']
def onSettingsChanged(self):
self.update_method()
BackupScheduler().start()