Compare commits
78 Commits
1.6.0_fixe
...
matrix-1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b6260521 | ||
|
|
5aad014dbc | ||
|
|
ef3b820ca5 | ||
|
|
76e8e0efeb | ||
|
|
f7e77fd739 | ||
|
|
382dbce4ac | ||
|
|
4b066432be | ||
|
|
d71c923e78 | ||
|
|
b7587c6170 | ||
|
|
79cddb422c | ||
|
|
8415ec12ba | ||
|
|
a284451640 | ||
|
|
dc8d334352 | ||
|
|
c17a185639 | ||
|
|
aff124af1f | ||
|
|
350d81caf4 | ||
|
|
46d7d22523 | ||
|
|
11c644cb15 | ||
|
|
941b593751 | ||
|
|
e622a0455f | ||
|
|
9c1ecc254f | ||
|
|
44fdf7a20a | ||
|
|
ec214c074f | ||
|
|
02d852a7e9 | ||
|
|
d7e4946d9a | ||
|
|
6fdaa4f253 | ||
|
|
5f1f9fef38 | ||
|
|
4098cd18cb | ||
|
|
8119a09449 | ||
|
|
a0ccd85d9e | ||
|
|
71c8d9ae54 | ||
|
|
b470412b4f | ||
|
|
7a5886cd26 | ||
|
|
f20887b6e7 | ||
|
|
a0eb28a5f6 | ||
|
|
a198c9448a | ||
|
|
fa3a30eb55 | ||
|
|
201d04afeb | ||
|
|
2dabb23c2d | ||
|
|
2f19ec2b75 | ||
|
|
db215873cf | ||
|
|
bd963719d4 | ||
|
|
18b7f338c7 | ||
|
|
92a9245bdc | ||
|
|
568c3758a4 | ||
|
|
af999f7d04 | ||
|
|
1264ab86b2 | ||
|
|
90c458d4fc | ||
|
|
0e6f5acfb5 | ||
|
|
c9dd381037 | ||
|
|
55b2ac83d4 | ||
|
|
0d14dd17c6 | ||
|
|
9fa354b467 | ||
|
|
006485b19e | ||
|
|
3c2f512ecf | ||
|
|
190b4fd86f | ||
|
|
9ecf706d63 | ||
|
|
05c53b7ed8 | ||
|
|
8bc73f2832 | ||
|
|
edc4a7b20f | ||
|
|
90b4aeeebe | ||
|
|
7ce9123e1f | ||
|
|
8bfef6692f | ||
|
|
e63560f0c4 | ||
|
|
51f2ef3973 | ||
|
|
04bac77690 | ||
|
|
b1f6d36d73 | ||
|
|
5d398836ba | ||
|
|
23a14d67c4 | ||
|
|
47fcb119f3 | ||
|
|
f9f49e3fe6 | ||
|
|
7c23c17e33 | ||
|
|
8d66fa6a9f | ||
|
|
5ee610a586 | ||
|
|
8c4465f552 | ||
|
|
3849a902ea | ||
|
|
4492ab593e | ||
|
|
0cc0684263 |
7
.gitattributes
vendored
Normal 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
|
||||
10
.travis.yml
@@ -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
|
||||
16
README.md
@@ -1,13 +1,13 @@
|
||||
# Backup Addon
|
||||
 [](https://travis-ci.org/robweber/xbmcbackup) [](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [](https://www.python.org/dev/peps/pep-0008/)
|
||||
  [](https://travis-ci.com/robweber/xbmcbackup) [](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [](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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
34
addon.xml
@@ -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>
|
||||
|
||||
114
changelog.md
@@ -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
|
||||
|
||||
|
||||
69
default.py
@@ -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
|
||||
|
||||
59
launcher.py
@@ -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()
|
||||
2
resources/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
|
||||
*.pyo
|
||||
|
Before Width: | Height: | Size: 125 KiB |
BIN
resources/images/screenshot1.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 129 KiB |
BIN
resources/images/screenshot2.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 270 KiB |
BIN
resources/images/screenshot3.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 150 KiB |
BIN
resources/images/screenshot4.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 150 KiB |
BIN
resources/images/screenshot5.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 102 KiB |
BIN
resources/images/screenshot6.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 76 KiB |
@@ -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 ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
74
resources/skins/default/1080i/script-backup-qrcode.xml
Normal 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>
|
||||
BIN
resources/skins/default/media/background-black.png
Normal file
|
After Width: | Height: | Size: 166 B |
BIN
resources/skins/default/media/dialog-bg.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
resources/skins/default/media/dialogbutton-fo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
resources/skins/default/media/dialogbutton-nofo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
4
service.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from resources.lib.scheduler import BackupScheduler
|
||||
|
||||
# start the backup scheduler
|
||||
BackupScheduler().start()
|
||||