3 Commits

Author SHA1 Message Date
Rob Weber
67cd5ae069 Merge branch 'master' into jarvis 2019-08-26 14:54:42 -05:00
Rob Weber
46257f1626 jarvis icon.png needs to be in root directory 2019-08-20 09:41:30 -05:00
Rob Weber
36f094a258 added version compatibility info 2019-08-20 09:39:57 -05:00
27 changed files with 1043 additions and 1535 deletions

View File

@@ -1,6 +1,6 @@
# Backup Addon # Backup Addon
__Kodi Version Compatibility:__ Kodi 17.x (Krypton) and greater __Kodi Version Compatibility:__ Kodi 16.x (Jarvis) and greater
## About ## About
@@ -19,11 +19,6 @@ For more specific information please check out the [wiki on Github](https://gith
* [FAQ](https://github.com/robweber/xbmcbackup/wiki/FAQ) * [FAQ](https://github.com/robweber/xbmcbackup/wiki/FAQ)
## Attributions
Icon files from Open Iconic — www.useiconic.com/open

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.xbmcbackup" <addon id="script.xbmcbackup"
name="Backup" version="1.5.0" provider-name="robweber"> name="Backup" version="1.1.3" provider-name="robweber">
<requires> <requires>
<!-- jarvis --> <!-- jarvis -->
<import addon="xbmc.python" version="2.25.0"/> <import addon="xbmc.python" version="2.24.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" />
@@ -87,14 +87,6 @@
<license>The MIT License</license> <license>The MIT License</license>
<forum>https://forum.kodi.tv/showthread.php?tid=129499</forum> <forum>https://forum.kodi.tv/showthread.php?tid=129499</forum>
<source>https://github.com/robweber/xbmcbackup</source> <source>https://github.com/robweber/xbmcbackup</source>
<email></email>
<assets>
<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>
<news>Version 1.1.4 <news>Version 1.1.4
- added file chunk support for dropbox uploads - added file chunk support for dropbox uploads
- fixed settings duplicate ids, thanks aster-anto - fixed settings duplicate ids, thanks aster-anto

37
authorize_cloud.py Normal file
View File

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

View File

@@ -1,7 +1,3 @@
Version 1.5.0
Overhaul of file selection and restore procedures. Breaking Change with previous versions PR117
Version 1.1.3 Version 1.1.3
added file chunk support for dropbox uploads added file chunk support for dropbox uploads

View File

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

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -1,64 +0,0 @@
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()

13
remove_auth.py Normal file
View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

View File

@@ -48,14 +48,6 @@ 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"
@@ -137,12 +129,12 @@ msgid "Config Files"
msgstr "Config Files" msgstr "Config Files"
msgctxt "#30036" msgctxt "#30036"
msgid "Disclaimer" msgid "Custom Directory 1"
msgstr "Disclaimer" msgstr "Custom Directory 1"
msgctxt "#30037" msgctxt "#30037"
msgid "Canceling this menu will close and save changes" msgid "Custom Directory 2"
msgstr "Canceling this menu will close and save changes" msgstr "Custom Directory 2"
msgctxt "#30038" msgctxt "#30038"
msgid "Advanced Settings Detected" msgid "Advanced Settings Detected"
@@ -428,131 +420,3 @@ 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.5.0 requires you to setup your file selections again - this is a breaking change"
msgstr ""
msgctxt "#30133"
msgid "Game Saves"
msgstr ""
msgctxt "#30134"
msgid "Include"
msgstr ""
msgctxt "#30135"
msgid "Add Include Folder"
msgstr ""
msgctxt "#30136"
msgid "Path must be within root folder"
msgstr ""
msgctxt "#30137"
msgid "This path is part of a rule already"
msgstr ""
msgctxt "#30138"
msgid "Set Name exists already"
msgstr ""
msgctxt "#30139"
msgid "Copy Simple Config"
msgstr ""
msgctxt "#30140"
msgid "This will copy the default Simple file selection to the Advanced Editor"
msgstr ""
msgctxt "#30141"
msgid "This will erase any current Advanced Editor settings"
msgstr ""

View File

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

View File

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

View File

@@ -3,18 +3,9 @@ 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
#don't die on import error yet, these might not even get used from resources.lib.pydrive.auth import GoogleAuth
try: from resources.lib.pydrive.drive import GoogleDrive
import dropbox
except ImportError:
pass
try:
from resources.lib.pydrive.auth import GoogleAuth
from resources.lib.pydrive.drive import GoogleDrive
except ImportError:
pass
class DropboxAuthorizer: class DropboxAuthorizer:
APP_KEY = "" APP_KEY = ""

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -237,11 +237,12 @@ 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)
return { #fix for python 2.6
self.encode_sub(validator.key_validator, key): result = {}
self.encode_sub(validator.value_validator, value) for for key, value in validated_value.items():
key, value in validated_value.items() result[self.encode_sub(validator.key_validator,key)] = self.encode_sub(validator.value_validator, value)
}
return result
def encode_nullable(self, validator, value): def encode_nullable(self, validator, value):
if value is None: if value is None:
@@ -830,11 +831,12 @@ 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 [
_json_compat_obj_decode_helper( result = []
data_type.item_validator, item, alias_validators, strict, for item in obj:
old_style, for_msgpack) result.append(_json_compat_obj_decode_helper(data_type.item_validator, item, alias_validators, strict,old_style, for_msgpack))
for item in obj]
return result
def _decode_map( def _decode_map(
@@ -846,15 +848,12 @@ 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 {
_json_compat_obj_decode_helper( result = {}
data_type.key_validator, key, alias_validators, strict, for key, value in obj.items():
old_style, for_msgpack): 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)
_json_compat_obj_decode_helper(
data_type.value_validator, value, alias_validators, strict, return result
old_style, for_msgpack)
for key, value in obj.items()
}
def _decode_nullable( def _decode_nullable(

View File

@@ -422,10 +422,13 @@ 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 {
self.key_validator.validate(key): #fix for python 2.6
self.value_validator.validate(value) for key, value in val.items() result = {}
} for key, value in val.items():
result[self.key_validator.validate(key)] = self.value_validator.validate(value)
return result
class Struct(Composite): class Struct(Composite):

View File

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

View File

@@ -1,12 +1,16 @@
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:
@@ -228,7 +232,7 @@ class DropboxFileSystem(Vfs):
self.client.files_upload_session_append_v2(f.read(self.MAX_CHUNK),upload_cursor) self.client.files_upload_session_append_v2(f.read(self.MAX_CHUNK),upload_cursor)
upload_cursor.offset = f.tell() upload_cursor.offset = f.tell()
#if no errors we're good! #if no errors we're good!
return True return True
except Exception as anError: except Exception as anError:
utils.log(str(anError)) utils.log(str(anError))

View File

@@ -4,7 +4,6 @@
<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="1" />
</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"/>
@@ -14,22 +13,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/launcher.py,action=authorize_cloud&provider=dropbox)" visible="eq(-7,2)"/> <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_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="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="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=remove_auth)" visible="gt(-9,1)"/> <setting id="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/remove_auth.py)" visible="gt(-9,1)"/>
</category> </category>
<category id="selection" label="30012"> <category id="selection" label="30012">
<setting id="backup_selection_type" type="enum" lvalues="30014|30015" default="0" label="30023" /> <setting id="backup_addons" type="bool" label="30030" default="true" />
<setting id="backup_addon_data" type="bool" label="30031" default="false" visible="eq(-1,0)"/> <setting id="backup_addon_data" type="bool" label="30031" default="false" />
<setting id="backup_config" type="bool" label="30035" default="true" visible="eq(-2,0)"/> <setting id="backup_database" type="bool" label="30032" default="true" />
<setting id="backup_database" type="bool" label="30032" default="true" visible="eq(-3,0)"/> <setting id="backup_playlists" type="bool" label="30033" default="true" />
<setting id="backup_game_saves" type="bool" label="30133" default="false" visible="eq(-4,0)" /> <setting id="backup_profiles" type="bool" label="30080" default="false" />
<setting id="backup_playlists" type="bool" label="30033" default="true" visible="eq(-5,0)"/> <setting id="backup_thumbnails" type="bool" label="30034" default="true" />
<setting id="backup_profiles" type="bool" label="30080" default="false" visible="eq(-6,0)"/> <setting id="backup_config" type="bool" label="30035" default="true" />
<setting id="backup_thumbnails" type="bool" label="30034" default="true" visible="eq(-7,0)"/> <setting id="custom_dir_1_enable" type="bool" label="30036" default="false" />
<setting id="backup_addons" type="bool" label="30030" default="true" visible="eq(-8,0)" /> <setting id="backup_custom_dir_1" type="folder" label="30018" default="" visible="eq(-1,true)"/>
<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="custom_dir_2_enable" type="bool" label="30037" default="false" />
<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)" /> <setting id="backup_custom_dir_2" type="folder" label="30018" default="" visible="eq(-1,true)"/>
</category> </category>
<category id="scheduling" label="30013"> <category id="scheduling" label="30013">
<setting id="enable_scheduler" type="bool" label="30060" default="false" /> <setting id="enable_scheduler" type="bool" label="30060" default="false" />

View File

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