78 Commits

Author SHA1 Message Date
Rob Weber
40b6260521 version bump 2021-04-16 09:40:31 -05:00
Rob Weber
5aad014dbc update changelog 2021-04-16 09:36:26 -05:00
Rob Weber
ef3b820ca5 fix some gui dialog prompts 2021-04-16 09:34:43 -05:00
Rob Weber
76e8e0efeb check for restores right away 2021-04-16 09:30:22 -05:00
Rob Weber
f7e77fd739 fixes #190 - need to set file number to 1 2021-04-16 09:30:11 -05:00
Rob Weber
382dbce4ac added qrcode for dropbox setup 2021-04-08 15:43:26 -05:00
Rob
4b066432be Launcher (#189)
* move launcher code to default.py, modify RunScript in settings
2021-04-01 15:33:38 -05:00
Rob Weber
d71c923e78 forgot to add release date 2021-03-15 13:44:46 -05:00
Rob Weber
b7587c6170 version bump 2021-03-15 13:39:07 -05:00
robweber
79cddb422c part of #186 2021-03-14 21:18:28 -05:00
robweber
8415ec12ba updated changelog.md 2021-03-07 14:30:31 -06:00
robweber
a284451640 part of #186 2021-03-07 14:30:17 -06:00
robweber
dc8d334352 fix addon.xml version 2021-03-06 19:20:54 -06:00
robweber
c17a185639 version bump 2021-03-06 19:20:31 -06:00
robweber
aff124af1f more of #183 2021-03-06 19:11:26 -06:00
robweber
350d81caf4 updated changelog.md 2021-02-21 21:27:04 -06:00
robweber
46d7d22523 part of #183 2021-02-21 21:25:56 -06:00
Rob Weber
11c644cb15 added help strings 2021-01-20 14:35:52 -06:00
Rob Weber
941b593751 updated change log 2021-01-20 14:27:34 -06:00
Rob Weber
e622a0455f fixes #83 2021-01-20 14:25:48 -06:00
Rob Weber
9c1ecc254f path to this folder was incorrect 2021-01-18 13:48:17 -06:00
Rob Weber
44fdf7a20a rmdir function only accepts one arg now 2021-01-18 13:10:50 -06:00
Rob Weber
ec214c074f simplified path expansions for special://temp zip file location 2021-01-18 09:01:19 -06:00
Rob
02d852a7e9 Matrix Settings (#179)
* added settings levels
2021-01-17 14:54:10 -06:00
Rob Weber
d7e4946d9a fix travis link 2020-12-23 15:52:05 -06:00
Rob Weber
6fdaa4f253 fix syntax error 2020-12-23 15:51:38 -06:00
Rob Weber
5f1f9fef38 version bump 2020-12-23 15:44:32 -06:00
Rob Weber
4098cd18cb updated changelog.md 2020-12-18 09:32:19 -06:00
Rob Weber
8119a09449 fix oauth import 2020-12-18 09:31:52 -06:00
Rob Weber
a0ccd85d9e part of #174, fixes ok() method def change 2020-12-18 09:28:17 -06:00
Rob
71c8d9ae54 ui settings restore upgrade
* added ability to export/save settings as json using GetSettings

* added generic copyFile method instead of duplicating

* copy and load settings file after file restore (right now only reads)

* set settings values from backup when differ than current

* store settings as part of validation file

* prompt for settings restore or set always via toggle

* unused import

* added new strings for settings restore

* updated changelog

* fix pep8 syntax

* swap setting to always prompt instead of always restore (invert)
2020-12-03 14:08:25 -06:00
Rob Weber
b470412b4f removed unused imports 2020-11-18 14:28:09 -06:00
Rob Weber
7a5886cd26 beta version, updated news 2020-11-18 14:10:22 -06:00
Rob Weber
f20887b6e7 update travis CI badge 2020-11-18 14:09:22 -06:00
Rob Weber
a0eb28a5f6 convert xbmc.translatePath to xbmcvfs.translatePath 2020-11-18 14:07:06 -06:00
Rob Weber
a198c9448a update future module 2020-11-17 19:56:15 -06:00
Rob Weber
fa3a30eb55 added flake8 pollyfill dep 2020-11-17 12:55:05 -06:00
Rob Weber
201d04afeb updated version number 2020-11-17 09:54:14 -06:00
Rob Weber
2dabb23c2d Merge branch 'matrix'
# Conflicts:
#	README.md
2020-11-17 09:52:39 -06:00
Rob Weber
2f19ec2b75 added download stats to README.md 2020-06-18 15:37:12 -05:00
Rob Weber
db215873cf updated badge path 2020-06-17 13:51:49 -05:00
Rob Weber
bd963719d4 updated badge path 2020-06-17 13:51:00 -05:00
Rob Weber
18b7f338c7 added attributes file to export-ignore 2020-06-16 15:07:16 -05:00
Rob Weber
92a9245bdc added export ignore items 2020-06-16 15:02:21 -05:00
Rob Weber
568c3758a4 add export-ignore files 2020-06-16 14:54:15 -05:00
robweber
af999f7d04 wrong rep 2020-06-15 20:56:11 -05:00
robweber
1264ab86b2 space 2020-06-15 20:48:15 -05:00
robweber
90c458d4fc updated changelog.md 2020-06-15 20:46:39 -05:00
robweber
0e6f5acfb5 added deploy script using kodi-addon-submitter 2020-06-15 20:44:41 -05:00
Wuff
c9dd381037 Version 1.6.3 - fix validatePath error (issue 166) (#167) 2020-05-20 13:01:43 -05:00
Rob Weber
55b2ac83d4 version bump 2020-04-09 14:37:09 -05:00
Rob Weber
0d14dd17c6 replace png path with jpg #165 2020-04-09 13:53:29 -05:00
Rob Weber
9fa354b467 replace PNG screenshots with JPG #165 2020-04-09 13:50:34 -05:00
Oldřich Jedlička
006485b19e Remove upper-case PNGs (#164)
Signed-off-by: Oldřich Jedlička <oldium.pro@gmail.com>
2020-02-11 15:23:52 -06:00
Rob Weber
3c2f512ecf update changelog 2019-12-31 11:03:18 -06:00
Rob Weber
190b4fd86f one more context lib 2019-12-31 11:03:11 -06:00
Rob Weber
9ecf706d63 use contextlib 2019-12-31 10:49:16 -06:00
Rob Weber
05c53b7ed8 localize advanced editor strings 2019-12-31 10:41:15 -06:00
Rob Weber
8bc73f2832 don't need this file 2019-12-31 10:33:13 -06:00
Rob Weber
edc4a7b20f updated to release version v1.6.1 2019-12-30 10:21:01 -06:00
Rob Weber
90b4aeeebe pep8 fixes 2019-12-30 10:17:58 -06:00
Rob Weber
7ce9123e1f updated changelog 2019-12-30 10:14:35 -06:00
Rob Weber
8bfef6692f Merge branch 'master' into matrix 2019-12-30 10:13:23 -06:00
Rob Weber
e63560f0c4 added a clean path function and applied it to rotate backups 2019-12-30 10:09:01 -06:00
Wuff
51f2ef3973 Fix deleting old backups on remote drives + log error (#163) 2019-12-30 09:59:03 -06:00
Rob Weber
04bac77690 moved scheduler to resources/lib and created non-complex entry point 2019-12-17 15:02:07 -06:00
Rob Weber
b1f6d36d73 updated news and version (beta2) 2019-12-10 15:24:56 -06:00
Rob
5d398836ba Show File Transfer Size (#160)
adds file transfer size to progress bar - closes #157
2019-12-10 15:16:54 -06:00
Rob Weber
23a14d67c4 Merge branch '1.6.0_fixes' into matrix 2019-12-06 13:56:06 -06:00
Rob Weber
47fcb119f3 pull build from matrix branch 2019-12-04 11:53:51 -06:00
Rob Weber
f9f49e3fe6 Merge branch 'master' into matrix 2019-12-04 11:53:12 -06:00
Rob Weber
7c23c17e33 Merge branch '1.6.0_fixes' 2019-12-04 11:49:35 -06:00
Rob Weber
8d66fa6a9f part of #159 - this will get rid of the most significant logging and keep the essentials 2019-11-27 14:24:35 -06:00
Rob Weber
5ee610a586 update getSetting calls to get ints and bools where needed 2019-11-27 14:19:41 -06:00
Rob Weber
8c4465f552 add methods for getting bools and int values directly 2019-11-27 14:19:25 -06:00
Rob Weber
3849a902ea working 16.1 beta 2019-11-27 14:05:08 -06:00
Rob Weber
4492ab593e update news 2019-11-27 13:14:42 -06:00
Rob Weber
0cc0684263 Use the dropbox module instead of bundling code - module still needs to be updated in Kodi repo 2019-11-26 12:58:35 -06:00
40 changed files with 1223 additions and 555 deletions

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
.github/ export-ignore
.settings/ export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
README.md export-ignore
changelog.md export-ignore

View File

@@ -3,7 +3,7 @@ language: python
python: 3.7
install:
- pip install flake8 kodi-addon-checker
- pip install flake8 flake8_polyfill kodi-addon-checker git+https://github.com/romanvm/kodi-addon-submitter.git
before_script:
- git config core.quotepath false
@@ -11,4 +11,10 @@ before_script:
# command to run our tests
script:
- flake8 ./ --statistics --show-source --builtins=sys --ignore=E501,E722 --exclude=croniter.py # check python structure against flake8 tests, ignore long lines
- kodi-addon-checker --branch=matrix --allow-folder-id-mismatch
- kodi-addon-checker --branch=matrix --allow-folder-id-mismatch
deploy:
- provider: script
script: submit-addon -r repo-scripts -b matrix --push-branch script.xbmcbackup
on:
tags: true

View File

@@ -1,13 +1,13 @@
# Backup Addon
![Kodi Version](https://img.shields.io/endpoint?url=https%3A%2F%2Fweberjr.com%2Fkodi-shield%2Frobweber%2Fxbmcbackup%2Fmaster%2Ftrue%2Ftrue) [![Build Status](https://img.shields.io/travis/robweber/xbmcbackup/master)](https://travis-ci.org/robweber/xbmcbackup) [![License](https://img.shields.io/github/license/robweber/xbmcbackup)](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [![PEP8](https://img.shields.io/badge/code%20style-pep8-orange.svg)](https://www.python.org/dev/peps/pep-0008/)
![Kodi Version](https://img.shields.io/endpoint?url=https%3A%2F%2Fweberjr.com%2Fkodi-shield%2Fversion%2Frobweber%2Fxbmcbackup%2Fmatrix%2Ftrue%2Ftrue) ![Total Downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fweberjr.com%2Fkodi-shield%2Fdownloads%2Fmatrix%2Fscript.xbmcbackup%2F1.6.7) [![Build Status](https://img.shields.io/travis/com/robweber/xbmcbackup/matrix)](https://travis-ci.com/robweber/xbmcbackup) [![License](https://img.shields.io/github/license/robweber/xbmcbackup)](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [![PEP8](https://img.shields.io/badge/code%20style-pep8-orange.svg)](https://www.python.org/dev/peps/pep-0008/)
## 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.
## Running the Program
Running the program will allow you to select Backup or Restore as a running mode. Selecting Backup will push files to your remote store using the addon settings you defined. Selecting Restore will give you a list of restore points currently in your remote destination. Selecting one will pull the files matching your selection criteria from the restore point to your local Kodi folders.
Running the program will allow you to select Backup or Restore as a running mode. Selecting Backup will push files to your remote store using the addon settings you defined. Selecting Restore will give you a list of restore points currently in your remote destination. Selecting one will pull the files matching your selection criteria from the restore point to your local Kodi folders.
For more specific information please check out the [wiki on Github](https://github.com/robweber/xbmcbackup/wiki) for this project. Advanced descriptions for the following are all there:
@@ -15,17 +15,9 @@ For more specific information please check out the [wiki on Github](https://gith
* [Cloud Storage](https://github.com/robweber/xbmcbackup/wiki/Cloud-Storage)
* [Scheduling](https://github.com/robweber/xbmcbackup/wiki/Scheduling)
* [Scripting](https://github.com/robweber/xbmcbackup/wiki/Scripting)
* [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,16 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.xbmcbackup"
name="Backup" version="1.6.0" provider-name="robweber">
name="Backup" version="1.6.7" provider-name="robweber">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.dateutil" version="2.8.0" />
<import addon="script.module.future" version="0.16.0.4"/>
<import addon="script.module.dropbox" version="9.4.0"/>
<import addon="script.module.future" version="0.18.2+matrix.1" />
<import addon="script.module.dropbox" version="9.4.0" />
<import addon="script.module.pyqrcode" version="1.2.1+matrix.1" />
</requires>
<extension point="xbmc.python.script" library="default.py">
<provides>executable</provides>
</extension>
<extension point="xbmc.service" library="scheduler.py" />
<extension point="xbmc.python.script" library="default.py">
<provides>executable</provides>
</extension>
<extension point="xbmc.service" library="service.py" />
<extension point="xbmc.addon.metadata">
<summary lang="ar_SA">إنسخ إحتياطياً قاعده بيانات إكس بى إم سى وملفات اﻹعدادات فى حاله وقوع إنهيار مع إمكانيه اﻹسترجاع</summary>
<summary lang="be_BY">Backup and restore your Kodi database and configuration files in the event of a crash or file corruption.</summary>
@@ -84,16 +85,15 @@
<source>https://github.com/robweber/xbmcbackup</source>
<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>
<screenshot>resources/images/screenshot1.jpg</screenshot>
<screenshot>resources/images/screenshot2.jpg</screenshot>
<screenshot>resources/images/screenshot3.jpg</screenshot>
<screenshot>resources/images/screenshot4.jpg</screenshot>
</assets>
<news>Version 1.6.0
- Backups/Restores now use the concept of a Set to define groups of related files. Restores can restore one set or all sets within a backup archive (no more all or nothing restores)
- Added a new Advanced Editor script for more dynamic included/excluded directories based on a JSON formatted file
- Fixed guisettings restores
- Removed GoogleDrive support - Python 3 compatibility was an issue
</news>
<news>Version 1.6.7
- fixed issue with RunScript not launching Advanced Editor in some cases
- added qr code for Dropbox setup
- fixed error on advanced settings restore
</news>
</extension>
</addon>

View File

@@ -4,6 +4,94 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Version 1.6.7](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.5...robweber:matrix-1.6.7) - 2021-04-16
### Added
- added QRcode when setting up Dropbox, uses pyqrcode
### Fixed
- fixed issue when using ```RunScript()``` within settings to launch Advanced Editor
- error on advanced settings restore prior to reboot
- minor gui dialog fixes
## [Version 1.6.6](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.5...robweber:matrix-1.6.6) - 2021-03-15
### Fixed
- error when typing the remote path, ```listBackups()``` function was not working if final slash not included in typed directory path name.
- added ```force=True``` flag to the ```rmdir()``` function. Fixes issue with directories being removed when not empty
## [Version 1.6.5](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.4...robweber:matrix-1.6.5) - 2021-03-06
### Added
- added Expert setting to change location of zip file temp location as it's being built or extracted
### Changed
- updated ```settings.xml``` to match new [Kodi settings syntax](https://kodi.wiki/view/Add-on_settings_conversion), including visibility levels
### Fixed
- when restoring from a zip file the command to delete the extracted directory was incorrect
- ```Dialog().yesno()``` no longer takes line1 arg, changed to message
## [Version 1.6.4](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.3...robweber:matrix-1.6.4) - 2020-12-23
### Added
- merged duplicate copy code into ```_copyFile``` method
- added method to backup/restore Kodi settings via the GetSettings/SetSettingValue JSON methods in the validation file
- added setting to always restore settings or prompt at the time of backup
### Changed
- updated script.module.future version to current
- swapped xbmc.translatePath for xbmcvfs.translatePath, deprecated
### Fixed
- fixed calls to ```xbmcgui.Dialog().ok()```, method definition changed to only allow one message arg with Kodi 19
- fixed import of dropbox Oauth package in authorizer flow
### Removed
- removed old xml GuiSettings parsing for settings restore
## [Version 1.6.3](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.2...robweber:matrix-1.6.3) - 2020-06-15
### Changed
- fixed validatePath error (issue #166) thanks (thanks @AnonTester)
## [Version 1.6.2](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.1...robweber:matrix-1.6.2) - 2019-04-09
### Changed
- changed PNG screenshots to JPG (per [#165](https://github.com/robweber/xbmcbackup/issues/165))
## [Version 1.6.1](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.0...robweber:matrix-1.6.1) - 2019-12-30
### Added
- added method to get size of a file from the VFS
- added total transfer size information to progress bar with appropriate precision (KB, MB, etc)
- show file size of zip files in the restore selection dialog
- added getSettingInt and getSettingBool to utils.py class
- added verbose logging setting and tied it to logging related to file paths added/written, this will significantly reduce the debug log size (thanks CastagnaIT)
- localize advanced editor strings instead of hard coding English
### Changed
- display every file transfered in progress bar, not just directory
- base progress bar percent on transfer size, not total files
- changed getSettings where needed to getSettingBool and getSettingInt
- use service.py to start scheduler, moving scheduler to resources/lib/scheduler.py Kodi doesn't cache files in the root directory
- fixed issues with rotating backups where trailing slash was missing (thanks @AnonTester)
- read/write files using contextlib
## [Version 1.6.0](https://github.com/robweber/xbmcbackup/compare/krypton-1.5.2...robweber:matrix-1.6.0) - 2019-11-26
### Added
@@ -16,7 +104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- addon.xml updated to use Leia specific syntax and library imports
- removed specific encode() calls per Python2/3 compatibility
- call isdigit() method on the string directly instead of str.isdigit() (results in unicode error)
- added flake8 testing to travis-ci
- added flake8 testing to travis-ci
- updated code to make python3 compatible
- updated code for pep9 styling
- use setArt() to set ListItem icons as the icon= constructor is deprecated
@@ -34,16 +122,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Updated Changelog format to the one suggested by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Added script.module.dropbox import as a dependency for Dropbox filesystem
### Changed
- Fixed issue getting xbmcbackup.val file from non-zipped remote directories. Was being copied as though it was a local file so it was failing.
- Fixed issue getting xbmcbackup.val file from non-zipped remote directories. Was being copied as though it was a local file so it was failing.
- Use linux path separator (/) all the time, Kodi will interpret this correctly on windows. Was causing issues with remote file systems since os.path.sep
- Fixed minor python code style changes based on kodi-addon-checker output
- Fixed minor python code style changes based on kodi-addon-checker output
### Removed
- files releated to dropbox library, using script.module.dropbox import now
- files releated to dropbox library, using script.module.dropbox import now
## Version 1.5.1 - 2019-09-10
@@ -53,7 +141,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## Version 1.5.0 - 2019-08-26
### Added
- Added new Advanced file editor and file selection based on a .json
- Added new Advanced file editor and file selection based on a .json
### Removed
- File backups and restores will not work with old version - breaking change with previous versions PR117
@@ -63,7 +151,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- added file chunk support for Dropbox uploads
- added scheduler delay to assist with time sync (rpi mostly), will delay startup by 2 min
### Changed
- fixed settings duplicate ids, thanks aster-anto
@@ -75,7 +163,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## Version 1.1.1
### Added
- added ability to "catchup" on missed scheduled backup
- added ability to "catchup" on missed scheduled backup
### Changed
- fixed error on authorizers (missing secret/key)
@@ -86,10 +174,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- added tinyurl generation for oauth urls
### Changed
- moved authorize to settings area for cloud storage
## Version 1.0.9
### Changed
@@ -110,7 +198,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- added progress for zip extraction - hopefully helps with extract errors
### Changed
- fix for custom directories not working recursively
@@ -250,7 +338,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## Version 0.3.9
- added "just once" scheduler for one-off type backups
- added "just once" scheduler for one-off type backups
- show notification on scheduler
- update updated language files from Transifex

View File

@@ -1,7 +1,37 @@
import xbmc
import xbmcgui
import xbmcvfs
import resources.lib.utils as utils
from resources.lib.backup import XbmcBackup
from resources.lib.authorizers import DropboxAuthorizer
from resources.lib.advanced_editor import AdvancedBackupEditor
# mode constants
BACKUP = 0
RESTORE = 1
SETTINGS = 2
ADVANCED_EDITOR = 3
LAUNCHER = 4
def authorize_cloud(cloudProvider):
# drobpox
if(cloudProvider == 'dropbox'):
authorizer = DropboxAuthorizer()
if(authorizer.authorize()):
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s' % (utils.getString(30027), utils.getString(30106)))
else:
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s' % (utils.getString(30107), utils.getString(30027)))
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(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt")) # dropbox
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "google_drive.dat")) # google drive
def get_params():
@@ -13,6 +43,7 @@ def get_params():
if(args.startswith('?')):
args = args[1:] # legacy in case of url params
splitString = args.split('=')
utils.log(splitString[1])
param[splitString[0]] = splitString[1]
except:
pass
@@ -24,13 +55,13 @@ def get_params():
mode = -1
params = get_params()
if("mode" in params):
if(params['mode'] == 'backup'):
mode = 0
mode = BACKUP
elif(params['mode'] == 'restore'):
mode = 1
mode = RESTORE
elif(params['mode'] == 'launcher'):
mode = LAUNCHER
# if mode wasn't passed in as arg, get from user
if(mode == -1):
@@ -38,7 +69,7 @@ if(mode == -1):
options = [utils.getString(30016), utils.getString(30017), utils.getString(30099)]
# find out if we're using the advanced editor
if(int(utils.getSetting('backup_selection_type')) == 1):
if(utils.getSettingInt('backup_selection_type') == 1):
options.append(utils.getString(30125))
# figure out if this is a backup or a restore from the user
@@ -49,15 +80,30 @@ if(mode != -1):
# run the profile backup
backup = XbmcBackup()
if(mode == 2):
if(mode == SETTINGS):
# open the settings dialog
utils.openSettings()
elif(mode == 3 and int(utils.getSetting('backup_selection_type')) == 1):
# open the advanced editor
xbmc.executebuiltin('RunScript(special://home/addons/script.xbmcbackup/launcher.py, action=advanced_editor)')
elif(mode == ADVANCED_EDITOR and utils.getSettingInt('backup_selection_type') == 1):
# open the advanced editor but only if in advanced mode
editor = AdvancedBackupEditor()
editor.showMainScreen()
elif(mode == LAUNCHER):
# copied from old launcher.py
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()
elif(backup.remoteConfigured()):
if(mode == backup.Restore):
# if mode was RESTORE
if(mode == RESTORE):
# get list of valid restore points
restorePoints = backup.listBackups()
pointNames = []
@@ -90,6 +136,7 @@ if(mode != -1):
else:
backup.restore()
else:
# mode was BACKUP
backup.backup()
else:
# can't go any further

View File

@@ -1,59 +0,0 @@
# launcher for various helpful functions found in the settings.xml area
import sys
import xbmc
import xbmcgui
import xbmcvfs
import resources.lib.utils as utils
from resources.lib.authorizers import DropboxAuthorizer
from resources.lib.advanced_editor import AdvancedBackupEditor
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))
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('=' in args):
if(args.startswith('?')):
args = args[1:] # legacy in case of url params
splitString = args.split('=')
param[splitString[0]] = splitString[1]
except:
pass
return param
params = get_params()
if(params['action'] == 'authorize_cloud'):
authorize_cloud(params['provider'])
elif(params['action'] == 'remove_auth'):
remove_auth()
elif(params['action'] == 'advanced_editor'):
editor = AdvancedBackupEditor()
editor.showMainScreen()
elif(params['action'] == 'advanced_copy_config'):
editor = AdvancedBackupEditor()
editor.copySimpleConfig()

View File

@@ -1,2 +0,0 @@
*.pyo

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 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.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -173,8 +173,8 @@ msgid "Would you like to continue?"
msgstr "Would you like to continue?"
msgctxt "#30045"
msgid "Error: Remote path doesn't exist"
msgstr "Error: Remote path doesn't exist"
msgid "Error: Remote or zip file path doesn't exist"
msgstr "Error: Remote or zip file path doesn't exist"
msgctxt "#30046"
msgid "Starting"
@@ -213,8 +213,8 @@ msgid "Removing backup"
msgstr "Removing backup"
msgctxt "#30056"
msgid "Go to this URL to authorize"
msgstr "Go to this URL to authorize"
msgid "Scan or click this URL to authorize, click OK AFTER completion"
msgstr "Scan or click this URL to authorize, click OK AFTER completion"
msgctxt "#30057"
msgid "Click OK AFTER completion"
@@ -555,4 +555,72 @@ msgstr ""
msgctxt "#30141"
msgid "This will erase any current Advanced Editor settings"
msgstr ""
msgstr ""
msgctxt "#30142"
msgid "Enable Verbose Logging"
msgstr ""
msgctxt "#30143"
msgid "Exclude a specific folder from this backup set"
msgstr ""
msgctxt "#30144"
msgid "Include a specific folder to this backup set"
msgstr ""
msgctxt "#30145"
msgid "Type"
msgstr ""
msgctxt "#30146"
msgid "Include Sub Folders"
msgstr ""
msgctxt "#30147"
msgid "Toggle Sub Folders"
msgstr ""
msgctxt "#30148"
msgid "Ask before restoring Kodi UI settings"
msgstr ""
msgctxt "#30149"
msgid "Restore Kodi UI Settings"
msgstr ""
msgctxt "#30150"
msgid "Restore saved Kodi system settings from backup?"
msgstr ""
msgctxt "#30151"
msgid "Enable Verbose Logging"
msgstr ""
msgctxt "#30152"
msgid "Set Zip File Location"
msgstr ""
msgctxt "#30153"
msgid "Full path to where the zip file will be staged during backup or restore - must be local to this device"
msgstr ""
msgctxt "#30154"
msgid "Always prompt if Kodi settings should be restored - no by default"
msgstr ""
msgctxt "#30155"
msgid "Adds additional information to the log file"
msgstr ""
msgctxt "#30156"
msgid "Must save key/secret first, then return to settings to authorize"
msgstr ""
msgctxt "#30157"
msgid "Simple uses pre-defined folder locations, use Advanced Editor to define custom paths"
msgstr ""
msgctxt "#30158"
msgid "Run backup on daily, weekly, monthly, or custom schedule"
msgstr ""

View File

@@ -1,5 +1,4 @@
import json
import xbmc
import xbmcgui
import xbmcvfs
import os.path
@@ -7,7 +6,7 @@ from . import utils as utils
class BackupSetManager:
jsonFile = xbmc.translatePath(utils.data_dir() + "custom_paths.json")
jsonFile = xbmcvfs.translatePath(utils.data_dir() + "custom_paths.json")
paths = None
def __init__(self):
@@ -95,7 +94,7 @@ class AdvancedBackupEditor:
if(name is not 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))
enterHome = self.dialog.yesno(utils.getString(30111), message=utils.getString(30112) + " - " + utils.getString(30114) + "\n" + utils.getString(30113) + " - " + utils.getString(30115), nolabel=utils.getString(30112), yeslabel=utils.getString(30113))
rootFolder = 'special://home'
if(enterHome):
@@ -106,7 +105,7 @@ class AdvancedBackupEditor:
rootFolder = rootFolder + '/'
# check that this path even exists
if(not xbmcvfs.exists(xbmc.translatePath(rootFolder))):
if(not xbmcvfs.exists(xbmcvfs.translatePath(rootFolder))):
self.dialog.ok(utils.getString(30117), utils.getString(30118), rootFolder)
return None
else:
@@ -122,13 +121,13 @@ class AdvancedBackupEditor:
rootPath = backupSet['root']
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))]
options = [xbmcgui.ListItem(utils.getString(30120), utils.getString(30143)), xbmcgui.ListItem(utils.getString(30135), utils.getString(30144)), 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))))
options.append(xbmcgui.ListItem(self._cleanPath(rootPath, aDir['path']), "%s: %s" % (utils.getString(30145), 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']))))
options.append(xbmcgui.ListItem(self._cleanPath(rootPath, aDir['path']), "%s: %s | %s: %s" % (utils.getString(30145), utils.getString(30134), utils.getString(30146), str(aDir['recurse']))))
optionSelected = self.dialog.select(utils.getString(30122) + ' ' + name, options, useDetails=True)
@@ -157,12 +156,12 @@ class AdvancedBackupEditor:
cOptions = ['Delete']
if(backupSet['dirs'][optionSelected - 3]['type'] == 'include'):
cOptions.append('Toggle Sub Folders')
cOptions.append(utils.getString(30147))
contextOption = self.dialog.contextmenu(cOptions)
if(contextOption == 0):
if(self.dialog.yesno(heading=utils.getString(30123), line1=utils.getString(30128))):
if(self.dialog.yesno(heading=utils.getString(30123), message=utils.getString(30128))):
# remove folder
del backupSet['dirs'][optionSelected - 3]
elif(contextOption == 1 and backupSet['dirs'][optionSelected - 3]['type'] == 'include'):
@@ -218,7 +217,7 @@ class AdvancedBackupEditor:
customPaths.updateSet(aSet['name'], updatedSet)
elif(menuOption == 1):
if(self.dialog.yesno(heading=utils.getString(30127), line1=utils.getString(30128))):
if(self.dialog.yesno(heading=utils.getString(30127), message=utils.getString(30128))):
# delete this path - subtract one because of "add" item
customPaths.deleteSet(exitCondition - 1)
@@ -227,7 +226,7 @@ class AdvancedBackupEditor:
shouldContinue = self.dialog.yesno(utils.getString(30139), utils.getString(30140), utils.getString(30141))
if(shouldContinue):
source = xbmc.translatePath(os.path.join(utils.addon_dir(), 'resources', 'data', 'default_files.json'))
dest = xbmc.translatePath(os.path.join(utils.data_dir(), 'custom_paths.json'))
source = xbmcvfs.translatePath(os.path.join(utils.addon_dir(), 'resources', 'data', 'default_files.json'))
dest = xbmcvfs.translatePath(os.path.join(utils.data_dir(), 'custom_paths.json'))
xbmcvfs.copy(source, dest)

View File

@@ -1,16 +1,41 @@
import xbmc
import xbmcgui
import xbmcvfs
import pyqrcode
import resources.lib.tinyurl as tinyurl
import resources.lib.utils as utils
# don't die on import error yet, these might not even get used
try:
from dropbox import dropbox
from dropbox import oauth
except ImportError:
pass
class QRCode(xbmcgui.WindowXMLDialog):
def __init__(self, *args, **kwargs):
self.image = kwargs["image"]
self.text = kwargs["text"]
self.url = kwargs['url']
def onInit(self):
self.imagecontrol = 501
self.textbox1 = 502
self.textbox2 = 504
self.okbutton = 503
self.showdialog()
def showdialog(self):
self.getControl(self.imagecontrol).setImage(self.image)
self.getControl(self.textbox1).setText(self.text)
self.getControl(self.textbox2).setText(self.url)
self.setFocus(self.getControl(self.okbutton))
def onClick(self, controlId):
if (controlId == self.okbutton):
self.close()
class DropboxAuthorizer:
APP_KEY = ""
APP_SECRET = ""
@@ -24,7 +49,7 @@ class DropboxAuthorizer:
if(self.APP_KEY == '' and self.APP_SECRET == ''):
# we can't go any farther, need these for sure
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30027) + ' ' + utils.getString(30058), utils.getString(30059))
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s\n%s' % (utils.getString(30027), utils.getString(30058), utils.getString(30059)))
result = False
@@ -46,13 +71,26 @@ class DropboxAuthorizer:
self._deleteToken()
# copied flow from http://dropbox-sdk-python.readthedocs.io/en/latest/moduledoc.html#dropbox.oauth.DropboxOAuth2FlowNoRedirect
flow = dropbox.oauth.DropboxOAuth2FlowNoRedirect(self.APP_KEY, self.APP_SECRET)
flow = oauth.DropboxOAuth2FlowNoRedirect(self.APP_KEY, self.APP_SECRET)
url = flow.start()
# print url in log
utils.log("Authorize URL: " + url)
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30056), utils.getString(30057), tinyurl.shorten(url))
# create a QR Code
shortUrl = str(tinyurl.shorten(url), 'utf-8')
imageFile = xbmcvfs.translatePath(utils.data_dir() + '/qrcode.png')
qrIMG = pyqrcode.create(shortUrl)
qrIMG.png(imageFile, scale=10)
# show the dialog prompt to authorize
qr = QRCode("script-backup-qrcode.xml", utils.addon_dir(), "default", image=imageFile, text=utils.getString(30056), url=shortUrl)
qr.doModal()
# cleanup
del qr
xbmcvfs.delete(imageFile)
# get the auth code
code = xbmcgui.Dialog().input(utils.getString(30027) + ' ' + utils.getString(30103))
@@ -89,14 +127,14 @@ class DropboxAuthorizer:
def _setToken(self, token):
# write the token files
token_file = open(xbmc.translatePath(utils.data_dir() + "tokens.txt"), 'w')
token_file = open(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"), 'w')
token_file.write(token)
token_file.close()
def _getToken(self):
# get token, if it exists
if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "tokens.txt"))):
token_file = open(xbmc.translatePath(utils.data_dir() + "tokens.txt"))
if(xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))):
token_file = open(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))
token = token_file.read()
token_file.close()
@@ -105,5 +143,5 @@ class DropboxAuthorizer:
return ""
def _deleteToken(self):
if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "tokens.txt"))):
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt"))
if(xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))):
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))

View File

@@ -4,6 +4,7 @@ import json
import xbmc
import xbmcgui
import xbmcvfs
import os.path
from . import utils as utils
from datetime import datetime
from . vfs import XBMCFileSystem, DropboxFileSystem, ZipFileSystem
@@ -26,6 +27,8 @@ class XbmcBackup:
Backup = 0
Restore = 1
ZIP_TEMP_PATH = None
# list of dirs for the "simple" file selection
simple_directory_list = ['addons', 'addon_data', 'database', 'game_saves', 'playlists', 'profiles', 'thumbnails', 'config']
@@ -39,34 +42,34 @@ class XbmcBackup:
# for the progress bar
progressBar = None
filesLeft = 0
filesTotal = 1
transferSize = 0
transferLeft = 0
restore_point = None
skip_advanced = False # if we should check for the existance of advancedsettings in the restore
def __init__(self):
self.xbmc_vfs = XBMCFileSystem(xbmc.translatePath('special://home'))
self.xbmc_vfs = XBMCFileSystem(xbmcvfs.translatePath('special://home'))
self.ZIP_TEMP_PATH = xbmcvfs.translatePath(utils.getSetting('zip_temp_path'))
self.configureRemote()
utils.log(utils.getString(30046))
def configureRemote(self):
if(utils.getSetting('remote_selection') == '1'):
self.remote_base_path = utils.getSetting('remote_path_2')
self.remote_vfs = XBMCFileSystem(utils.getSetting('remote_path_2'))
utils.setSetting("remote_path", "")
elif(utils.getSetting('remote_selection') == '0'):
self.remote_base_path = utils.getSetting('remote_path')
self.remote_vfs = XBMCFileSystem(utils.getSetting("remote_path"))
elif(utils.getSetting('remote_selection') == '2'):
self.remote_base_path = "/"
self.remote_vfs = DropboxFileSystem("/")
self.remote_base_path = self.remote_vfs.root_path
def remoteConfigured(self):
result = True
if(self.remote_base_path == ""):
if(self.remote_base_path == "" or not xbmcvfs.exists(self.ZIP_TEMP_PATH)):
result = False
return result
@@ -92,8 +95,8 @@ class XbmcBackup:
if(file_ext == 'zip' and len(folderName) == 12 and folderName.isdigit()):
# format the name according to regional settings
folderName = self._dateFormat(folderName)
# format the name according to regional settings and display the file size
folderName = "%s - %s" % (self._dateFormat(folderName), utils.diskString(self.remote_vfs.fileSize(self.remote_base_path + aFile)))
result.append((aFile, folderName))
@@ -124,14 +127,14 @@ class XbmcBackup:
utils.log('File Selection Type: ' + str(utils.getSetting('backup_selection_type')))
allFiles = []
if(int(utils.getSetting('backup_selection_type')) == 0):
if(utils.getSettingInt('backup_selection_type') == 0):
# read in a list of the directories to backup
selectedDirs = self._readBackupConfig(utils.addon_dir() + "/resources/data/default_files.json")
# simple mode - get file listings for all enabled directories
for aDir in self.simple_directory_list:
# if this dir enabled
if(utils.getSetting('backup_' + aDir) == 'true'):
if(utils.getSettingBool('backup_' + aDir)):
# get a file listing and append it to the allfiles array
allFiles.append(self._addBackupDir(aDir, selectedDirs[aDir]['root'], selectedDirs[aDir]['dirs']))
else:
@@ -154,7 +157,7 @@ class XbmcBackup:
if(not writeCheck):
# we may not be able to write to this destination for some reason
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30089), utils.getString(30090), utils.getString(30044), autoclose=25000)
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30089), "%s\n%s" % (utils.getString(30090), utils.getString(30044)), autoclose=25000)
if(not shouldContinue):
return
@@ -162,9 +165,9 @@ class XbmcBackup:
orig_base_path = self.remote_vfs.root_path
# backup all the files
self.filesLeft = self.filesTotal
self.transferLeft = self.transferSize
for fileGroup in allFiles:
self.xbmc_vfs.set_root(xbmc.translatePath(fileGroup['source']))
self.xbmc_vfs.set_root(xbmcvfs.translatePath(fileGroup['source']))
self.remote_vfs.set_root(fileGroup['dest'] + fileGroup['name'])
filesCopied = self._copyFiles(fileGroup['files'], self.xbmc_vfs, self.remote_vfs)
@@ -176,28 +179,30 @@ class XbmcBackup:
self.xbmc_vfs.set_root("special://home/")
self.remote_vfs.set_root(orig_base_path)
if(utils.getSetting("compress_backups") == 'true'):
if(utils.getSettingBool("compress_backups")):
fileManager = FileManager(self.xbmc_vfs)
# send the zip file to the real remote vfs
zip_name = self.remote_vfs.root_path[:-1] + ".zip"
zip_name = os.path.join(self.ZIP_TEMP_PATH, self.remote_vfs.root_path[:-1] + ".zip")
self.remote_vfs.cleanup()
self.xbmc_vfs.rename(xbmc.translatePath("special://temp/xbmc_backup_temp.zip"), xbmc.translatePath("special://temp/" + zip_name))
fileManager.addFile(xbmc.translatePath("special://temp/" + zip_name))
# set root to data dir home
self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/"))
self.xbmc_vfs.rename(os.path.join(self.ZIP_TEMP_PATH, "xbmc_backup_temp.zip"), zip_name)
fileManager.addFile(zip_name)
# set root to data dir home and reset remote
self.xbmc_vfs.set_root(self.ZIP_TEMP_PATH)
self.remote_vfs = self.saved_remote_vfs
self.progressBar.updateProgress(98, utils.getString(30088))
# update the amount to transfer
self.transferSize = fileManager.fileSize()
self.transferLeft = self.transferSize
fileCopied = self._copyFiles(fileManager.getFiles(), self.xbmc_vfs, self.remote_vfs)
if(not fileCopied):
# zip archive copy filed, inform the user
shouldContinue = xbmcgui.Dialog().ok(utils.getString(30089), utils.getString(30090), utils.getString(30091))
shouldContinue = xbmcgui.Dialog().ok(utils.getString(30089), '%s\n%s' % (utils.getString(30090), utils.getString(30091)))
# delete the temp zip file
self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + zip_name))
self.xbmc_vfs.rmfile(zip_name)
# remove old backups
self._rotateBackups()
@@ -217,25 +222,28 @@ class XbmcBackup:
utils.log("copying zip file: " + self.restore_point)
# set root to data dir home
self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/"))
if(not self.xbmc_vfs.exists(xbmc.translatePath("special://temp/" + self.restore_point))):
self.xbmc_vfs.set_root(self.ZIP_TEMP_PATH)
restore_path = os.path.join(self.ZIP_TEMP_PATH, self.restore_point)
if(not self.xbmc_vfs.exists(restore_path)):
# copy just this file from the remote vfs
self.transferSize = self.remote_vfs.fileSize(self.remote_base_path + self.restore_point)
zipFile = []
zipFile.append(self.remote_base_path + self.restore_point)
zipFile.append({'file': self.remote_base_path + self.restore_point, 'size': self.transferSize})
# set transfer size
self.transferLeft = self.transferSize
self._copyFiles(zipFile, self.remote_vfs, self.xbmc_vfs)
else:
utils.log("zip file exists already")
# extract the zip file
zip_vfs = ZipFileSystem(xbmc.translatePath("special://temp/" + self.restore_point), 'r')
zip_vfs = ZipFileSystem(restore_path, 'r')
extractor = ZipExtractor()
if(not extractor.extract(zip_vfs, xbmc.translatePath("special://temp/"), self.progressBar)):
if(not extractor.extract(zip_vfs, self.ZIP_TEMP_PATH, self.progressBar)):
# we had a problem extracting the archive, delete everything
zip_vfs.cleanup()
self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + self.restore_point))
self.xbmc_vfs.rmfile(restore_path)
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30101))
return
@@ -244,12 +252,12 @@ class XbmcBackup:
self.progressBar.updateProgress(0, utils.getString(30049) + "......")
# set the new remote vfs and fix xbmc path
self.remote_vfs = XBMCFileSystem(xbmc.translatePath("special://temp/" + self.restore_point.split(".")[0] + "/"))
self.xbmc_vfs.set_root(xbmc.translatePath("special://home/"))
self.remote_vfs = XBMCFileSystem(os.path.join(self.ZIP_TEMP_PATH, self.restore_point.split(".")[0]))
self.xbmc_vfs.set_root(xbmcvfs.translatePath("special://home/"))
# for restores remote path must exist
if(not self.remote_vfs.exists(self.remote_vfs.root_path)):
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30045), self.remote_vfs.root_path)
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s' % (utils.getString(30045), self.remote_vfs.root_path))
return
valFile = self._checkValidationFile(self.remote_vfs.root_path)
@@ -264,10 +272,12 @@ class XbmcBackup:
# check for the existance of an advancedsettings file
if(self.remote_vfs.exists(self.remote_vfs.root_path + "config/advancedsettings.xml") and not self.skip_advanced):
# let the user know there is an advanced settings file present
restartXbmc = xbmcgui.Dialog().yesno(utils.getString(30038), utils.getString(30039), utils.getString(30040), utils.getString(30041))
restartXbmc = xbmcgui.Dialog().yesno(utils.getString(30038), "%s\n%s\n%s" % (utils.getString(30039), utils.getString(30040), utils.getString(30041)))
if(restartXbmc):
# add only this file to the file list
self.transferSize = 1
self.transferLeft = 1
fileManager.addFile(self.remote_vfs.root_path + "config/advancedsettings.xml")
self._copyFiles(fileManager.getFiles(), self.remote_vfs, self.xbmc_vfs)
@@ -278,6 +288,12 @@ class XbmcBackup:
xbmcgui.Dialog().ok(utils.getString(30077), utils.getString(30078))
return
# check if settings should be restored from this backup
restoreSettings = not utils.getSettingBool('always_prompt_restore_settings')
if(not restoreSettings and 'system_settings' in valFile):
# prompt the user to restore settings yes/no
restoreSettings = xbmcgui.Dialog().yesno(utils.getString(30149), utils.getString(30150))
# use a multiselect dialog to select sets to restore
restoreSets = [n['name'] for n in valFile['directories']]
@@ -288,39 +304,45 @@ class XbmcBackup:
selectedSets = [restoreSets.index(n) for n in selectedSets if n in restoreSets] # if set name not found just skip it
if(selectedSets is not None):
# go through each of the directories in the backup and write them to the correct location
for index in selectedSets:
# add this directory
aDir = valFile['directories'][index]
self.xbmc_vfs.set_root(xbmc.translatePath(aDir['path']))
self.xbmc_vfs.set_root(xbmcvfs.translatePath(aDir['path']))
if(self.remote_vfs.exists(self.remote_vfs.root_path + aDir['name'] + '/')):
# walk the directory
fileManager.walkTree(self.remote_vfs.root_path + aDir['name'] + '/')
self.filesTotal = self.filesTotal + fileManager.size()
self.transferSize = self.transferSize + fileManager.fileSize()
allFiles.append({"source": self.remote_vfs.root_path + aDir['name'], "dest": self.xbmc_vfs.root_path, "files": fileManager.getFiles()})
else:
utils.log("error path not found: " + self.remote_vfs.root_path + aDir['name'])
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30045), self.remote_vfs.root_path + aDir['name'])
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s' % (utils.getString(30045), self.remote_vfs.root_path + aDir['name']))
# restore all the files
self.filesLeft = self.filesTotal
self.transferLeft = self.transferSize
for fileGroup in allFiles:
self.remote_vfs.set_root(fileGroup['source'])
self.xbmc_vfs.set_root(fileGroup['dest'])
self._copyFiles(fileGroup['files'], self.remote_vfs, self.xbmc_vfs)
# update the Kodi settings - if we can
if('system_settings' in valFile and restoreSettings):
self.progressBar.updateProgress(98, "Restoring Kodi settings")
gui_settings = GuiSettingsManager()
gui_settings.restore(valFile['system_settings'])
self.progressBar.updateProgress(99, "Clean up operations .....")
if(self.restore_point.split('.')[-1] == 'zip'):
# delete the zip file and the extracted directory
self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + self.restore_point))
self.xbmc_vfs.rmdir(self.remote_vfs.root_path)
# update the guisettings information (or what we can from it)
gui_settings = GuiSettingsManager()
gui_settings.run()
self.xbmc_vfs.rmfile(os.path.join(self.ZIP_TEMP_PATH, self.restore_point))
xbmc.sleep(1000)
self.xbmc_vfs.rmdir(self.remote_vfs.clean_path(os.path.join(self.ZIP_TEMP_PATH, self.restore_point.split(".")[0])))
xbmc.sleep(1000)
# call update addons to refresh everything
xbmc.executebuiltin('UpdateLocalAddons')
@@ -333,17 +355,18 @@ class XbmcBackup:
# append backup folder name
progressBarTitle = utils.getString(30010) + " - "
if(mode == self.Backup and self.remote_vfs.root_path != ''):
if(utils.getSetting("compress_backups") == 'true'):
if(utils.getSettingBool("compress_backups")):
# delete old temp file
if(self.xbmc_vfs.exists(xbmc.translatePath('special://temp/xbmc_backup_temp.zip'))):
if(not self.xbmc_vfs.rmfile(xbmc.translatePath('special://temp/xbmc_backup_temp.zip'))):
zip_path = os.path.join(self.ZIP_TEMP_PATH, 'xbmc_backup_temp.zip')
if(self.xbmc_vfs.exists(zip_path)):
if(not self.xbmc_vfs.rmfile(zip_path)):
# we had some kind of error deleting the old file
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30096), utils.getString(30097))
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s' % (utils.getString(30096), utils.getString(30097)))
return False
# save the remote file system and use the zip vfs
self.saved_remote_vfs = self.remote_vfs
self.remote_vfs = ZipFileSystem(xbmc.translatePath("special://temp/xbmc_backup_temp.zip"), "w")
self.remote_vfs = ZipFileSystem(zip_path, "w")
self.remote_vfs.set_root(self.remote_vfs.root_path + time.strftime("%Y%m%d%H%M") + "/")
progressBarTitle = progressBarTitle + utils.getString(30023) + ": " + utils.getString(30016)
@@ -358,6 +381,7 @@ class XbmcBackup:
utils.log(utils.getString(30047) + ": " + self.xbmc_vfs.root_path)
utils.log(utils.getString(30048) + ": " + self.remote_vfs.root_path)
utils.log(utils.getString(30152) + ": " + utils.getSetting('zip_temp_path'))
# setup the progress bar
self.progressBar = BackupProgressBar(progressOverride)
@@ -379,7 +403,7 @@ class XbmcBackup:
result = True
utils.log("Source: " + source.root_path)
utils.log("Desintation: " + dest.root_path)
utils.log("Destination: " + dest.root_path)
# make sure the dest folder exists - can cause write errors if the full path doesn't exist
if(not dest.exists(dest.root_path)):
@@ -387,21 +411,18 @@ class XbmcBackup:
for aFile in fileList:
if(not self.progressBar.checkCancel()):
utils.log('Writing file: ' + aFile, xbmc.LOGDEBUG)
if(aFile.startswith("-")):
self._updateProgress(aFile[len(source.root_path) + 1:])
dest.mkdir(dest.root_path + aFile[len(source.root_path) + 1:])
else:
self._updateProgress()
if(utils.getSettingBool('verbose_logging')):
utils.log('Writing file: ' + aFile['file'])
wroteFile = True
destFile = dest.root_path + aFile[len(source.root_path):]
if(isinstance(source, DropboxFileSystem)):
# if copying from cloud storage we need the file handle, use get_file
wroteFile = source.get_file(aFile, destFile)
else:
# copy using normal method
wroteFile = dest.put(aFile, destFile)
if(aFile['file'].startswith("-")):
self._updateProgress('%s remaining, writing %s' % (utils.diskString(self.transferLeft), os.path.basename(aFile['file'][len(source.root_path):]) + "/"))
dest.mkdir(dest.root_path + aFile['file'][len(source.root_path) + 1:])
else:
self._updateProgress('%s remaining, writing %s' % (utils.diskString(self.transferLeft), os.path.basename(aFile['file'][len(source.root_path):])))
self.transferLeft = self.transferLeft - aFile['size']
# copy the file
wroteFile = self._copyFile(source, dest, aFile['file'], dest.root_path + aFile['file'][len(source.root_path):])
# if result is still true but this file failed
if(not wroteFile and result):
@@ -409,18 +430,31 @@ class XbmcBackup:
return result
def _copyFile(self, source, dest, sourceFile, destFile):
result = True
if(isinstance(source, DropboxFileSystem)):
# if copying from cloud storage we need the file handle, use get_file
result = source.get_file(sourceFile, destFile)
else:
# copy using normal method
result = dest.put(sourceFile, destFile)
return result
def _addBackupDir(self, folder_name, root_path, dirList):
utils.log('Backup set: ' + folder_name)
fileManager = FileManager(self.xbmc_vfs)
self.xbmc_vfs.set_root(xbmc.translatePath(root_path))
self.xbmc_vfs.set_root(xbmcvfs.translatePath(root_path))
for aDir in dirList:
fileManager.addDir(aDir)
# walk all the root trees
fileManager.walk()
# update total files
self.filesTotal = self.filesTotal + fileManager.size()
# update total size
self.transferSize = self.transferSize + fileManager.fileSize()
return {"name": folder_name, "source": root_path, "dest": self.remote_vfs.root_path, "files": fileManager.getFiles()}
@@ -434,11 +468,10 @@ class XbmcBackup:
return result
def _updateProgress(self, message=None):
self.filesLeft = self.filesLeft - 1
self.progressBar.updateProgress(int((float(self.filesTotal - self.filesLeft) / float(self.filesTotal)) * 100), message)
self.progressBar.updateProgress(int((float(self.transferSize - self.transferLeft) / float(self.transferSize)) * 100), message)
def _rotateBackups(self):
total_backups = int(utils.getSetting('backup_rotation'))
total_backups = utils.getSettingInt('backup_rotation')
if(total_backups > 0):
# get a list of valid backup folders
@@ -447,7 +480,6 @@ class XbmcBackup:
if(len(dirs) > total_backups):
# remove backups to equal total wanted
remove_num = 0
self.filesTotal = self.filesTotal + remove_num + 1
# update the progress bar if it is available
while(remove_num < (len(dirs) - total_backups) and not self.progressBar.checkCancel()):
@@ -456,37 +488,42 @@ class XbmcBackup:
if(dirs[remove_num][0].split('.')[-1] == 'zip'):
# this is a file, remove it that way
self.remote_vfs.rmfile(self.remote_base_path + dirs[remove_num][0])
self.remote_vfs.rmfile(self.remote_vfs.clean_path(self.remote_base_path) + dirs[remove_num][0])
else:
self.remote_vfs.rmdir(self.remote_base_path + dirs[remove_num][0] + "/")
self.remote_vfs.rmdir(self.remote_vfs.clean_path(self.remote_base_path) + dirs[remove_num][0] + "/")
remove_num = remove_num + 1
def _createValidationFile(self, dirList):
valInfo = {"name": "XBMC Backup Validation File", "xbmc_version": xbmc.getInfoLabel('System.BuildVersion'), "type": 0}
valInfo = {"name": "XBMC Backup Validation File", "xbmc_version": xbmc.getInfoLabel('System.BuildVersion'), "type": 0, "system_settings": []}
valDirs = []
# save list of file sets
for aDir in dirList:
valDirs.append({"name": aDir['name'], "path": aDir['source']})
valInfo['directories'] = valDirs
vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"), 'w')
# dump all current Kodi settings
gui_settings = GuiSettingsManager()
valInfo['system_settings'] = gui_settings.backup()
vFile = xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup.val"), 'w')
vFile.write(json.dumps(valInfo))
vFile.write("")
vFile.close()
success = self.remote_vfs.put(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"), self.remote_vfs.root_path + "xbmcbackup.val")
success = self._copyFile(self.xbmc_vfs, self.remote_vfs, xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup.val"), self.remote_vfs.root_path + "xbmcbackup.val")
# remove the validation file
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"))
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup.val"))
if(success):
# android requires a .nomedia file to not index the directory as media
if(not xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + ".nomedia"))):
nmFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + ".nomedia"), 'w')
if(not xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + ".nomedia"))):
nmFile = xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + ".nomedia"), 'w')
nmFile.close()
success = self.remote_vfs.put(xbmc.translatePath(utils.data_dir() + ".nomedia"), self.remote_vfs.root_path + ".nomedia")
success = self._copyFile(self.xbmc_vfs, self.remote_vfs, xbmcvfs.translatePath(utils.data_dir() + ".nomedia"), self.remote_vfs.root_path + ".nomedia")
return success
@@ -494,23 +531,19 @@ class XbmcBackup:
result = None
# copy the file and open it
if(isinstance(self.remote_vfs, DropboxFileSystem)):
self.remote_vfs.get_file(path + "xbmcbackup.val", xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
else:
self.xbmc_vfs.put(path + "xbmcbackup.val", xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
self._copyFile(self.remote_vfs, self.xbmc_vfs, path + "xbmcbackup.val", xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"), 'r')
jsonString = vFile.read()
vFile.close()
with xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"), 'r') as vFile:
jsonString = vFile.read()
# delete after checking
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
try:
result = json.loads(jsonString)
if(xbmc.getInfoLabel('System.BuildVersion') != result['xbmc_version']):
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30085), utils.getString(30086), utils.getString(30044))
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30085), "%s\n%s" % (utils.getString(30086), utils.getString(30044)))
if(not shouldContinue):
result = None
@@ -522,11 +555,11 @@ class XbmcBackup:
return result
def _createResumeBackupFile(self):
with xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"), 'w') as f:
with xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "resume.txt"), 'w') as f:
f.write(self.restore_point)
def _readBackupConfig(self, aFile):
with xbmcvfs.File(xbmc.translatePath(aFile), 'r') as f:
with xbmcvfs.File(xbmcvfs.translatePath(aFile), 'r') as f:
jsonString = f.read()
return json.loads(jsonString)
@@ -536,6 +569,7 @@ class FileManager:
exclude_dir = []
root_dirs = []
pathSep = '/'
totalSize = 0
def __init__(self, vfs):
self.vfs = vfs
@@ -546,11 +580,13 @@ class FileManager:
def walk(self):
for aDir in self.root_dirs:
self.addFile('-' + xbmc.translatePath(aDir['path']))
self.walkTree(xbmc.translatePath(aDir['path']), aDir['recurse'])
self.addFile('-' + xbmcvfs.translatePath(aDir['path']))
self.walkTree(xbmcvfs.translatePath(aDir['path']), aDir['recurse'])
def walkTree(self, directory, recurse=True):
utils.log('walking ' + directory + ', recurse: ' + str(recurse))
if(utils.getSettingBool('verbose_logging')):
utils.log('walking ' + directory + ', recurse: ' + str(recurse))
if(directory[-1:] == '/' or directory[-1:] == '\\'):
directory = directory[:-1]
@@ -560,7 +596,7 @@ class FileManager:
if(recurse):
# create all the subdirs first
for aDir in dirs:
dirPath = xbmc.validatePath(xbmc.translatePath(directory + self.pathSep + aDir))
dirPath = xbmcvfs.validatePath(xbmcvfs.translatePath(directory + self.pathSep + aDir))
file_ext = aDir.split('.')[-1]
# check if directory is excluded
@@ -580,22 +616,27 @@ class FileManager:
# copy all the files
for aFile in files:
filePath = xbmc.translatePath(directory + self.pathSep + aFile)
filePath = xbmcvfs.translatePath(directory + self.pathSep + aFile)
self.addFile(filePath)
def addDir(self, dirMeta):
if(dirMeta['type'] == 'include'):
self.root_dirs.append({'path': dirMeta['path'], 'recurse': dirMeta['recurse']})
else:
self.excludeFile(xbmc.translatePath(dirMeta['path']))
self.excludeFile(xbmcvfs.translatePath(dirMeta['path']))
def addFile(self, filename):
# write the full remote path name of this file
utils.log("Add File: " + filename)
self.fileArray.append(filename)
if(utils.getSettingBool('verbose_logging')):
utils.log("Add File: " + filename)
# get the file size
fSize = self.vfs.fileSize(filename)
self.totalSize = self.totalSize + fSize
self.fileArray.append({'file': filename, 'size': fSize})
def excludeFile(self, filename):
# remove trailing slash
if(filename[-1] == '/' or filename[-1] == '\\'):
filename = filename[:-1]
@@ -609,8 +650,12 @@ class FileManager:
self.fileArray = []
self.root_dirs = []
self.exclude_dir = []
self.totalSize = 0
return result
def size(self):
def totalFiles(self):
return len(self.fileArray)
def fileSize(self):
return self.totalSize

View File

@@ -1,72 +1,47 @@
import json
import xbmc
import xbmcvfs
from . import utils as utils
from xml.dom import minidom
from xml.parsers.expat import ExpatError
class GuiSettingsManager:
doc = None
filename = 'kodi_settings.json'
systemSettings = None
def __init__(self):
# first make a copy of the file
xbmcvfs.copy(xbmc.translatePath('special://home/userdata/guisettings.xml'), xbmc.translatePath("special://home/userdata/guisettings.xml.restored"))
# get all of the current Kodi settings
json_response = json.loads(xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.GetSettings","params":{"level":"expert"}}'))
# read in the copy
self._readFile(xbmc.translatePath('special://home/userdata/guisettings.xml.restored'))
self.systemSettings = json_response['result']['settings']
def run(self):
# get a list of all the settings we can manipulate via json
json_response = json.loads(xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.GetSettings","params":{"level":"advanced"}}'))
def backup(self):
utils.log('Backing up Kodi settings')
settings = json_response['result']['settings']
currentSettings = {}
# return all current settings
return self.systemSettings
for aSetting in settings:
if('value' in aSetting):
currentSettings[aSetting['id']] = aSetting['value']
def restore(self, restoreSettings):
utils.log('Restoring Kodi settings')
# parse the existing xml file and get all the settings we need to restore
restoreSettings = self.__parseNodes(self.doc.getElementsByTagName('setting'))
updateJson = {"jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": {"setting": "", "value": ""}}
# get a list where the restore setting value != the current value
updateSettings = {k: v for k, v in list(restoreSettings.items()) if (k in currentSettings and currentSettings[k] != v)}
# create a setting=value dict of the current settings
settingsDict = {}
for aSetting in self.systemSettings:
# ignore action types, no value
if(aSetting['type'] != 'action'):
settingsDict[aSetting['id']] = aSetting['value']
# go through all the found settings and update them
jsonObj = {"jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": {"setting": "", "value": ""}}
for anId, aValue in list(updateSettings.items()):
utils.log("updating: " + anId + ", value: " + str(aValue))
restoreCount = 0
for aSetting in restoreSettings:
# only update a setting if its different than the current (action types have no value)
if(aSetting['type'] != 'action' and settingsDict[aSetting['id']] != aSetting['value']):
if(utils.getSettingBool('verbose_logging')):
utils.log('%s different than current: %s' % (aSetting['id'], str(aSetting['value'])))
jsonObj['params']['setting'] = anId
jsonObj['params']['value'] = aValue
updateJson['params']['setting'] = aSetting['id']
updateJson['params']['value'] = aSetting['value']
xbmc.executeJSONRPC(json.dumps(jsonObj))
xbmc.executeJSONRPC(json.dumps(updateJson))
restoreCount = restoreCount + 1
def __parseNodes(self, nodeList):
result = {}
for node in nodeList:
nodeValue = ''
if(node.firstChild is not None):
nodeValue = node.firstChild.nodeValue
# check for numbers and booleans
if(nodeValue.isdigit()):
nodeValue = int(nodeValue)
elif(nodeValue == 'true'):
nodeValue = True
elif(nodeValue == 'false'):
nodeValue = False
result[node.getAttribute('id')] = nodeValue
return result
def _readFile(self, fileLoc):
if(xbmcvfs.exists(fileLoc)):
try:
self.doc = minidom.parse(fileLoc)
except ExpatError:
utils.log("Can't read " + fileLoc)
utils.log('Update %d settings' % restoreCount)

View File

@@ -15,9 +15,9 @@ class BackupProgressBar:
self.override = progressOverride
# check if we should use the progress bar
if(int(utils.getSetting('progress_mode')) != 2):
if(utils.getSettingInt('progress_mode') != 2):
# check if background or normal
if(int(utils.getSetting('progress_mode')) == 0 and not self.override):
if(utils.getSettingInt('progress_mode') == 0 and not self.override):
self.mode = self.DIALOG
self.progressBar = xbmcgui.DialogProgress()
else:

View File

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

View File

@@ -1,6 +1,7 @@
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
__addon_id__ = 'script.xbmcbackup'
__Addon = xbmcaddon.Addon(__addon_id__)
@@ -23,13 +24,21 @@ def log(message, loglevel=xbmc.LOGDEBUG):
def showNotification(message):
xbmcgui.Dialog().notification(getString(30010), message, time=4000, icon=xbmc.translatePath(__Addon.getAddonInfo('path') + "/resources/images/icon.png"))
xbmcgui.Dialog().notification(getString(30010), message, time=4000, icon=xbmcvfs.translatePath(__Addon.getAddonInfo('path') + "/resources/images/icon.png"))
def getSetting(name):
return __Addon.getSetting(name)
def getSettingBool(name):
return bool(__Addon.getSettingBool(name))
def getSettingInt(name):
return __Addon.getSettingInt(name)
def setSetting(name, value):
__Addon.setSetting(name, value)
@@ -45,3 +54,16 @@ def getRegionalTimestamp(date_time, dateformat=['dateshort']):
result = result + ("%s " % date_time.strftime(xbmc.getRegion(aFormat)))
return result.strip()
def diskString(fSize):
# convert a size in kilobytes to the best possible match and return as a string
fSize = float(fSize)
i = 0
sizeNames = ['KB', 'MB', 'GB', 'TB']
while(fSize > 1024):
fSize = fSize / 1024
i = i + 1
return "%0.2f%s" % (fSize, sizeNames[i])

View File

@@ -2,7 +2,6 @@ from __future__ import unicode_literals
import zipfile
import os.path
import sys
import xbmc
import xbmcvfs
import xbmcgui
from dropbox import dropbox
@@ -17,16 +16,19 @@ class Vfs:
def __init__(self, rootString):
self.set_root(rootString)
def set_root(self, rootString):
old_root = self.root_path
self.root_path = rootString
def clean_path(self, path):
# fix slashes
self.root_path = self.root_path.replace("\\", "/")
path = path.replace("\\", "/")
# check if trailing slash is included
if(self.root_path[-1:] != "/"):
self.root_path = self.root_path + "/"
if(path[-1:] != '/'):
path = path + '/'
return path
def set_root(self, rootString):
old_root = self.root_path
self.root_path = self.clean_path(rootString)
# return the old root
return old_root
@@ -55,6 +57,9 @@ class Vfs:
def cleanup(self):
return True
def fileSize(self, filename):
return 0 # result should be in KB
class XBMCFileSystem(Vfs):
@@ -62,13 +67,13 @@ class XBMCFileSystem(Vfs):
return xbmcvfs.listdir(directory)
def mkdir(self, directory):
return xbmcvfs.mkdir(xbmc.translatePath(directory))
return xbmcvfs.mkdir(xbmcvfs.translatePath(directory))
def put(self, source, dest):
return xbmcvfs.copy(xbmc.translatePath(source), xbmc.translatePath(dest))
return xbmcvfs.copy(xbmcvfs.translatePath(source), xbmcvfs.translatePath(dest))
def rmdir(self, directory):
return xbmcvfs.rmdir(directory, True)
return xbmcvfs.rmdir(directory, force=True) # use force=True to make sure it works recursively
def rmfile(self, aFile):
return xbmcvfs.delete(aFile)
@@ -79,6 +84,12 @@ class XBMCFileSystem(Vfs):
def exists(self, aFile):
return xbmcvfs.exists(aFile)
def fileSize(self, filename):
with xbmcvfs.File(filename) as f:
result = f.size() / 1024 # bytes to kilobytes
return result
class ZipFileSystem(Vfs):
zip = None
@@ -96,7 +107,7 @@ class ZipFileSystem(Vfs):
def put(self, source, dest):
aFile = xbmcvfs.File(xbmc.translatePath(source), 'r')
aFile = xbmcvfs.File(xbmcvfs.translatePath(source), 'r')
self.zip.writestr(dest, aFile.readBytes())
@@ -246,6 +257,16 @@ class DropboxFileSystem(Vfs):
else:
return False
def fileSize(self, filename):
result = 0
aFile = self._fix_slashes(filename)
if(self.client is not None):
metadata = self.client.files_get_metadata(aFile)
result = metadata.size / 1024 # bytes to KB
return result
def get_file(self, source, dest):
if(self.client is not None):
# write the file locally

View File

@@ -1,43 +1,392 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<settings>
<category id="general" label="30011">
<setting id="compress_backups" type="bool" label="30087" default="false" />
<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="upgrade_notes" type="number" label="upgrade_notes" visible="false" default="1" />
</category>
<category id="backup_path" label="30048">
<setting id="remote_selection" type="enum" lvalues="30018|30019|30027" default="0" label="30025"/>
<setting id="remote_path_2" type="text" label="30024" default="" visible="eq(-1,1)" />
<setting id="remote_path" type="folder" label="30020" visible="eq(-2,0)" />
<setting id="dropbox_key" type="text" label="30028" visible="eq(-3,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_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_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/launcher.py,action=remove_auth)" visible="gt(-9,1)"/>
</category>
<category id="selection" label="30012">
<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" visible="eq(-1,0)"/>
<setting id="backup_config" type="bool" label="30035" default="true" visible="eq(-2,0)"/>
<setting id="backup_database" type="bool" label="30032" default="true" visible="eq(-3,0)"/>
<setting id="backup_game_saves" type="bool" label="30133" default="false" visible="eq(-4,0)" />
<setting id="backup_playlists" type="bool" label="30033" default="true" visible="eq(-5,0)"/>
<setting id="backup_profiles" type="bool" label="30080" default="false" visible="eq(-6,0)"/>
<setting id="backup_thumbnails" type="bool" label="30034" default="true" visible="eq(-7,0)"/>
<setting id="backup_addons" type="bool" label="30030" default="true" visible="eq(-8,0)" />
<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="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 id="scheduling" label="30013">
<setting id="enable_scheduler" type="bool" label="30060" default="false" />
<setting id="schedule_interval" type="enum" label="30061" lvalues="30079|30072|30073|30074|30075" default="1" enable="eq(-1,true)"/>
<setting id="schedule_time" type="labelenum" label="30062" values="00:00|01:00|02:00|03:00|04:00|05:00|06:00|07:00|08:00|09:00|10:00|11:00|12:00|13:00|14:00|15:00|16:00|17:00|18:00|19:00|20:00|21:00|22:00|23:00" default="00:00" visible="!eq(-1,4)" enable="eq(-2,true)"/>
<setting id="day_of_week" type="enum" label="30063" lvalues="30065|30066|30067|30068|30069|30070|30071" default="0" visible="eq(-2,2)" enable="eq(-3,true)"/>
<setting id="cron_schedule" type="text" label="30064" default="0 0 * * *" visible="eq(-3,4)" enable="eq(-4,true)"/>
<setting id="schedule_miss" type="bool" label="30109" default="false" enable="eq(-5,true)" />
<setting id="cron_shutdown" type="bool" label="30076" default="false" enable="eq(-6,true)" />
</category>
<?xml version="1.0"?>
<settings version="1">
<section id="service.xbmcbackup">
<category id="general" label="30011" help="">
<group id="1" label="">
<!-- compress backups -->
<setting id="compress_backups" type="boolean" label="30087" help="">
<level>0</level>
<default>false</default>
<control type="toggle" />
</setting>
<!-- zip folder staging path -->
<setting id="zip_temp_path" type="string" label="30152" help="30153">
<level>3</level>
<default>special://temp</default>
<constraints>
<allowempty>true</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="compress_backups">true</dependency>
</dependencies>
<control type="edit" format="string">
<heading>30152</heading>
</control>
</setting>
<!-- backup rotation -->
<setting id="backup_rotation" type="integer" label="30026" help="">
<level>0</level>
<default>0</default>
<control type="edit" format="integer">
<heading>30026</heading>
</control>
</setting>
<!-- prompt to restore settings -->
<setting id="always_prompt_restore_settings" type="boolean" label="30148" help="30154">
<level>2</level>
<default>false</default>
<control type="toggle" />
</setting>
<!-- progress mode -->
<setting id="progress_mode" type="integer" label="30022" help="">
<level>1</level>
<default>0</default>
<constraints>
<options>
<option label="30082">0</option>
<option label="30083">1</option>
<option label="30084">2</option>
</options>
</constraints>
<control type="spinner" format="string" />
</setting>
</group>
<group id="2" label="">
<!-- verbose logging -->
<setting id="verbose_logging" type="boolean" label="30151" help="30155">
<level>3</level>
<default>false</default>
<control type="toggle" />
</setting>
<!-- upgrade notes not visible to users -->
<setting id="upgrade_notes" type="integer" label="upgrade_notes" help="">
<level>4</level>
<default>1</default>
<visible>false</visible>
<control type="edit" format="integer">
<heading>upgrade_notes</heading>
</control>
</setting>
</group>
</category>
<category id="backup_path" label="30048" help="">
<group id="1" label="">
<!-- backup repo type -->
<setting id="remote_selection" type="integer" label="30025" help="">
<level>0</level>
<default>0</default>
<constraints>
<options>
<option label="30018">0</option>
<option label="30019">1</option>
<option label="30027">2</option>
</options>
</constraints>
<control type="spinner" format="string" />
</setting>
<!-- folder select path -->
<setting id="remote_path" type="path" label="30020" help="">
<level>0</level>
<default/>
<constraints>
<allowempty>true</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="remote_selection">0</dependency>
</dependencies>
<control type="button" format="path">
<heading>30020</heading>
</control>
</setting>
<!-- type remote path -->
<setting id="remote_path_2" type="string" label="30024" help="">
<level>0</level>
<default></default>
<constraints>
<allowempty>true</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="remote_selection">1</dependency>
</dependencies>
<control type="edit" format="string">
<heading>30024</heading>
</control>
</setting>
<!-- dropbox key and secret -->
<setting id="dropbox_key" type="string" label="30028" help="30156">
<level>0</level>
<default></default>
<constraints>
<allowempty>true</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="remote_selection">2</dependency>
</dependencies>
<control type="edit" format="string">
<heading>30028</heading>
</control>
</setting>
<setting id="dropbox_secret" type="string" label="30029" help="30156">
<level>0</level>
<default></default>
<constraints>
<allowempty>true</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="remote_selection">2</dependency>
</dependencies>
<control type="edit" format="string">
<heading>30029</heading>
</control>
</setting>
<!-- authorize dropbox -->
<setting id="auth_dropbox_button" type="action" label="30104" help="">
<level>0</level>
<default />
<dependencies>
<dependency type="visible" setting="remote_selection">2</dependency>
</dependencies>
<control type="button" format="action">
<data>RunScript(script.xbmcbackup,mode=launcher,action=authorize_cloud,provider=dropbox)</data>
</control>
</setting>
</group>
<group id="2" label="">
<!-- remove auth button -->
<setting id="remove_auth_button" type="action" label="30093" help="">
<level>2</level>
<default />
<dependencies>
<dependency type="visible" setting="remote_selection">2</dependency>
</dependencies>
<control type="button" format="action">
<data>RunScript(script.xbmcbackup,mode=launcher,action=remove_auth)</data>
</control>
</setting>
</group>
</category>
<category id="selection" label="30012">
<group id="1" label="">
<!-- selection type (simple/advanced) -->
<setting id="backup_selection_type" type="integer" label="30023" help="30157">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="30014">0</option>
<option label="30015">1</option>
</options>
</constraints>
<control type="spinner" format="string" />
</setting>
<!-- simple selection settings -->
<setting id="backup_addon_data" type="boolean" label="30031" help="">
<level>0</level>
<default>false</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_config" type="boolean" label="30035" help="">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_database" type="boolean" label="30032" help="">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_game_saves" type="boolean" label="30133" help="">
<level>0</level>
<default>false</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_playlists" type="boolean" label="30033" help="">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_profiles" type="boolean" label="30080" help="">
<level>0</level>
<default>false</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_thumbnails" type="boolean" label="30034" help="">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<setting id="backup_addons" type="boolean" label="30030" help="">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="visible" setting="backup_selection_type">0</dependency>
</dependencies>
<control type="toggle" />
</setting>
<!-- advanced editor options -->
<setting id="advanced_button" type="action" label="30125" help="">
<level>2</level>
<default />
<dependencies>
<dependency type="visible" setting="backup_selection_type">1</dependency>
</dependencies>
<control type="button" format="action">
<data>RunScript(script.xbmcbackup,mode=launcher,action=advanced_editor)</data>
</control>
</setting>
<setting id="advanced_defaults" type="action" label="30139" help="">
<level>2</level>
<default />
<dependencies>
<dependency type="visible" setting="backup_selection_type">1</dependency>
</dependencies>
<control type="button" format="action">
<data>RunScript(script.xbmcbackup,mode=launcher,action=advanced_copy_config)</data>
</control>
</setting>
</group>
</category>
<category id="scheduling" label="30013">
<group id="1" label="">
<!-- enable scheduler -->
<setting id="enable_scheduler" type="boolean" label="30060" help="30158">
<level>0</level>
<default>false</default>
<control type="toggle" />
</setting>
<!-- schedule interval -->
<setting id="schedule_interval" type="integer" label="30061" help="">
<level>0</level>
<default>1</default>
<constraints>
<options>
<option label="30079">0</option>
<option label="30072">1</option>
<option label="30073">2</option>
<option label="30074">3</option>
<option label="30075">4</option>
</options>
</constraints>
<dependencies>
<dependency type="enable" setting="enable_scheduler">true</dependency>
</dependencies>
<control type="spinner" format="string"/>
</setting>
<!-- hour of the day -->
<setting id="schedule_time" type="string" label="30062" help="">
<level>0</level>
<default>00:00</default>
<constraints>
<options sort="ascending">
<option>00:00</option>
<option>01:00</option>
<option>02:00</option>
<option>03:00</option>
<option>04:00</option>
<option>05:00</option>
<option>06:00</option>
<option>07:00</option>
<option>08:00</option>
<option>09:00</option>
<option>10:00</option>
<option>11:00</option>
<option>12:00</option>
<option>13:00</option>
<option>14:00</option>
<option>15:00</option>
<option>16:00</option>
<option>17:00</option>
<option>18:00</option>
<option>19:00</option>
<option>20:00</option>
<option>21:00</option>
<option>22:00</option>
<option>23:00</option>
</options>
<allowempty>false</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="schedule_interval" operator="!is">4</dependency>
<dependency type="enable" setting="enable_scheduler">true</dependency>
</dependencies>
<control type="spinner" format="string"/>
</setting>
<!-- day of the week -->
<setting id="day_of_week" type="integer" label="30063" help="">
<level>0</level>
<default>0</default>
<constraints>
<options>
<option label="30065">0</option>
<option label="30066">1</option>
<option label="30067">2</option>
<option label="30068">3</option>
<option label="30069">4</option>
<option label="30070">5</option>
<option label="30071">6</option>
</options>
</constraints>
<dependencies>
<dependency type="visible" setting="schedule_interval">2</dependency>
<dependency type="enable" setting="enable_scheduler">true</dependency>
</dependencies>
<control type="spinner" format="string"/>
</setting>
<!-- cron schedule -->
<setting id="cron_schedule" type="string" label="30064" help="">
<level>0</level>
<default>0 0 * * *</default>
<constraints>
<allowempty>false</allowempty>
</constraints>
<dependencies>
<dependency type="visible" setting="schedule_interval">4</dependency>
<dependency type="enable" setting="enable_scheduler">true</dependency>
</dependencies>
<control type="edit" format="string">
<heading>30064</heading>
</control>
</setting>
<!-- run if schedule missed -->
<setting id="schedule_miss" type="boolean" label="30109" help="">
<level>1</level>
<default>false</default>
<dependencies>
<dependency type="enable" setting="enable_scheduler">true</dependency>
</dependencies>
<control type="toggle" />
</setting>
<!-- shutdown on complete -->
<setting id="cron_shutdown" type="boolean" label="30076" help="">
<level>1</level>
<default>false</default>
<dependencies>
<dependency type="enable" setting="enable_scheduler">true</dependency>
</dependencies>
<control type="toggle" />
</setting>
</group>
</category>
</section>
</settings>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<coordinates>
<left>502</left>
<top>345</top>
</coordinates>
<controls>
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="back" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="back" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<control type="image">
<left>-1920</left>
<top>-1080</top>
<width>5760</width>
<height>3240</height>
<animation effect="fade" start="0" end="100" time="300">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
<texture colordiffuse="C2FFFFFF">background-black.png</texture>
</control>
<control type="image">
<left>0</left>
<top>0</top>
<width>915</width>
<height>450</height>
<texture border="2">dialog-bg.png</texture>
</control>
<control type="textbox" id="502">
<left>0</left>
<top>20</top>
<width>915</width>
<height>300</height>
<font>font12_title</font>
<align>center</align>
<aligny>top</aligny>
<shadowcolor>FF000000</shadowcolor>
</control>
<control type="textbox" id="504">
<left>0</left>
<top>60</top>
<width>915</width>
<height>300</height>
<font>font12_title</font>
<align>center</align>
<aligny>top</aligny>
<shadowcolor>FF000000</shadowcolor>
</control>
<control type="image" id="501">
<left>300</left>
<top>80</top>
<width>300</width>
<height>300</height>
<aspectratio aligny="center" align="center">keep</aspectratio>
</control>
<control type="button" id="503">
<left>302</left>
<top>350</top>
<width>300</width>
<height>90</height>
<font>font12_title</font>
<textcolor>FFF0F0F0</textcolor>
<textoffsetx>20</textoffsetx>
<label>OK</label>
<align>center</align>
<aligny>center</aligny>
<texturefocus border="40" colordiffuse="FF12B2E7">dialogbutton-fo.png</texturefocus>
<texturenofocus border="40">dialogbutton-nofo.png</texturenofocus>
</control>
</controls>
</window>

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

4
service.py Normal file
View File

@@ -0,0 +1,4 @@
from resources.lib.scheduler import BackupScheduler
# start the backup scheduler
BackupScheduler().start()