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
| @@ -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 | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								addon.xml
									
									
									
									
									
								
							
							
						
						| @@ -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 | ||||||
|   | |||||||
| @@ -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)) |  | ||||||
							
								
								
									
										168
									
								
								default.py
									
									
									
									
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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() | ||||||
| @@ -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 |  | ||||||
|      |  | ||||||
							
								
								
									
										105
									
								
								resources/data/default_files.json
									
									
									
									
									
										Normal 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 | ||||||
|  | 			} | ||||||
|  | 		] | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								resources/images/folder-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 226 B | 
| Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/images/plus-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 196 B | 
							
								
								
									
										
											BIN
										
									
								
								resources/images/screenshot1.PNG
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 125 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/images/screenshot2.PNG
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 129 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/images/screenshot3.PNG
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 270 KiB | 
							
								
								
									
										
											BIN
										
									
								
								resources/images/screenshot4.PNG
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 188 KiB | 
| @@ -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 "" | ||||||
| @@ -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" | ||||||
|   | |||||||
							
								
								
									
										229
									
								
								resources/lib/advanced_editor.py
									
									
									
									
									
										Normal 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) | ||||||
|  |  | ||||||
|  |                | ||||||
|  |  | ||||||
| @@ -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 = "" | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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): | ||||||
|   | |||||||
| @@ -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 = '' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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)) | ||||||
|   | |||||||
| @@ -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" /> | ||||||
|   | |||||||
							
								
								
									
										389
									
								
								scheduler.py
									
									
									
									
									
								
							
							
						
						| @@ -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() | ||||||
|   | |||||||
 Rob
					Rob