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()
|
||||||
|