├── .gitignore ├── addon.xml ├── changelog.txt ├── default.py ├── fanart.jpg ├── icon.png ├── resources ├── __init__.py ├── lib │ ├── __init__.py │ └── modules │ │ ├── TextViewer.py │ │ ├── __init__.py │ │ ├── backtothefuture.py │ │ ├── control.py │ │ ├── logviewer.py │ │ ├── maintenance.py │ │ ├── pastebin.py │ │ ├── skinSwitch.py │ │ ├── speedtest.py │ │ ├── tools.py │ │ └── wiz.py ├── settings.xml └── skins │ └── Default │ ├── 1080i │ └── textview-skin.xml │ └── media │ ├── bg-fade.png │ ├── close-fo.png │ ├── close-nofo.png │ ├── scrollbar-V-background.png │ ├── scrollbar-V-focus.png │ └── scrollbar-V.png └── service.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | executable 8 | 9 | 10 | 11 | A Streamlined Utility Tool for Kodi 12 | No Bloatware, no Nonsense! Just Keep your Kodi sharp and clean with this utility tool... Now with a Custom Wizard. 13 | 14 | all 15 | The MIT License 16 | - 17 | https://github.com 18 | 19 | icon.png 20 | fanart.png 21 | 22 | 23 | 2023.01.09.0 24 | - Added AutoClean Cache functionality every x days at a given hour. 25 | - Backup always replaced the home folder reference in xml files with special://home 26 | Not all addons can handle this replace so it is now possible via a setting to disable this. 27 | - Backup Cancel didn't work. This is now fixed. 28 | 29 | 2023.01.01.0 30 | - Fix a fix on 2022.12.28.0 31 | 32 | 2022.12.28.0 33 | - Fix crash with full backup when there are foreign, like Hebrew, characters in xml files 34 | 35 | 2021.12.19.0 36 | - Made kodi 19 (Matrix) compatible. 37 | 38 | 2021.11.03.0 39 | - Fix Speedtest crash. 40 | 41 | 2020.12.19.1 42 | - The plugin was also installed under Video add-ons. Now it only is in Program add-ons where it belongs 43 | - Clean removed some files created by Common plugin cache or StorageServer or script.common.plugin.cache at kodi startup. 44 | This resulted in breaking cache functionality of kodi 45 | - Upload kodi log file to pastebin didn't always work and the first bytes were binary characters 46 | - Made all decode calls UTF-8 47 | - Improved ADVANCED SETTINGS(BUFFER SIZE) functionality 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | v1.0 2 | Initial Release 3 | 4 | v2020.12.19.1 5 | - The plugin was also installed under Video add-ons. Now it only is in Program add-ons where it belongs 6 | - Clean removed some files created by Common plugin cache or StorageServer or script.common.plugin.cache at kodi startup. 7 | This resulted in breaking cache functionality of kodi 8 | - Upload kodi log file to pastebin didn't always work and the first bytes were binary characters 9 | - Made all decode calls UTF-8 10 | - Improved ADVANCED SETTINGS(BUFFER SIZE) functionality 11 | 12 | v2021.11.03.0 13 | - Fix Speedtest crash. 14 | 15 | v2021.12.19.0 16 | - Made kodi 19 (Matrix) compatible. 17 | 18 | v2022.12.28.0 19 | - Fix backup. Crashed when foreign like Hebrew characters are used in xml files 20 | 21 | 2023.01.01.0 22 | - Fix a fix on 2022.12.28.0 23 | 24 | 2023.01.09.0 25 | - Added AutoClean Cache functionality every x days at a given hour. 26 | - Backup always replaced the home folder reference in xml files with special://home 27 | Not all addons can handle this replace so it is now possible via a setting to disable this. 28 | - Backup Cancel didn't work. This is now fixed. 29 | -------------------------------------------------------------------------------- /default.py: -------------------------------------------------------------------------------- 1 | import xbmc, xbmcaddon, xbmcgui, xbmcplugin, xbmcvfs,os,sys 2 | import urllib 3 | import re 4 | import time 5 | import requests 6 | from resources.lib.modules import control, tools 7 | from resources.lib.modules.backtothefuture import unicode, PY2 8 | from resources.lib.modules import maintenance 9 | 10 | if PY2: 11 | quote_plus = urllib.quote_plus 12 | translatePath = xbmc.translatePath 13 | else: 14 | quote_plus = urllib.parse.quote_plus 15 | translatePath = xbmcvfs.translatePath 16 | 17 | AddonID ='script.ezmaintenanceplus' 18 | USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3' 19 | selfAddon = xbmcaddon.Addon(id=AddonID) 20 | 21 | # ADDON SETTINGS 22 | wizard1 = control.setting('enable_wiz1') 23 | wizard2 = control.setting('enable_wiz2') 24 | wizard3 = control.setting('enable_wiz3') 25 | wizard4 = control.setting('enable_wiz4') 26 | wizard5 = control.setting('enable_wiz5') 27 | backupfull = control.setting('backup_database') 28 | backupaddons = control.setting('backup_addon_data') 29 | backupzip = control.setting("remote_backup") 30 | USB = translatePath(os.path.join(backupzip)) 31 | 32 | # ICONS FANARTS 33 | ADDON_FANART = control.addonFanart() 34 | ADDON_ICON = control.addonIcon() 35 | 36 | # DIRECTORIES 37 | backupdir = translatePath(os.path.join('special://home/backupdir','')) 38 | packagesdir = translatePath(os.path.join('special://home/addons/packages','')) 39 | USERDATA = translatePath(os.path.join('special://home/userdata','')) 40 | ADDON_DATA = translatePath(os.path.join(USERDATA,'addon_data')) 41 | HOME = translatePath('special://home/') 42 | HOME_ADDONS = translatePath('special://home/addons') 43 | backup_zip = translatePath(os.path.join(backupdir,'backup_addon_data.zip')) 44 | 45 | # DIALOGS 46 | dialog = xbmcgui.Dialog() 47 | progressDialog = xbmcgui.DialogProgress() 48 | 49 | AddonTitle = "EZ Maintenance+" 50 | EXCLUDES = [AddonID, 'backupdir','backup.zip','script.module.requests','script.module.urllib3','script.module.chardet','script.module.idna','script.module.certifi'] 51 | EXCLUDES_ADDONS = ['notification','packages'] 52 | 53 | def SETTINGS(): 54 | xbmcaddon.Addon(id=AddonID).openSettings() 55 | 56 | def ENABLE_WIZARD(): 57 | try: 58 | query = '{"jsonrpc":"2.0", "method":"Addons.SetAddonEnabled","params":{"addonid":"%s","enabled":true}, "id":1}' % (AddonID) 59 | xbmc.executeJSONRPC(query) 60 | 61 | except: 62 | pass 63 | 64 | # ######################### CATEGORIES ################################ 65 | def CATEGORIES(): 66 | CreateDir('[COLOR red][B]FRESH START[/B][/COLOR]','url','fresh_start',ADDON_ICON,ADDON_FANART,'') 67 | CreateDir('[COLOR lime][B]MY WIZARD[/B][/COLOR]','ur','builds',ADDON_ICON,ADDON_FANART,'', isFolder=True) 68 | CreateDir('[COLOR white][B]BACKUP/RESTORE[/B][/COLOR]','ur','backup_restore',ADDON_ICON,ADDON_FANART,'') 69 | # CreateDir('[COLOR white][B]TOOLS[/B][/COLOR]','ur','tools',ADDON_ICON,ADDON_FANART,'', isFolder=True) 70 | 71 | CreateDir('[COLOR white][B]MAINTENANCE[/B][/COLOR]','ur', 'maintenance', ADDON_ICON,ADDON_FANART,'', isFolder=True) 72 | CreateDir('[COLOR white][B]ADVANCED SETTINGS (BUFFER SIZE)[/B][/COLOR]','ur', 'adv_settings', ADDON_ICON,ADDON_FANART,'') 73 | CreateDir('[COLOR white][B]LOG VIEWER/UPLOADER[/B][/COLOR]','ur', 'log_tools', ADDON_ICON,ADDON_FANART,'') 74 | CreateDir('[COLOR white][B]SPEEDTEST[/B][/COLOR]','ur', 'speedtest', ADDON_ICON,ADDON_FANART,'') 75 | 76 | CreateDir('[COLOR white][B]SETTINGS[/B][/COLOR]','ur','settings',ADDON_ICON,ADDON_FANART,'') 77 | 78 | def CAT_TOOLS(): 79 | print ("NONE YET") 80 | 81 | def MAINTENANCE(): 82 | nextAutoCleanup = maintenance.getNextMaintenance() 83 | if nextAutoCleanup > 0: 84 | nextAutoCleanup = time.strftime("%a, %d %b %Y %I:%M:%S %p %Z", time.localtime(nextAutoCleanup)) 85 | CreateDir('Next Auto Cleanup: %s' % nextAutoCleanup,'xxx','xxx',None,ADDON_FANART,'',isFolder=False,iconImage='DefaultIconInfo.png') 86 | CreateDir('Clear Cache','url','clear_cache',ADDON_ICON,ADDON_FANART,'') 87 | CreateDir('Clear Packages','url','clear_packages',ADDON_ICON,ADDON_FANART,'') 88 | CreateDir('Clear Thumbnails','url','clear_thumbs',ADDON_ICON,ADDON_FANART,'') 89 | 90 | 91 | # ########################################################################################### 92 | # ########################################################################################### 93 | 94 | 95 | 96 | def OPEN_URL(url): 97 | r = requests.get(url).content 98 | return r 99 | 100 | 101 | def BUILDS(): 102 | if wizard1!='false': 103 | try: 104 | name = unicode(control.setting('name1')) 105 | url = unicode(control.setting('url1')) 106 | img = unicode(control.setting('img1')) 107 | fanart = unicode(control.setting('img1')) 108 | CreateDir('[COLOR lime][B][Wizard][/B][/COLOR] ' + name, url, 'install_build' , img, fanart, 'My custom Build', isFolder=False) 109 | except: pass 110 | if wizard2!='false': 111 | try: 112 | name=unicode(selfAddon.getSetting('name2')) 113 | url=unicode(selfAddon.getSetting('url2')) 114 | img=unicode(selfAddon.getSetting('img2')) 115 | fanart=unicode(selfAddon.getSetting('img2')) 116 | CreateDir('[COLOR skyblue][B][Wizard][/B][/COLOR] ' +name, url, 'install_build' , img, fanart, 'My custom Build', isFolder=False) 117 | except: pass 118 | if wizard3!='false': 119 | try: 120 | name=unicode(selfAddon.getSetting('name3')) 121 | url=unicode(selfAddon.getSetting('url3')) 122 | img=unicode(selfAddon.getSetting('img3')) 123 | fanart=unicode(selfAddon.getSetting('img3')) 124 | CreateDir('[COLOR cyan][B][Wizard][/B][/COLOR] ' +name, url, 'install_build' , img, fanart, 'My custom Build', isFolder=False) 125 | except: pass 126 | if wizard4!='false': 127 | try: 128 | name=unicode(selfAddon.getSetting('name4')) 129 | url=unicode(selfAddon.getSetting('url4')) 130 | img=unicode(selfAddon.getSetting('img4')) 131 | fanart=unicode(selfAddon.getSetting('img4')) 132 | CreateDir('[COLOR yellow][B][Wizard][/B][/COLOR] ' +name, url, 'install_build' , img, fanart, 'My custom Build', isFolder=False) 133 | except: pass 134 | if wizard5!='false': 135 | try: 136 | name=unicode(selfAddon.getSetting('name5')) 137 | url=unicode(selfAddon.getSetting('url5')) 138 | img=unicode(selfAddon.getSetting('img5')) 139 | fanart=unicode(selfAddon.getSetting('img5')) 140 | CreateDir('[COLOR purple][B][Wizard][/B][/COLOR] ' +name, url, 'install_build' , img, fanart, 'My custom Build', isFolder=False) 141 | except: pass 142 | 143 | def FRESHSTART(mode='verbose'): 144 | if mode != 'silent': select = xbmcgui.Dialog().yesno("Ez Maintenance+", 'Are you absolutely certain you want to wipe this install?' + '\n' + 'All addons EXCLUDING THIS WIZARD will be completely wiped!', yeslabel='Yes',nolabel='No') 145 | else: select = 1 146 | if select == 0: return 147 | elif select == 1: 148 | 149 | progressDialog.create(AddonTitle,"Wiping Install" + '\n' + 'Please Wait') 150 | try: 151 | for root, dirs, files in os.walk(HOME,topdown=True): 152 | dirs[:] = [d for d in dirs if d not in EXCLUDES] 153 | for name in files: 154 | try: 155 | os.remove(os.path.join(root,name)) 156 | os.rmdir(os.path.join(root,name)) 157 | except: pass 158 | 159 | for name in dirs: 160 | try: os.rmdir(os.path.join(root,name)); os.rmdir(root) 161 | except: pass 162 | except: pass 163 | REMOVE_EMPTY_FOLDERS() 164 | REMOVE_EMPTY_FOLDERS() 165 | REMOVE_EMPTY_FOLDERS() 166 | REMOVE_EMPTY_FOLDERS() 167 | REMOVE_EMPTY_FOLDERS() 168 | REMOVE_EMPTY_FOLDERS() 169 | REMOVE_EMPTY_FOLDERS() 170 | # RESTOREFAV() 171 | # ENABLE_WIZARD() 172 | if mode != 'silent': dialog.ok(AddonTitle,'Wipe Successful, The interface will now be reset...') 173 | 174 | 175 | # xbmc.executebuiltin('Mastermode') 176 | if mode != 'silent': xbmc.executebuiltin('LoadProfile(Master user)') 177 | # xbmc.executebuiltin('Mastermode') 178 | 179 | def REMOVE_EMPTY_FOLDERS(): 180 | #initialize the counters 181 | print('########### Start Removing Empty Folders #########') 182 | empty_count = 0 183 | used_count = 0 184 | for curdir, subdirs, files in os.walk(HOME): 185 | try: 186 | if len(subdirs) == 0 and len(files) == 0: #check for empty directories. len(files) == 0 may be overkill 187 | empty_count += 1 #increment empty_count 188 | os.rmdir(curdir) #delete the directory 189 | print('successfully removed: ' + curdir) 190 | elif len(subdirs) > 0 and len(files) > 0: #check for used directories 191 | used_count += 1 #increment used_count 192 | except:pass 193 | 194 | 195 | def killxbmc(): 196 | dialog.ok("PROCESS COMPLETE", 'The skin will now be reset' + '\n' + 'To start using your new setup please switch the skin System > Appearance > Skin to the desired one... if images are not showing, just restart Kodi' + '\n' + 'Click OK to Continue') 197 | 198 | # xbmc.executebuiltin('Mastermode') 199 | xbmc.executebuiltin('LoadProfile(Master user)') 200 | # xbmc.executebuiltin('Mastermode') 201 | 202 | 203 | 204 | 205 | def CreateDir(name, url, action, icon, fanart, description, isFolder=False, iconImage="DefaultFolder.png"): 206 | if icon == None or icon == '': icon = ADDON_ICON 207 | u=sys.argv[0]+"?url="+quote_plus(url)+"&action="+str(action)+"&name="+quote_plus(name)+"&icon="+quote_plus(icon)+"&fanart="+quote_plus(fanart)+"&description="+quote_plus(description) 208 | ok=True 209 | if PY2: 210 | liz=xbmcgui.ListItem(name, iconImage=iconImage, thumbnailImage=icon) 211 | else: 212 | liz=xbmcgui.ListItem(name) 213 | liz.setArt({'icon': iconImage}) 214 | liz.setArt({'thumbnailImage': icon}) 215 | liz.setInfo(type="Video", infoLabels={ "Title": name, "Plot": description } ) 216 | liz.setProperty( "Fanart_Image", fanart) 217 | ok=xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=liz,isFolder=isFolder) 218 | return ok 219 | 220 | 221 | if PY2: 222 | from urlparse import parse_qsl 223 | else: 224 | from urllib.parse import parse_qsl 225 | 226 | params = dict(parse_qsl(sys.argv[2].replace('?',''))) 227 | action = params.get('action') 228 | 229 | icon = params.get('icon') 230 | 231 | name = params.get('name') 232 | 233 | title = params.get('title') 234 | 235 | year = params.get('year') 236 | 237 | fanart = params.get('fanart') 238 | 239 | tvdb = params.get('tvdb') 240 | 241 | tmdb = params.get('tmdb') 242 | 243 | season = params.get('season') 244 | 245 | episode = params.get('episode') 246 | 247 | tvshowtitle = params.get('tvshowtitle') 248 | 249 | premiered = params.get('premiered') 250 | 251 | url = params.get('url') 252 | 253 | image = params.get('image') 254 | 255 | meta = params.get('meta') 256 | 257 | select = params.get('select') 258 | 259 | query = params.get('query') 260 | 261 | description = params.get('description') 262 | 263 | content = params.get('content') 264 | 265 | #xbmc.log("ezmaintenanceplus: action: %s" % action, level=xbmc.LOGINFO) 266 | 267 | if action == None: CATEGORIES() 268 | elif action == 'settings': control.openSettings() 269 | 270 | elif action == 'fresh_start': 271 | dialog.ok(AddonTitle,'Before Proceeding please switch skin to the default Kodi... Confluence or Estuary...') 272 | from resources.lib.modules import wiz 273 | wiz.skinswap() 274 | FRESHSTART() 275 | 276 | elif action == 'builds': BUILDS() 277 | elif action == 'tools': CAT_TOOLS() 278 | elif action == 'maintenance': MAINTENANCE() 279 | 280 | elif action == 'adv_settings': 281 | from resources.lib.modules import tools 282 | tools.advancedSettings() 283 | 284 | elif action == 'clear_cache': 285 | from resources.lib.modules import maintenance 286 | maintenance.clearCache() 287 | 288 | elif action == 'log_tools': 289 | from resources.lib.modules import logviewer 290 | logviewer.logView() 291 | 292 | 293 | elif action == 'clear_packages': 294 | from resources.lib.modules import maintenance 295 | maintenance.purgePackages() 296 | elif action == 'clear_thumbs': 297 | from resources.lib.modules import maintenance 298 | maintenance.deleteThumbnails() 299 | 300 | elif action == 'backup_restore': 301 | from resources.lib.modules import wiz 302 | typeOfBackup = ['BACKUP', 'RESTORE'] 303 | s_type = control.selectDialog(typeOfBackup) 304 | if s_type == 0: 305 | modes = ['Full Backup', 'Addons Settings'] 306 | select = control.selectDialog(modes) 307 | if select == 0: wiz.backup(mode='full') 308 | elif select == 1: wiz.backup(mode='userdata') 309 | elif s_type == 1: wiz.restoreFolder() 310 | 311 | elif action == 'install_build': 312 | from resources.lib.modules import wiz 313 | wiz.skinswap() 314 | yesDialog = dialog.yesno(AddonTitle, 'Do you want to perform a Fresh Start before Installing your Build?', yeslabel='Yes', nolabel='No') 315 | if yesDialog: FRESHSTART(mode='silent') 316 | 317 | wiz.buildInstaller(url) 318 | 319 | elif action == 'speedtest': 320 | xbmc.executebuiltin('Runscript("special://home/addons/script.ezmaintenanceplus/resources/lib/modules/speedtest.py")') 321 | 322 | xbmcplugin.endOfDirectory(int(sys.argv[1])) 323 | 324 | -------------------------------------------------------------------------------- /fanart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/fanart.jpg -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/icon.png -------------------------------------------------------------------------------- /resources/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/lib/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/lib/modules/TextViewer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import xbmc 4 | import xbmcaddon 5 | import xbmcgui 6 | from resources.lib.modules.backtothefuture import PY2 7 | 8 | Addon = xbmcaddon.Addon() 9 | addon = Addon.getAddonInfo('id') 10 | addonName = Addon.getAddonInfo('name') 11 | moduleName = 'Log Viewer' 12 | dialog = xbmcgui.Dialog() 13 | contents = '' 14 | path = '' 15 | 16 | # get actioncodes from keymap.xml 17 | ACTION_MOVE_LEFT = 1 18 | ACTION_MOVE_RIGHT = 2 19 | ACTION_MOVE_UP = 3 20 | ACTION_MOVE_DOWN = 4 21 | ACTION_PAGE_UP = 5 22 | ACTION_PAGE_DOWN = 6 23 | ACTION_SELECT_ITEM = 7 24 | 25 | 26 | class Viewer(xbmcgui.WindowXML): 27 | def __init__(self, strXMLname, strFallbackPath): 28 | self.previous_menu = 10 29 | self.back = 92 30 | self.page_up = 5 31 | self.page_down = 6 32 | 33 | # XML id's 34 | self.main_window = 1100 35 | self.title_box_control = 20301 36 | self.content_box_control = 20302 37 | self.list_box_control = 20303 38 | self.line_number_box_control = 20201 39 | self.scroll_bar = 20212 40 | 41 | def onInit(self): 42 | # title box 43 | title_box = self.getControl(self.title_box_control) 44 | title_box.setText(str.format('%s %s') % (addonName, moduleName)) 45 | 46 | # content box 47 | content_box = self.getControl(self.content_box_control) 48 | content_box.setText(contents) 49 | 50 | # Set initial focus 51 | self.setFocusId(self.scroll_bar) 52 | 53 | def onAction(self, action): 54 | # non Display Button control 55 | if action == self.previous_menu: 56 | self.close() 57 | elif action == self.back: 58 | self.close() 59 | 60 | def onClick(self, control_id): 61 | if control_id == 20293: 62 | self.close() 63 | text_view(path) 64 | 65 | def onFocus(self, control_id): 66 | pass 67 | 68 | 69 | def text_view(loc='', data=''): 70 | global contents 71 | global path 72 | contents = '' 73 | path = loc 74 | # todo, path can be a url to an internet file 75 | if not path and not data: return 76 | if path and not data: 77 | if 'http' in path.lower(): 78 | # todo, open internet files from a url path 79 | dialog.ok('Notice', 'This feature is not yet available') 80 | return 81 | # Open and read the file from path location 82 | temp_file = open(path, 'rb') 83 | contents = temp_file.read() 84 | temp_file.close() 85 | # Send contents to text display function 86 | elif data: 87 | contents = data 88 | if not contents: 89 | dialog.ok('Notice', 'The file was empty') 90 | return 91 | #contents = str(contents) 92 | if not PY2: 93 | contents = contents.decode('UTF-8') 94 | contents = contents.replace(' ERROR: ', ' [COLOR red]ERROR[/COLOR]: ') \ 95 | .replace(' WARNING: ', ' [COLOR gold]WARNING[/COLOR]: ') 96 | 97 | win = Viewer('textview-skin.xml', Addon.getAddonInfo('path')) 98 | win.doModal() 99 | del win 100 | 101 | # To call module put the following in the addon list or context menu 102 | # import TextViewer 103 | # TextViewer.text_view('log') -------------------------------------------------------------------------------- /resources/lib/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/lib/modules/__init__.py -------------------------------------------------------------------------------- /resources/lib/modules/backtothefuture.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import sys 4 | 5 | PY2 = sys.version_info[0] == 2 6 | PY3 = sys.version_info[0] == 3 7 | 8 | if PY2: 9 | # noinspection PyUnresolvedReferences 10 | import __builtin__ 11 | # noinspection PyShadowingBuiltins 12 | unichr = __builtin__.unichr 13 | # noinspection PyShadowingBuiltins 14 | unicode = __builtin__.unicode 15 | # noinspection PyShadowingBuiltins 16 | basestring = __builtin__.basestring 17 | else: 18 | # noinspection PyShadowingBuiltins 19 | unichr = chr 20 | # noinspection PyShadowingBuiltins 21 | unicode = str 22 | # noinspection PyShadowingBuiltins 23 | basestring = str 24 | -------------------------------------------------------------------------------- /resources/lib/modules/control.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | CONTROL ROUTINES 5 | ''' 6 | 7 | 8 | import os,sys 9 | 10 | import xbmc,xbmcaddon,xbmcplugin,xbmcgui,xbmcvfs 11 | from resources.lib.modules.backtothefuture import PY2 12 | 13 | 14 | integer = 1000 15 | 16 | lang = xbmcaddon.Addon().getLocalizedString 17 | 18 | lang2 = xbmc.getLocalizedString 19 | 20 | setting = xbmcaddon.Addon().getSetting 21 | 22 | setSetting = xbmcaddon.Addon().setSetting 23 | 24 | addon = xbmcaddon.Addon 25 | 26 | addItem = xbmcplugin.addDirectoryItem 27 | 28 | item = xbmcgui.ListItem 29 | 30 | directory = xbmcplugin.endOfDirectory 31 | 32 | content = xbmcplugin.setContent 33 | 34 | property = xbmcplugin.setProperty 35 | 36 | addonInfo = xbmcaddon.Addon().getAddonInfo 37 | 38 | infoLabel = xbmc.getInfoLabel 39 | 40 | condVisibility = xbmc.getCondVisibility 41 | 42 | jsonrpc = xbmc.executeJSONRPC 43 | 44 | window = xbmcgui.Window(10000) 45 | 46 | dialog = xbmcgui.Dialog() 47 | 48 | progressDialog = xbmcgui.DialogProgress() 49 | 50 | progressDialogBG = xbmcgui.DialogProgressBG() 51 | 52 | windowDialog = xbmcgui.WindowDialog() 53 | 54 | button = xbmcgui.ControlButton 55 | 56 | image = xbmcgui.ControlImage 57 | 58 | keyboard = xbmc.Keyboard 59 | 60 | sleep = xbmc.sleep 61 | 62 | execute = xbmc.executebuiltin 63 | 64 | skin = xbmc.getSkinDir() 65 | 66 | player = xbmc.Player() 67 | 68 | playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) 69 | 70 | resolve = xbmcplugin.setResolvedUrl 71 | 72 | openFile = xbmcvfs.File 73 | 74 | makeFile = xbmcvfs.mkdir 75 | 76 | deleteFile = xbmcvfs.delete 77 | 78 | deleteDir = xbmcvfs.rmdir 79 | 80 | listDir = xbmcvfs.listdir 81 | 82 | if PY2: 83 | translatePath = xbmc.translatePath 84 | else: 85 | translatePath = xbmcvfs.translatePath 86 | 87 | skinPath = translatePath('special://skin/') 88 | 89 | addonPath = translatePath(addonInfo('path')) 90 | 91 | AddonID = 'script.ezmaintenanceplus' 92 | artPath = translatePath(os.path.join('special://home/addons/' + AddonID, 'art')) 93 | # DIRECTORIES 94 | backupdir = translatePath(os.path.join('special://home/backupdir','')) 95 | packagesdir = translatePath(os.path.join('special://home/addons/packages','')) 96 | USERDATA = translatePath(os.path.join('special://home/userdata','')) 97 | ADDON_DATA = translatePath(os.path.join(USERDATA, 'addon_data')) 98 | HOME = translatePath('special://home/') 99 | HOME_ADDONS = translatePath('special://home/addons') 100 | 101 | 102 | def addonIcon(): 103 | path = translatePath(os.path.join('special://home/addons/' + AddonID , 'icon.png')) 104 | return path 105 | 106 | def addonThumb(): 107 | theme = appearance() ; art = artPath() 108 | if not (art == None and theme in ['-', '']): return os.path.join(art, 'poster.png') 109 | elif theme == '-': return 'DefaultFolder.png' 110 | return addonInfo('icon') 111 | 112 | 113 | def addonPoster(): 114 | theme = appearance() ; art = artPath() 115 | if not (art == None and theme in ['-', '']): return os.path.join(art, 'poster.png') 116 | return 'DefaultVideo.png' 117 | 118 | 119 | def addonBanner(): 120 | theme = appearance() ; art = artPath() 121 | if not (art == None and theme in ['-', '']): return os.path.join(art, 'banner.png') 122 | return 'DefaultVideo.png' 123 | 124 | 125 | def addonFanart(): 126 | return translatePath(os.path.join('special://home/addons/' + AddonID , 'fanart.jpg')) 127 | 128 | 129 | def addonNext(): 130 | theme = appearance() ; art = artPath() 131 | if not (art == None and theme in ['-', '']): return os.path.join(art, 'next.png') 132 | return 'DefaultVideo.png' 133 | 134 | 135 | 136 | def infoDialog(message, heading=addonInfo('name'), icon='', time=None, sound=False): 137 | if time == None: time = 3000 138 | else: time = int(time) 139 | if icon == '': icon = addonIcon() 140 | elif icon == 'INFO': icon = xbmcgui.NOTIFICATION_INFO 141 | elif icon == 'WARNING': icon = xbmcgui.NOTIFICATION_WARNING 142 | elif icon == 'ERROR': icon = xbmcgui.NOTIFICATION_ERROR 143 | dialog.notification(heading, message, icon, time, sound=sound) 144 | 145 | 146 | def yesnoDialog(line1, line2, line3, heading=addonInfo('name'), nolabel='', yeslabel=''): 147 | return dialog.yesno(heading, line1 + '\n' + line2 + '\n' + line3, nolabel=nolabel, yeslabel=yeslabel) 148 | 149 | 150 | def selectDialog(list, heading=addonInfo('name')): 151 | return dialog.select(heading, list) 152 | 153 | 154 | def openSettings(query=None, id=addonInfo('id')): 155 | try: 156 | idle() 157 | execute('Addon.OpenSettings(%s)' % id) 158 | if query == None: raise Exception() 159 | c, f = query.split('.') 160 | execute('SetFocus(%i)' % (int(c) + 100)) 161 | execute('SetFocus(%i)' % (int(f) + 200)) 162 | except: 163 | return 164 | 165 | 166 | def getCurrentViewId(): 167 | win = xbmcgui.Window(xbmcgui.getCurrentWindowId()) 168 | return str(win.getFocusId()) 169 | 170 | 171 | def refresh(): 172 | return execute('Container.Refresh') 173 | 174 | def busy(): 175 | return execute('ActivateWindow(busydialog)') 176 | 177 | def idle(): 178 | return execute('Dialog.Close(busydialog)') 179 | 180 | def queueItem(): 181 | return execute('Action(Queue)') -------------------------------------------------------------------------------- /resources/lib/modules/logviewer.py: -------------------------------------------------------------------------------- 1 | """ 2 | This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | """ 15 | import xbmc, xbmcaddon, xbmcgui, xbmcplugin, xbmcvfs,os,sys 16 | import urllib 17 | import re 18 | import time 19 | from resources.lib.modules import control 20 | from datetime import datetime 21 | from resources.lib.modules.backtothefuture import unicode, PY2 22 | 23 | dp = xbmcgui.DialogProgress() 24 | dialog = xbmcgui.Dialog() 25 | addonInfo = xbmcaddon.Addon().getAddonInfo 26 | 27 | AddonTitle="EZ Maintenance+" 28 | AddonID ='script.ezmaintenanceplus' 29 | 30 | def open_Settings(): 31 | open_Settings = xbmcaddon.Addon(id=AddonID).openSettings() 32 | 33 | def logView(): 34 | modes = ['View Log', 'Upload Log to Pastebin'] 35 | logPaths = [] 36 | logNames = [] 37 | select = control.selectDialog(modes) 38 | 39 | # Code to map the old translatePath 40 | try: 41 | translatePath = xbmcvfs.translatePath 42 | except AttributeError: 43 | translatePath = xbmc.translatePath 44 | 45 | try: 46 | if select == -1: raise Exception() 47 | logfile_path = translatePath('special://logpath') 48 | logfile_names = ('kodi.log', 'kodi.old.log', 'spmc.log', 'spmc.old.log', 'tvmc.log', 'freetelly.log', 'ftmc.log', 'firemc.log', 'nodi.log') 49 | for logfile_name in logfile_names: 50 | log_file_path = os.path.join(logfile_path, logfile_name) 51 | if os.path.isfile(log_file_path): 52 | logNames.append(logfile_name) 53 | logPaths.append(log_file_path) 54 | 55 | selectLog = control.selectDialog(logNames) 56 | selectedLog = logPaths[selectLog] 57 | if selectLog == -1: raise Exception() 58 | if select == 0: 59 | from resources.lib.modules import TextViewer 60 | TextViewer.text_view(selectedLog) 61 | elif select == 1: 62 | xbmc.executebuiltin('ActivateWindow(busydialognocancel)') 63 | f = open(selectedLog, 'rb') 64 | text = f.read() 65 | text = text.decode('UTF-8') 66 | f.close() 67 | from resources.lib.modules import pastebin 68 | upload_Link = pastebin.api().paste(unicode(text)) 69 | xbmc.executebuiltin('Dialog.Close(busydialognocancel)') 70 | print ("LOGVIEW UPLOADED LINK", upload_Link) 71 | if upload_Link != None: 72 | if not "Error" in upload_Link: 73 | label = "Log Link: [COLOR skyblue][B]" + upload_Link + "[/B][/COLOR]" 74 | dialog.ok(AddonTitle, "Log Uploaded to Pastebin" + '\n' + label) 75 | else: dialog.ok(AddonTitle, "Cannot Upload Log to Pastebin" + '\n' + "Reason " + upload_Link) 76 | else:dialog.ok(AddonTitle, "Cannot Upload Log to Pastebin") 77 | 78 | except:pass 79 | 80 | 81 | ############################## END ######################################### -------------------------------------------------------------------------------- /resources/lib/modules/maintenance.py: -------------------------------------------------------------------------------- 1 | import xbmc, xbmcaddon, xbmcgui, xbmcplugin, os, sys, xbmcvfs, glob, math, time 2 | import shutil 3 | import urllib 4 | import re 5 | import os 6 | from resources.lib.modules.backtothefuture import PY2 7 | 8 | # Code to map the old translatePath 9 | if PY2: 10 | translatePath = xbmc.translatePath 11 | loglevel = xbmc.LOGNOTICE 12 | else: 13 | translatePath = xbmcvfs.translatePath 14 | loglevel = xbmc.LOGINFO 15 | 16 | thumbnailPath = translatePath('special://thumbnails'); 17 | cachePath = os.path.join(translatePath('special://home'), 'cache') 18 | tempPath = translatePath('special://temp') 19 | addonPath = os.path.join(os.path.join(translatePath('special://home'), 'addons'),'script.ezmaintenance') 20 | 21 | mediaPath = os.path.join(addonPath, 'media') 22 | databasePath = translatePath('special://database') 23 | THUMBS = translatePath(os.path.join('special://home/userdata/Thumbnails','')) 24 | 25 | addon_id = 'script.ezmaintenanceplus' 26 | fanart = translatePath(os.path.join('special://home/addons/' + addon_id , 'fanart.jpg')) 27 | iconpath = translatePath(os.path.join('special://home/addons/' + addon_id, 'icon.png')) 28 | class cacheEntry: 29 | def __init__(self, namei, pathi): 30 | self.name = namei 31 | self.path = pathi 32 | 33 | def clearCache(mode='verbose'): 34 | if os.path.exists(cachePath)==True: 35 | for root, dirs, files in os.walk(cachePath): 36 | file_count = 0 37 | file_count += len(files) 38 | if file_count > 0: 39 | 40 | for f in files: 41 | try: 42 | if (f == "xbmc.log" or f == "xbmc.old.log" or f == "kodi.log" or f == "kodi.old.log" or f == "archive_cache" or f == "commoncache.db" or f == "commoncache.socket" or f == "temp"): continue 43 | os.unlink(os.path.join(root, f)) 44 | except: 45 | pass 46 | for d in dirs: 47 | try: 48 | if (d == "archive_cache" or d == "temp"): continue 49 | shutil.rmtree(os.path.join(root, d)) 50 | except: 51 | pass 52 | 53 | else: 54 | pass 55 | if os.path.exists(tempPath)==True: 56 | for root, dirs, files in os.walk(tempPath): 57 | file_count = 0 58 | file_count += len(files) 59 | if file_count > 0: 60 | for f in files: 61 | try: 62 | if (f == "xbmc.log" or f == "xbmc.old.log" or f == "kodi.log" or f == "kodi.old.log" or f == "archive_cache" or f == "commoncache.db" or f == "commoncache.socket" or f == "temp"): continue 63 | os.unlink(os.path.join(root, f)) 64 | except: 65 | pass 66 | for d in dirs: 67 | try: 68 | if (d == "archive_cache" or d == "temp"): continue 69 | shutil.rmtree(os.path.join(root, d)) 70 | except: 71 | pass 72 | 73 | else: 74 | pass 75 | if xbmc.getCondVisibility('system.platform.ATV2'): 76 | atv2_cache_a = os.path.join('/private/var/mobile/Library/Caches/AppleTV/Video/', 'Other') 77 | 78 | for root, dirs, files in os.walk(atv2_cache_a): 79 | file_count = 0 80 | file_count += len(files) 81 | 82 | if file_count > 0: 83 | for f in files: 84 | os.unlink(os.path.join(root, f)) 85 | for d in dirs: 86 | shutil.rmtree(os.path.join(root, d)) 87 | else: 88 | pass 89 | atv2_cache_b = os.path.join('/private/var/mobile/Library/Caches/AppleTV/Video/', 'LocalAndRental') 90 | 91 | for root, dirs, files in os.walk(atv2_cache_b): 92 | file_count = 0 93 | file_count += len(files) 94 | 95 | if file_count > 0: 96 | for f in files: 97 | os.unlink(os.path.join(root, f)) 98 | for d in dirs: 99 | shutil.rmtree(os.path.join(root, d)) 100 | else: 101 | pass 102 | 103 | cacheEntries = [] 104 | 105 | for entry in cacheEntries: 106 | clear_cache_path = translatePath(entry.path) 107 | if os.path.exists(clear_cache_path)==True: 108 | for root, dirs, files in os.walk(clear_cache_path): 109 | file_count = 0 110 | file_count += len(files) 111 | if file_count > 0: 112 | for f in files: 113 | os.unlink(os.path.join(root, f)) 114 | for d in dirs: 115 | shutil.rmtree(os.path.join(root, d)) 116 | else: 117 | pass 118 | 119 | if mode == 'verbose': xbmc.executebuiltin('Notification(%s, %s, %s, %s)' % ('Maintenance' , 'Clean Completed' , '3000', iconpath)) 120 | 121 | def deleteThumbnails(mode='verbose'): 122 | 123 | if os.path.exists(thumbnailPath)==True: 124 | # dialog = xbmcgui.Dialog() 125 | # if dialog.yesno("Delete Thumbnails", "This option deletes all thumbnails" + '\n' + "Are you sure you want to do this?"): 126 | for root, dirs, files in os.walk(thumbnailPath): 127 | file_count = 0 128 | file_count += len(files) 129 | if file_count > 0: 130 | for f in files: 131 | try: 132 | os.unlink(os.path.join(root, f)) 133 | except: 134 | pass 135 | 136 | 137 | if os.path.exists(THUMBS): 138 | try: 139 | for root, dirs, files in os.walk(THUMBS): 140 | file_count = 0 141 | file_count += len(files) 142 | # Count files and give option to delete 143 | if file_count > 0: 144 | for f in files: os.unlink(os.path.join(root, f)) 145 | for d in dirs: shutil.rmtree(os.path.join(root, d)) 146 | except: 147 | pass 148 | 149 | try: 150 | text13 = os.path.join(databasePath,"Textures13.db") 151 | os.unlink(text13) 152 | except: 153 | pass 154 | if mode == 'verbose': xbmc.executebuiltin('Notification(%s, %s, %s, %s)' % ('Maintenance' , 'Clean Thumbs Completed' , '3000', iconpath)) 155 | 156 | def purgePackages(mode='verbose'): 157 | 158 | purgePath = translatePath('special://home/addons/packages') 159 | dialog = xbmcgui.Dialog() 160 | for root, dirs, files in os.walk(purgePath): 161 | file_count = 0 162 | file_count += len(files) 163 | # if dialog.yesno("Delete Package Cache Files", "%d packages found."%file_count + '\n' + "Delete Them?"): 164 | for root, dirs, files in os.walk(purgePath): 165 | file_count = 0 166 | file_count += len(files) 167 | if file_count > 0: 168 | for f in files: 169 | os.unlink(os.path.join(root, f)) 170 | for d in dirs: 171 | shutil.rmtree(os.path.join(root, d)) 172 | # dialog = xbmcgui.Dialog() 173 | # dialog.ok("Maintenance", "Deleting Packages all done") 174 | if mode == 'verbose': xbmc.executebuiltin('Notification(%s, %s, %s, %s)' % ('Maintenance' , 'Clean Packages Completed' , '3000', iconpath)) 175 | 176 | def determineNextMaintenance(): 177 | getSetting = xbmcaddon.Addon().getSetting 178 | 179 | autoCleanDays = getSetting('autoCleanDays') 180 | if autoCleanDays is None: 181 | days = 0 182 | else: 183 | days = int(autoCleanDays) 184 | 185 | t1 = 0 186 | 187 | if days > 0: 188 | autoCleanHour = getSetting('autoCleanHour') 189 | if autoCleanHour is None: 190 | hour = 0 191 | else: 192 | hour = int(autoCleanHour) 193 | 194 | t0 = int(math.floor(time.time())) 195 | 196 | t1 = t0 + (days * 24 * 60 * 60) # days * 24h * 60m * 60s 197 | 198 | x = time.localtime(t1) 199 | 200 | t1 += (hour - x.tm_hour) * 60 * 60 - x.tm_min * 60 - x.tm_sec 201 | while (t1 <= t0): 202 | t1 += 24 * 60 * 60 # add days until we are in the future 203 | 204 | #t1 = t0 + 1 * 60 # for testing - every minute 205 | 206 | win = xbmcgui.Window(10000) 207 | win.setProperty("ezmaintenance.nextMaintenanceTime", str(t1)) 208 | 209 | logMaintenance("setNextMaintenance: %s" % str(t1)) 210 | 211 | 212 | def getNextMaintenance(): 213 | win = xbmcgui.Window(10000) 214 | t1 = int(win.getProperty("ezmaintenance.nextMaintenanceTime")) 215 | 216 | logMaintenance("getNextMaintenance: %s" % str(t1)) 217 | 218 | return t1 219 | 220 | def logMaintenance(message): 221 | # xbmc.log("ezmaintenanceplus: %s" % message, level=loglevel) 222 | return 223 | 224 | -------------------------------------------------------------------------------- /resources/lib/modules/pastebin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests, base64 3 | 4 | from resources.lib.modules.backtothefuture import PY2 5 | 6 | if PY2: 7 | import urlparse 8 | urljoin = urlparse.urljoin 9 | else: 10 | import urllib 11 | urljoin = urllib.parse.urljoin 12 | 13 | class api: 14 | def __init__(self): 15 | self.base_link = 'https://pastebin.com' 16 | self.paste_link = '/api/api_post.php' 17 | self.apiKey = base64.b64decode('MjNkNTNhMGMyMTdlZWY2OGM5ZWE3NDY0NDIwZTMzNmU=') 18 | 19 | 20 | def paste(self, text): 21 | url = urljoin(self.base_link, self.paste_link) 22 | payload = {'api_dev_key': self.apiKey, 'api_option':'paste', 'api_paste_code': text} 23 | result = requests.post(url, data=payload, timeout=10).content 24 | if not PY2: 25 | result = result.decode('UTF-8') 26 | if not self.base_link in result: return "Error: " + result 27 | else: return result 28 | 29 | -------------------------------------------------------------------------------- /resources/lib/modules/skinSwitch.py: -------------------------------------------------------------------------------- 1 | """ 2 | This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | """ 15 | import os, re, shutil, time, xbmc 16 | try: 17 | import json as simplejson 18 | except: 19 | import simplejson 20 | 21 | def getOld(old): 22 | try: 23 | old = '"%s"' % old 24 | query = '{"jsonrpc":"2.0", "method":"Settings.GetSettingValue","params":{"setting":%s}, "id":1}' % (old) 25 | response = xbmc.executeJSONRPC(query) 26 | response = simplejson.loads(response) 27 | if response.has_key('result'): 28 | if response['result'].has_key('value'): 29 | return response ['result']['value'] 30 | except: 31 | pass 32 | return None 33 | 34 | def setNew(new, value): 35 | try: 36 | new = '"%s"' % new 37 | value = '"%s"' % value 38 | query = '{"jsonrpc":"2.0", "method":"Settings.SetSettingValue","params":{"setting":%s,"value":%s}, "id":1}' % (new, value) 39 | response = xbmc.executeJSONRPC(query) 40 | except: 41 | pass 42 | return None 43 | 44 | def swapSkins(skin): 45 | old = 'lookandfeel.skin' 46 | value = skin 47 | current = getOld(old) 48 | new = old 49 | setNew(new, value) -------------------------------------------------------------------------------- /resources/lib/modules/speedtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2012-2018 Matt Martz 4 | # All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import os 19 | import re 20 | import csv 21 | import sys 22 | import math 23 | import errno 24 | import signal 25 | import socket 26 | import timeit 27 | import datetime 28 | import platform 29 | import threading 30 | import xml.parsers.expat 31 | from backtothefuture import unicode, PY2 32 | 33 | try: 34 | import gzip 35 | GZIP_BASE = gzip.GzipFile 36 | except ImportError: 37 | gzip = None 38 | GZIP_BASE = object 39 | 40 | try: 41 | import xml.etree.ElementTree as ET 42 | except: 43 | from xml.dom import minidom as DOM 44 | ET = None 45 | 46 | __version__ = '2.0.0' 47 | 48 | import time 49 | import xbmcgui, xbmcaddon 50 | dp = xbmcgui.DialogProgress() 51 | dp.create('Speedtest by Ookla', 'INITIALIZING...') 52 | downloadString = '0' 53 | 54 | class FakeShutdownEvent(object): 55 | """Class to fake a threading.Event.isSet so that users of this module 56 | are not required to register their own threading.Event() 57 | """ 58 | 59 | @staticmethod 60 | def isSet(): 61 | "Dummy method to always return false""" 62 | return False 63 | 64 | 65 | # Some global variables we use 66 | DEBUG = False 67 | _GLOBAL_DEFAULT_TIMEOUT = object() 68 | 69 | # Begin import game to handle Python 2 and Python 3 70 | try: 71 | import json 72 | except ImportError: 73 | try: 74 | import simplejson as json 75 | except ImportError: 76 | json = None 77 | 78 | 79 | from xml.dom import minidom as DOM 80 | ET = None 81 | 82 | try: 83 | from urllib2 import (urlopen, Request, HTTPError, URLError, 84 | AbstractHTTPHandler, ProxyHandler, 85 | HTTPDefaultErrorHandler, HTTPRedirectHandler, 86 | HTTPErrorProcessor, OpenerDirector) 87 | except ImportError: 88 | from urllib.request import (urlopen, Request, HTTPError, URLError, 89 | AbstractHTTPHandler, ProxyHandler, 90 | HTTPDefaultErrorHandler, HTTPRedirectHandler, 91 | HTTPErrorProcessor, OpenerDirector) 92 | 93 | try: 94 | from httplib import HTTPConnection 95 | except ImportError: 96 | from http.client import HTTPConnection 97 | 98 | try: 99 | from httplib import HTTPSConnection 100 | except ImportError: 101 | try: 102 | from http.client import HTTPSConnection 103 | except ImportError: 104 | HTTPSConnection = None 105 | 106 | try: 107 | from Queue import Queue 108 | except ImportError: 109 | from queue import Queue 110 | 111 | try: 112 | from urlparse import urlparse 113 | except ImportError: 114 | from urllib.parse import urlparse 115 | 116 | try: 117 | from urlparse import parse_qs 118 | except ImportError: 119 | try: 120 | from urllib.parse import parse_qs 121 | except ImportError: 122 | from cgi import parse_qs 123 | 124 | try: 125 | from hashlib import md5 126 | except ImportError: 127 | from md5 import md5 128 | 129 | try: 130 | from argparse import ArgumentParser as ArgParser 131 | from argparse import SUPPRESS as ARG_SUPPRESS 132 | PARSER_TYPE_INT = int 133 | PARSER_TYPE_STR = str 134 | PARSER_TYPE_FLOAT = float 135 | except ImportError: 136 | from optparse import OptionParser as ArgParser 137 | from optparse import SUPPRESS_HELP as ARG_SUPPRESS 138 | PARSER_TYPE_INT = 'int' 139 | PARSER_TYPE_STR = 'string' 140 | PARSER_TYPE_FLOAT = 'float' 141 | 142 | try: 143 | from cStringIO import StringIO 144 | BytesIO = None 145 | except ImportError: 146 | try: 147 | from StringIO import StringIO 148 | BytesIO = None 149 | except ImportError: 150 | from io import StringIO, BytesIO 151 | 152 | try: 153 | import __builtin__ 154 | except ImportError: 155 | import builtins 156 | from io import TextIOWrapper, FileIO 157 | 158 | class _Py3Utf8Output(TextIOWrapper): 159 | """UTF-8 encoded wrapper around stdout for py3, to override 160 | ASCII stdout 161 | """ 162 | def __init__(self, f, **kwargs): 163 | buf = FileIO(f.fileno(), 'w') 164 | super(_Py3Utf8Output, self).__init__( 165 | buf, 166 | encoding='utf8', 167 | errors='strict' 168 | ) 169 | 170 | def write(self, s): 171 | super(_Py3Utf8Output, self).write(s) 172 | self.flush() 173 | 174 | _py3_print = getattr(builtins, 'print') 175 | try: 176 | _py3_utf8_stdout = _Py3Utf8Output(sys.stdout) 177 | _py3_utf8_stderr = _Py3Utf8Output(sys.stderr) 178 | except: 179 | # sys.stdout/sys.stderr is not a compatible stdout/stderr object 180 | # just use it and hope things go ok 181 | _py3_utf8_stdout = sys.stdout 182 | _py3_utf8_stderr = sys.stderr 183 | 184 | def to_utf8(v): 185 | """No-op encode to utf-8 for py3""" 186 | return v 187 | 188 | def print_(*args, **kwargs): 189 | """Wrapper function for py3 to print, with a utf-8 encoded stdout""" 190 | if kwargs.get('file') == sys.stderr: 191 | kwargs['file'] = _py3_utf8_stderr 192 | else: 193 | kwargs['file'] = kwargs.get('file', _py3_utf8_stdout) 194 | _py3_print(*args, **kwargs) 195 | else: 196 | del __builtin__ 197 | 198 | def to_utf8(v): 199 | """Encode value to utf-8 if possible for py2""" 200 | try: 201 | return v.encode('utf8', 'strict') 202 | except AttributeError: 203 | return v 204 | 205 | def print_(*args, **kwargs): 206 | """The new-style print function for Python 2.4 and 2.5. 207 | 208 | Taken from https://pypi.python.org/pypi/six/ 209 | 210 | Modified to set encoding to UTF-8 always, and to flush after write 211 | """ 212 | fp = kwargs.pop("file", sys.stdout) 213 | if fp is None: 214 | return 215 | 216 | def write(data): 217 | if not isinstance(data, basestring): 218 | data = str(data) 219 | # If the file has an encoding, encode unicode with it. 220 | encoding = 'utf8' # Always trust UTF-8 for output 221 | if (isinstance(fp, file) and 222 | isinstance(data, unicode) and 223 | encoding is not None): 224 | errors = getattr(fp, "errors", None) 225 | if errors is None: 226 | errors = "strict" 227 | data = data.encode(encoding, errors) 228 | fp.write(data) 229 | fp.flush() 230 | want_unicode = False 231 | sep = kwargs.pop("sep", None) 232 | if sep is not None: 233 | if isinstance(sep, unicode): 234 | want_unicode = True 235 | elif not isinstance(sep, str): 236 | raise TypeError("sep must be None or a string") 237 | end = kwargs.pop("end", None) 238 | if end is not None: 239 | if isinstance(end, unicode): 240 | want_unicode = True 241 | elif not isinstance(end, str): 242 | raise TypeError("end must be None or a string") 243 | if kwargs: 244 | raise TypeError("invalid keyword arguments to print()") 245 | if not want_unicode: 246 | for arg in args: 247 | if isinstance(arg, unicode): 248 | want_unicode = True 249 | break 250 | if want_unicode: 251 | newline = unicode("\n") 252 | space = unicode(" ") 253 | else: 254 | newline = "\n" 255 | space = " " 256 | if sep is None: 257 | sep = space 258 | if end is None: 259 | end = newline 260 | for i, arg in enumerate(args): 261 | if i: 262 | write(sep) 263 | write(arg) 264 | write(end) 265 | 266 | 267 | # Exception "constants" to support Python 2 through Python 3 268 | try: 269 | import ssl 270 | try: 271 | CERT_ERROR = (ssl.CertificateError,) 272 | except AttributeError: 273 | CERT_ERROR = tuple() 274 | 275 | HTTP_ERRORS = ((HTTPError, URLError, socket.error, ssl.SSLError) + 276 | CERT_ERROR) 277 | except ImportError: 278 | HTTP_ERRORS = (HTTPError, URLError, socket.error) 279 | 280 | 281 | class SpeedtestException(Exception): 282 | """Base exception for this module""" 283 | 284 | 285 | class SpeedtestCLIError(SpeedtestException): 286 | """Generic exception for raising errors during CLI operation""" 287 | 288 | 289 | class SpeedtestHTTPError(SpeedtestException): 290 | """Base HTTP exception for this module""" 291 | 292 | 293 | class SpeedtestConfigError(SpeedtestException): 294 | """Configuration provided is invalid""" 295 | 296 | 297 | class ConfigRetrievalError(SpeedtestHTTPError): 298 | """Could not retrieve config.php""" 299 | 300 | 301 | class ServersRetrievalError(SpeedtestHTTPError): 302 | """Could not retrieve speedtest-servers.php""" 303 | 304 | 305 | class InvalidServerIDType(SpeedtestException): 306 | """Server ID used for filtering was not an integer""" 307 | 308 | 309 | class NoMatchedServers(SpeedtestException): 310 | """No servers matched when filtering""" 311 | 312 | 313 | class SpeedtestMiniConnectFailure(SpeedtestException): 314 | """Could not connect to the provided speedtest mini server""" 315 | 316 | 317 | class InvalidSpeedtestMiniServer(SpeedtestException): 318 | """Server provided as a speedtest mini server does not actually appear 319 | to be a speedtest mini server 320 | """ 321 | 322 | 323 | class ShareResultsConnectFailure(SpeedtestException): 324 | """Could not connect to speedtest.net API to POST results""" 325 | 326 | 327 | class ShareResultsSubmitFailure(SpeedtestException): 328 | """Unable to successfully POST results to speedtest.net API after 329 | connection 330 | """ 331 | 332 | 333 | class SpeedtestUploadTimeout(SpeedtestException): 334 | """testlength configuration reached during upload 335 | Used to ensure the upload halts when no additional data should be sent 336 | """ 337 | 338 | 339 | class SpeedtestBestServerFailure(SpeedtestException): 340 | """Unable to determine best server""" 341 | 342 | 343 | class SpeedtestMissingBestServer(SpeedtestException): 344 | """get_best_server not called or not able to determine best server""" 345 | 346 | 347 | def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, 348 | source_address=None): 349 | """Connect to *address* and return the socket object. 350 | 351 | Convenience function. Connect to *address* (a 2-tuple ``(host, 352 | port)``) and return the socket object. Passing the optional 353 | *timeout* parameter will set the timeout on the socket instance 354 | before attempting to connect. If no *timeout* is supplied, the 355 | global default timeout setting returned by :func:`getdefaulttimeout` 356 | is used. If *source_address* is set it must be a tuple of (host, port) 357 | for the socket to bind as a source address before making the connection. 358 | An host of '' or port 0 tells the OS to use the default. 359 | 360 | Largely vendored from Python 2.7, modified to work with Python 2.4 361 | """ 362 | 363 | host, port = address 364 | err = None 365 | for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): 366 | af, socktype, proto, canonname, sa = res 367 | sock = None 368 | try: 369 | sock = socket.socket(af, socktype, proto) 370 | if timeout is not _GLOBAL_DEFAULT_TIMEOUT: 371 | sock.settimeout(float(timeout)) 372 | if source_address: 373 | sock.bind(source_address) 374 | sock.connect(sa) 375 | return sock 376 | 377 | except socket.error: 378 | err = get_exception() 379 | if sock is not None: 380 | sock.close() 381 | 382 | if err is not None: 383 | raise err 384 | else: 385 | raise socket.error("getaddrinfo returns an empty list") 386 | 387 | 388 | class SpeedtestHTTPConnection(HTTPConnection): 389 | """Custom HTTPConnection to support source_address across 390 | Python 2.4 - Python 3 391 | """ 392 | def __init__(self, *args, **kwargs): 393 | source_address = kwargs.pop('source_address', None) 394 | context = kwargs.pop('context', None) 395 | timeout = kwargs.pop('timeout', 10) 396 | 397 | HTTPConnection.__init__(self, *args, **kwargs) 398 | 399 | self.source_address = source_address 400 | self._context = context 401 | self.timeout = timeout 402 | 403 | def connect(self): 404 | """Connect to the host and port specified in __init__.""" 405 | try: 406 | self.sock = socket.create_connection( 407 | (self.host, self.port), 408 | self.timeout, 409 | self.source_address 410 | ) 411 | except (AttributeError, TypeError): 412 | self.sock = create_connection( 413 | (self.host, self.port), 414 | self.timeout, 415 | self.source_address 416 | ) 417 | 418 | 419 | if HTTPSConnection: 420 | class SpeedtestHTTPSConnection(HTTPSConnection, 421 | SpeedtestHTTPConnection): 422 | """Custom HTTPSConnection to support source_address across 423 | Python 2.4 - Python 3 424 | """ 425 | def connect(self): 426 | "Connect to a host on a given (SSL) port." 427 | 428 | SpeedtestHTTPConnection.connect(self) 429 | 430 | kwargs = {} 431 | if hasattr(ssl, 'SSLContext'): 432 | kwargs['server_hostname'] = self.host 433 | 434 | self.sock = self._context.wrap_socket(self.sock, **kwargs) 435 | 436 | 437 | def _build_connection(connection, source_address, timeout, context=None): 438 | """Cross Python 2.4 - Python 3 callable to build an ``HTTPConnection`` or 439 | ``HTTPSConnection`` with the args we need 440 | 441 | Called from ``http(s)_open`` methods of ``SpeedtestHTTPHandler`` or 442 | ``SpeedtestHTTPSHandler`` 443 | """ 444 | def inner(host, **kwargs): 445 | kwargs.update({ 446 | 'source_address': source_address, 447 | 'timeout': timeout 448 | }) 449 | if context: 450 | kwargs['context'] = context 451 | return connection(host, **kwargs) 452 | return inner 453 | 454 | 455 | class SpeedtestHTTPHandler(AbstractHTTPHandler): 456 | """Custom ``HTTPHandler`` that can build a ``HTTPConnection`` with the 457 | args we need for ``source_address`` and ``timeout`` 458 | """ 459 | def __init__(self, debuglevel=0, source_address=None, timeout=10): 460 | AbstractHTTPHandler.__init__(self, debuglevel) 461 | self.source_address = source_address 462 | self.timeout = timeout 463 | 464 | def http_open(self, req): 465 | return self.do_open( 466 | _build_connection( 467 | SpeedtestHTTPConnection, 468 | self.source_address, 469 | self.timeout 470 | ), 471 | req 472 | ) 473 | 474 | http_request = AbstractHTTPHandler.do_request_ 475 | 476 | 477 | class SpeedtestHTTPSHandler(AbstractHTTPHandler): 478 | """Custom ``HTTPSHandler`` that can build a ``HTTPSConnection`` with the 479 | args we need for ``source_address`` and ``timeout`` 480 | """ 481 | def __init__(self, debuglevel=0, context=None, source_address=None, 482 | timeout=10): 483 | AbstractHTTPHandler.__init__(self, debuglevel) 484 | self._context = context 485 | self.source_address = source_address 486 | self.timeout = timeout 487 | 488 | def https_open(self, req): 489 | return self.do_open( 490 | _build_connection( 491 | SpeedtestHTTPSConnection, 492 | self.source_address, 493 | self.timeout, 494 | context=self._context, 495 | ), 496 | req 497 | ) 498 | 499 | https_request = AbstractHTTPHandler.do_request_ 500 | 501 | 502 | def build_opener(source_address=None, timeout=10): 503 | """Function similar to ``urllib2.build_opener`` that will build 504 | an ``OpenerDirector`` with the explicit handlers we want, 505 | ``source_address`` for binding, ``timeout`` and our custom 506 | `User-Agent` 507 | """ 508 | 509 | printer('Timeout set to %d' % timeout, debug=True) 510 | 511 | if source_address: 512 | source_address_tuple = (source_address, 0) 513 | printer('Binding to source address: %r' % (source_address_tuple,), 514 | debug=True) 515 | else: 516 | source_address_tuple = None 517 | 518 | handlers = [ 519 | ProxyHandler(), 520 | SpeedtestHTTPHandler(source_address=source_address_tuple, 521 | timeout=timeout), 522 | SpeedtestHTTPSHandler(source_address=source_address_tuple, 523 | timeout=timeout), 524 | HTTPDefaultErrorHandler(), 525 | HTTPRedirectHandler(), 526 | HTTPErrorProcessor() 527 | ] 528 | 529 | opener = OpenerDirector() 530 | opener.addheaders = [('User-agent', build_user_agent())] 531 | 532 | for handler in handlers: 533 | opener.add_handler(handler) 534 | 535 | return opener 536 | 537 | 538 | class GzipDecodedResponse(GZIP_BASE): 539 | """A file-like object to decode a response encoded with the gzip 540 | method, as described in RFC 1952. 541 | 542 | Largely copied from ``xmlrpclib``/``xmlrpc.client`` and modified 543 | to work for py2.4-py3 544 | """ 545 | def __init__(self, response): 546 | # response doesn't support tell() and read(), required by 547 | # GzipFile 548 | if not gzip: 549 | raise SpeedtestHTTPError('HTTP response body is gzip encoded, ' 550 | 'but gzip support is not available') 551 | IO = BytesIO or StringIO 552 | self.io = IO() 553 | while 1: 554 | chunk = response.read(1024) 555 | if len(chunk) == 0: 556 | break 557 | self.io.write(chunk) 558 | self.io.seek(0) 559 | gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io) 560 | 561 | def close(self): 562 | try: 563 | gzip.GzipFile.close(self) 564 | finally: 565 | self.io.close() 566 | 567 | 568 | def get_exception(): 569 | """Helper function to work with py2.4-py3 for getting the current 570 | exception in a try/except block 571 | """ 572 | return sys.exc_info()[1] 573 | 574 | 575 | def distance(origin, destination): 576 | """Determine distance between 2 sets of [lat,lon] in km""" 577 | 578 | lat1, lon1 = origin 579 | lat2, lon2 = destination 580 | radius = 6371 # km 581 | 582 | dlat = math.radians(lat2 - lat1) 583 | dlon = math.radians(lon2 - lon1) 584 | a = (math.sin(dlat / 2) * math.sin(dlat / 2) + 585 | math.cos(math.radians(lat1)) * 586 | math.cos(math.radians(lat2)) * math.sin(dlon / 2) * 587 | math.sin(dlon / 2)) 588 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 589 | d = radius * c 590 | 591 | return d 592 | 593 | 594 | def build_user_agent(): 595 | """Build a Mozilla/5.0 compatible User-Agent string""" 596 | 597 | ua_tuple = ( 598 | 'Mozilla/5.0', 599 | '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]), 600 | 'Python/%s' % platform.python_version(), 601 | '(KHTML, like Gecko)', 602 | 'speedtest-cli/%s' % __version__ 603 | ) 604 | user_agent = ' '.join(ua_tuple) 605 | printer('User-Agent: %s' % user_agent, debug=True) 606 | return user_agent 607 | 608 | 609 | def build_request(url, data=None, headers=None, bump='0', secure=False): 610 | """Build a urllib2 request object 611 | 612 | This function automatically adds a User-Agent header to all requests 613 | 614 | """ 615 | 616 | if not headers: 617 | headers = {} 618 | 619 | if url[0] == ':': 620 | scheme = ('http') 621 | schemed_url = '%s%s' % (scheme, url) 622 | else: 623 | schemed_url = url 624 | 625 | if '?' in url: 626 | delim = '&' 627 | else: 628 | delim = '?' 629 | 630 | # WHO YOU GONNA CALL? CACHE BUSTERS! 631 | final_url = '%s%sx=%s.%s' % (schemed_url, delim, 632 | int(timeit.time.time() * 1000), 633 | bump) 634 | 635 | headers.update({ 636 | 'Cache-Control': 'no-cache', 637 | }) 638 | 639 | printer('%s %s' % (('GET', 'POST')[bool(data)], final_url), 640 | debug=True) 641 | 642 | return Request(final_url, data=data, headers=headers) 643 | 644 | 645 | def catch_request(request, opener=None): 646 | """Helper function to catch common exceptions encountered when 647 | establishing a connection with a HTTP/HTTPS request 648 | 649 | """ 650 | 651 | if opener: 652 | _open = opener.open 653 | else: 654 | _open = urlopen 655 | 656 | try: 657 | uh = _open(request) 658 | return uh, False 659 | except HTTP_ERRORS: 660 | e = get_exception() 661 | return None, e 662 | 663 | 664 | def get_response_stream(response): 665 | """Helper function to return either a Gzip reader if 666 | ``Content-Encoding`` is ``gzip`` otherwise the response itself 667 | 668 | """ 669 | 670 | try: 671 | getheader = response.headers.getheader 672 | except AttributeError: 673 | getheader = response.getheader 674 | 675 | if getheader('content-encoding') == 'gzip': 676 | return GzipDecodedResponse(response) 677 | 678 | return response 679 | 680 | 681 | def get_attributes_by_tag_name(dom, tag_name): 682 | """Retrieve an attribute from an XML document and return it in a 683 | consistent format 684 | 685 | Only used with xml.dom.minidom, which is likely only to be used 686 | with python versions older than 2.5 687 | """ 688 | elem = dom.getElementsByTagName(tag_name)[0] 689 | return dict(list(elem.attributes.items())) 690 | 691 | 692 | def print_dots(shutdown_event): 693 | """Built in callback function used by Thread classes for printing 694 | status 695 | """ 696 | def inner(current, total, start=False, end=False): 697 | if shutdown_event.isSet(): 698 | return 699 | 700 | sys.stdout.write('.') 701 | if current + 1 == total and end is True: 702 | sys.stdout.write('\n') 703 | sys.stdout.flush() 704 | return inner 705 | 706 | 707 | def do_nothing(*args, **kwargs): 708 | pass 709 | 710 | 711 | class HTTPDownloader(threading.Thread): 712 | """Thread class for retrieving a URL""" 713 | 714 | def __init__(self, i, request, start, timeout, opener=None, 715 | shutdown_event=None): 716 | threading.Thread.__init__(self) 717 | self.request = request 718 | self.result = [0] 719 | self.starttime = start 720 | self.timeout = timeout 721 | self.i = i 722 | if opener: 723 | self._opener = opener.open 724 | else: 725 | self._opener = urlopen 726 | 727 | if shutdown_event: 728 | self._shutdown_event = shutdown_event 729 | else: 730 | self._shutdown_event = FakeShutdownEvent() 731 | 732 | def run(self): 733 | try: 734 | if (timeit.default_timer() - self.starttime) <= self.timeout: 735 | f = self._opener(self.request) 736 | while (not self._shutdown_event.isSet() and 737 | (timeit.default_timer() - self.starttime) <= 738 | self.timeout): 739 | self.result.append(len(f.read(10240))) 740 | if self.result[-1] == 0: 741 | break 742 | f.close() 743 | except IOError: 744 | pass 745 | 746 | 747 | class HTTPUploaderData(object): 748 | """File like object to improve cutting off the upload once the timeout 749 | has been reached 750 | """ 751 | 752 | def __init__(self, length, start, timeout, shutdown_event=None): 753 | self.length = length 754 | self.start = start 755 | self.timeout = timeout 756 | 757 | if shutdown_event: 758 | self._shutdown_event = shutdown_event 759 | else: 760 | self._shutdown_event = FakeShutdownEvent() 761 | 762 | self._data = None 763 | 764 | self.total = [0] 765 | 766 | def pre_allocate(self): 767 | chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 768 | multiplier = int(round(int(self.length) / 36.0)) 769 | IO = BytesIO or StringIO 770 | try: 771 | self._data = IO( 772 | ('content1=%s' % 773 | (chars * multiplier)[0:int(self.length) - 9] 774 | ).encode() 775 | ) 776 | except MemoryError: 777 | raise SpeedtestCLIError( 778 | 'Insufficient memory to pre-allocate upload data. Please ' 779 | 'use --no-pre-allocate' 780 | ) 781 | 782 | @property 783 | def data(self): 784 | if not self._data: 785 | self.pre_allocate() 786 | return self._data 787 | 788 | def read(self, n=10240): 789 | if ((timeit.default_timer() - self.start) <= self.timeout and 790 | not self._shutdown_event.isSet()): 791 | chunk = self.data.read(n) 792 | self.total.append(len(chunk)) 793 | return chunk 794 | else: 795 | raise SpeedtestUploadTimeout() 796 | 797 | def __len__(self): 798 | return self.length 799 | 800 | 801 | class HTTPUploader(threading.Thread): 802 | """Thread class for putting a URL""" 803 | 804 | def __init__(self, i, request, start, size, timeout, opener=None, 805 | shutdown_event=None): 806 | threading.Thread.__init__(self) 807 | self.request = request 808 | self.request.data.start = self.starttime = start 809 | self.size = size 810 | self.result = None 811 | self.timeout = timeout 812 | self.i = i 813 | 814 | if opener: 815 | self._opener = opener.open 816 | else: 817 | self._opener = urlopen 818 | 819 | if shutdown_event: 820 | self._shutdown_event = shutdown_event 821 | else: 822 | self._shutdown_event = FakeShutdownEvent() 823 | 824 | def run(self): 825 | request = self.request 826 | try: 827 | if ((timeit.default_timer() - self.starttime) <= self.timeout and 828 | not self._shutdown_event.isSet()): 829 | try: 830 | f = self._opener(request) 831 | except TypeError: 832 | # PY24 expects a string or buffer 833 | # This also causes issues with Ctrl-C, but we will concede 834 | # for the moment that Ctrl-C on PY24 isn't immediate 835 | request = build_request(self.request.get_full_url(), 836 | data=request.data.read(self.size)) 837 | f = self._opener(request) 838 | f.read(11) 839 | f.close() 840 | self.result = sum(self.request.data.total) 841 | else: 842 | self.result = 0 843 | except (IOError, SpeedtestUploadTimeout): 844 | self.result = sum(self.request.data.total) 845 | 846 | 847 | class SpeedtestResults(object): 848 | """Class for holding the results of a speedtest, including: 849 | 850 | Download speed 851 | Upload speed 852 | Ping/Latency to test server 853 | Data about server that the test was run against 854 | 855 | Additionally this class can return a result data as a dictionary or CSV, 856 | as well as submit a POST of the result data to the speedtest.net API 857 | to get a share results image link. 858 | """ 859 | 860 | def __init__(self, download=0, upload=0, ping=0, server=None, client=None, 861 | opener=None, secure=False): 862 | self.download = download 863 | self.upload = upload 864 | self.ping = ping 865 | if server is None: 866 | self.server = {} 867 | else: 868 | self.server = server 869 | self.client = client or {} 870 | 871 | self._share = None 872 | self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat() 873 | self.bytes_received = 0 874 | self.bytes_sent = 0 875 | 876 | if opener: 877 | self._opener = opener 878 | else: 879 | self._opener = build_opener() 880 | 881 | self._secure = secure 882 | 883 | def __repr__(self): 884 | return repr(self.dict()) 885 | 886 | def share(self): 887 | """POST data to the speedtest.net API to obtain a share results 888 | link 889 | """ 890 | 891 | if self._share: 892 | return self._share 893 | 894 | download = int(round(self.download / 1000.0, 0)) 895 | ping = int(round(self.ping, 0)) 896 | upload = int(round(self.upload / 1000.0, 0)) 897 | 898 | # Build the request to send results back to speedtest.net 899 | # We use a list instead of a dict because the API expects parameters 900 | # in a certain order 901 | api_data = [ 902 | 'recommendedserverid=%s' % self.server['id'], 903 | 'ping=%s' % ping, 904 | 'screenresolution=', 905 | 'promo=', 906 | 'download=%s' % download, 907 | 'screendpi=', 908 | 'upload=%s' % upload, 909 | 'testmethod=http', 910 | 'hash=%s' % md5(('%s-%s-%s-%s' % 911 | (ping, upload, download, '297aae72')) 912 | .encode()).hexdigest(), 913 | 'touchscreen=none', 914 | 'startmode=pingselect', 915 | 'accuracy=1', 916 | 'bytesreceived=%s' % self.bytes_received, 917 | 'bytessent=%s' % self.bytes_sent, 918 | 'serverid=%s' % self.server['id'], 919 | ] 920 | 921 | headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'} 922 | request = build_request('://www.speedtest.net/api/api.php', 923 | data='&'.join(api_data).encode(), 924 | headers=headers, secure=self._secure) 925 | f, e = catch_request(request, opener=self._opener) 926 | if e: 927 | raise ShareResultsConnectFailure(e) 928 | 929 | response = f.read() 930 | code = f.code 931 | f.close() 932 | 933 | if int(code) != 200: 934 | raise ShareResultsSubmitFailure('Could not submit results to ' 935 | 'speedtest.net') 936 | 937 | qsargs = parse_qs(response.decode('UTF-8')) 938 | resultid = qsargs.get('resultid') 939 | if not resultid or len(resultid) != 1: 940 | raise ShareResultsSubmitFailure('Could not submit results to ' 941 | 'speedtest.net') 942 | 943 | self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0] 944 | 945 | return self._share 946 | 947 | def dict(self): 948 | """Return dictionary of result data""" 949 | 950 | return { 951 | 'download': self.download, 952 | 'upload': self.upload, 953 | 'ping': self.ping, 954 | 'server': self.server, 955 | 'timestamp': self.timestamp, 956 | 'bytes_sent': self.bytes_sent, 957 | 'bytes_received': self.bytes_received, 958 | 'share': self._share, 959 | 'client': self.client, 960 | } 961 | 962 | @staticmethod 963 | def csv_header(delimiter=','): 964 | """Return CSV Headers""" 965 | 966 | row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance', 967 | 'Ping', 'Download', 'Upload', 'Share', 'IP Address'] 968 | out = StringIO() 969 | writer = csv.writer(out, delimiter=delimiter, lineterminator='') 970 | writer.writerow([to_utf8(v) for v in row]) 971 | return out.getvalue() 972 | 973 | def csv(self, delimiter=','): 974 | """Return data in CSV format""" 975 | 976 | data = self.dict() 977 | out = StringIO() 978 | writer = csv.writer(out, delimiter=delimiter, lineterminator='') 979 | row = [data['server']['id'], data['server']['sponsor'], 980 | data['server']['name'], data['timestamp'], 981 | data['server']['d'], data['ping'], data['download'], 982 | data['upload'], self._share or '', self.client['ip']] 983 | writer.writerow([to_utf8(v) for v in row]) 984 | return out.getvalue() 985 | 986 | def json(self, pretty=False): 987 | """Return data in JSON format""" 988 | 989 | kwargs = {} 990 | if pretty: 991 | kwargs.update({ 992 | 'indent': 4, 993 | 'sort_keys': True 994 | }) 995 | return json.dumps(self.dict(), **kwargs) 996 | 997 | 998 | class Speedtest(object): 999 | """Class for performing standard speedtest.net testing operations""" 1000 | 1001 | def __init__(self, config=None, source_address=None, timeout=10, 1002 | secure=False, shutdown_event=None): 1003 | self.config = {} 1004 | 1005 | self._source_address = source_address 1006 | self._timeout = timeout 1007 | self._opener = build_opener(source_address, timeout) 1008 | 1009 | self._secure = secure 1010 | 1011 | if shutdown_event: 1012 | self._shutdown_event = shutdown_event 1013 | else: 1014 | self._shutdown_event = FakeShutdownEvent() 1015 | 1016 | self.get_config() 1017 | if config is not None: 1018 | self.config.update(config) 1019 | 1020 | self.servers = {} 1021 | self.closest = [] 1022 | self._best = {} 1023 | 1024 | self.results = SpeedtestResults( 1025 | client=self.config['client'], 1026 | opener=self._opener, 1027 | secure=secure, 1028 | ) 1029 | 1030 | @property 1031 | def best(self): 1032 | if not self._best: 1033 | raise SpeedtestMissingBestServer( 1034 | 'get_best_server not called or not able to determine best ' 1035 | 'server' 1036 | ) 1037 | return self._best 1038 | 1039 | def get_config(self): 1040 | """Download the speedtest.net configuration and return only the data 1041 | we are interested in 1042 | """ 1043 | 1044 | headers = {} 1045 | if gzip: 1046 | headers['Accept-Encoding'] = 'gzip' 1047 | request = build_request('://www.speedtest.net/speedtest-config.php', 1048 | headers=headers, secure=self._secure) 1049 | 1050 | 1051 | uh, e = catch_request(request, opener=self._opener) 1052 | if e: 1053 | raise ConfigRetrievalError(e) 1054 | configxml = [] 1055 | 1056 | stream = get_response_stream(uh) 1057 | 1058 | print(stream) 1059 | 1060 | while 1: 1061 | try: 1062 | configxml.append(stream.read(1024)) 1063 | except (OSError, EOFError): 1064 | raise ConfigRetrievalError(get_exception()) 1065 | if len(configxml[-1]) == 0: 1066 | break 1067 | stream.close() 1068 | uh.close() 1069 | 1070 | print(configxml) 1071 | if int(uh.code) != 200: 1072 | return None 1073 | 1074 | printer('Config XML:\n%s' % ''.encode().join(configxml), debug=True) 1075 | 1076 | #buf = ''.join(configxml) 1077 | buf = ''.encode().join(configxml) 1078 | 1079 | try: 1080 | root = ET.fromstring(buf) 1081 | server_config = root.find('server-config').attrib 1082 | download = root.find('download').attrib 1083 | upload = root.find('upload').attrib 1084 | # times = root.find('times').attrib 1085 | client = root.find('client').attrib 1086 | 1087 | except: 1088 | root = DOM.parseString(buf) 1089 | server_config = get_attributes_by_tag_name(root, 'server-config') 1090 | download = get_attributes_by_tag_name(root, 'download') 1091 | upload = get_attributes_by_tag_name(root, 'upload') 1092 | # times = get_attributes_by_tag_name(root, 'times') 1093 | client = get_attributes_by_tag_name(root, 'client') 1094 | 1095 | try: 1096 | ignore_servers = list( 1097 | map(int, server_config['ignoreids'].split(',')) 1098 | ) 1099 | except: 1100 | ignore_servers = [] 1101 | 1102 | ratio = int(upload['ratio']) 1103 | upload_max = int(upload['maxchunkcount']) 1104 | up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032] 1105 | sizes = { 1106 | 'upload': up_sizes[ratio - 1:], 1107 | 'download': [350, 500, 750, 1000, 1500, 2000, 2500, 1108 | 3000, 3500, 4000] 1109 | } 1110 | 1111 | size_count = len(sizes['upload']) 1112 | 1113 | upload_count = int(math.ceil(upload_max / size_count)) 1114 | 1115 | counts = { 1116 | 'upload': upload_count, 1117 | 'download': int(download['threadsperurl']) 1118 | } 1119 | 1120 | threads = { 1121 | 'upload': int(upload['threads']), 1122 | 'download': int(server_config['threadcount']) * 2 1123 | } 1124 | 1125 | length = { 1126 | 'upload': int(upload['testlength']), 1127 | 'download': int(download['testlength']) 1128 | } 1129 | 1130 | self.config.update({ 1131 | 'client': client, 1132 | 'ignore_servers': ignore_servers, 1133 | 'sizes': sizes, 1134 | 'counts': counts, 1135 | 'threads': threads, 1136 | 'length': length, 1137 | 'upload_max': upload_count * size_count 1138 | }) 1139 | 1140 | self.lat_lon = (float(client['lat']), float(client['lon'])) 1141 | 1142 | printer('Config:\n%r' % self.config, debug=True) 1143 | 1144 | return self.config 1145 | 1146 | def get_servers(self, servers=None, exclude=None): 1147 | """Retrieve a the list of speedtest.net servers, optionally filtered 1148 | to servers matching those specified in the ``servers`` argument 1149 | """ 1150 | if servers is None: 1151 | servers = [] 1152 | 1153 | if exclude is None: 1154 | exclude = [] 1155 | 1156 | self.servers.clear() 1157 | 1158 | for server_list in (servers, exclude): 1159 | for i, s in enumerate(server_list): 1160 | try: 1161 | server_list[i] = int(s) 1162 | except ValueError: 1163 | raise InvalidServerIDType( 1164 | '%s is an invalid server type, must be int' % s 1165 | ) 1166 | 1167 | urls = [ 1168 | '://www.speedtest.net/speedtest-servers-static.php', 1169 | 'http://c.speedtest.net/speedtest-servers-static.php', 1170 | '://www.speedtest.net/speedtest-servers.php', 1171 | 'http://c.speedtest.net/speedtest-servers.php', 1172 | ] 1173 | 1174 | headers = {} 1175 | if gzip: 1176 | headers['Accept-Encoding'] = 'gzip' 1177 | 1178 | errors = [] 1179 | for url in urls: 1180 | try: 1181 | request = build_request( 1182 | '%s?threads=%s' % (url, 1183 | self.config['threads']['download']), 1184 | headers=headers, 1185 | secure=self._secure 1186 | ) 1187 | uh, e = catch_request(request, opener=self._opener) 1188 | if e: 1189 | errors.append('%s' % e) 1190 | raise ServersRetrievalError() 1191 | 1192 | stream = get_response_stream(uh) 1193 | 1194 | serversxml = [] 1195 | while 1: 1196 | try: 1197 | serversxml.append(stream.read(1024)) 1198 | except (OSError, EOFError): 1199 | raise ServersRetrievalError(get_exception()) 1200 | if len(serversxml[-1]) == 0: 1201 | break 1202 | 1203 | stream.close() 1204 | uh.close() 1205 | 1206 | if int(uh.code) != 200: 1207 | raise ServersRetrievalError() 1208 | 1209 | printer('Servers XML:\n%s' % ''.encode().join(serversxml), 1210 | debug=True) 1211 | 1212 | try: 1213 | #buf = ''.join(serversxml) 1214 | buf = ''.encode().join(serversxml) 1215 | try: 1216 | root = ET.fromstring(buf) 1217 | elements = root.getiterator('server') 1218 | except AttributeError: 1219 | root = DOM.parseString(buf) 1220 | elements = root.getElementsByTagName('server') 1221 | except (SyntaxError, xml.parsers.expat.ExpatError): 1222 | raise ServersRetrievalError() 1223 | 1224 | for server in elements: 1225 | try: 1226 | attrib = server.attrib 1227 | except AttributeError: 1228 | attrib = dict(list(server.attributes.items())) 1229 | 1230 | if servers and int(attrib.get('id')) not in servers: 1231 | continue 1232 | 1233 | if (int(attrib.get('id')) in self.config['ignore_servers'] 1234 | or int(attrib.get('id')) in exclude): 1235 | continue 1236 | 1237 | try: 1238 | d = distance(self.lat_lon, 1239 | (float(attrib.get('lat')), 1240 | float(attrib.get('lon')))) 1241 | except Exception: 1242 | continue 1243 | 1244 | attrib['d'] = d 1245 | 1246 | try: 1247 | self.servers[d].append(attrib) 1248 | except KeyError: 1249 | self.servers[d] = [attrib] 1250 | 1251 | break 1252 | 1253 | except ServersRetrievalError: 1254 | continue 1255 | 1256 | if (servers or exclude) and not self.servers: 1257 | raise NoMatchedServers() 1258 | 1259 | return self.servers 1260 | 1261 | def set_mini_server(self, server): 1262 | """Instead of querying for a list of servers, set a link to a 1263 | speedtest mini server 1264 | """ 1265 | 1266 | urlparts = urlparse(server) 1267 | 1268 | name, ext = os.path.splitext(urlparts[2]) 1269 | if ext: 1270 | url = os.path.dirname(server) 1271 | else: 1272 | url = server 1273 | 1274 | request = build_request(url) 1275 | uh, e = catch_request(request, opener=self._opener) 1276 | if e: 1277 | raise SpeedtestMiniConnectFailure('Failed to connect to %s' % 1278 | server) 1279 | else: 1280 | text = uh.read() 1281 | uh.close() 1282 | 1283 | extension = re.findall('upload_?[Ee]xtension: "([^"]+)"', 1284 | text.decode('UTF-8')) 1285 | if not extension: 1286 | for ext in ['php', 'asp', 'aspx', 'jsp']: 1287 | try: 1288 | f = self._opener.open( 1289 | '%s/speedtest/upload.%s' % (url, ext) 1290 | ) 1291 | except Exception: 1292 | pass 1293 | else: 1294 | data = f.read().strip().decode('UTF-8') 1295 | if (f.code == 200 and 1296 | len(data.splitlines()) == 1 and 1297 | re.match('size=[0-9]', data)): 1298 | extension = [ext] 1299 | break 1300 | if not urlparts or not extension: 1301 | raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: ' 1302 | '%s' % server) 1303 | 1304 | self.servers = [{ 1305 | 'sponsor': 'Speedtest Mini', 1306 | 'name': urlparts[1], 1307 | 'd': 0, 1308 | 'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]), 1309 | 'latency': 0, 1310 | 'id': 0 1311 | }] 1312 | 1313 | return self.servers 1314 | 1315 | def get_closest_servers(self, limit=5): 1316 | """Limit servers to the closest speedtest.net servers based on 1317 | geographic distance 1318 | """ 1319 | 1320 | if not self.servers: 1321 | self.get_servers() 1322 | 1323 | for d in sorted(self.servers.keys()): 1324 | for s in self.servers[d]: 1325 | self.closest.append(s) 1326 | if len(self.closest) == limit: 1327 | break 1328 | else: 1329 | continue 1330 | break 1331 | 1332 | printer('Closest Servers:\n%r' % self.closest, debug=True) 1333 | return self.closest 1334 | 1335 | def get_best_server(self, servers=None): 1336 | """Perform a speedtest.net "ping" to determine which speedtest.net 1337 | server has the lowest latency 1338 | """ 1339 | 1340 | if not servers: 1341 | if not self.closest: 1342 | servers = self.get_closest_servers() 1343 | servers = self.closest 1344 | 1345 | if self._source_address: 1346 | source_address_tuple = (self._source_address, 0) 1347 | else: 1348 | source_address_tuple = None 1349 | 1350 | user_agent = build_user_agent() 1351 | 1352 | results = {} 1353 | for server in servers: 1354 | cum = [] 1355 | url = os.path.dirname(server['url']) 1356 | stamp = int(timeit.time.time() * 1000) 1357 | latency_url = '%s/latency.txt?x=%s' % (url, stamp) 1358 | for i in range(0, 3): 1359 | this_latency_url = '%s.%s' % (latency_url, i) 1360 | printer('%s %s' % ('GET', this_latency_url), 1361 | debug=True) 1362 | urlparts = urlparse(latency_url) 1363 | try: 1364 | if urlparts[0] == 'https': 1365 | h = SpeedtestHTTPSConnection( 1366 | urlparts[1], 1367 | source_address=source_address_tuple 1368 | ) 1369 | else: 1370 | h = SpeedtestHTTPConnection( 1371 | urlparts[1], 1372 | source_address=source_address_tuple 1373 | ) 1374 | headers = {'User-Agent': user_agent} 1375 | path = '%s?%s' % (urlparts[2], urlparts[4]) 1376 | start = timeit.default_timer() 1377 | h.request("GET", path, headers=headers) 1378 | r = h.getresponse() 1379 | total = (timeit.default_timer() - start) 1380 | except HTTP_ERRORS: 1381 | e = get_exception() 1382 | printer('ERROR: %r' % e, debug=True) 1383 | cum.append(3600) 1384 | continue 1385 | 1386 | text = r.read(9) 1387 | if int(r.status) == 200 and text == 'test=test'.encode(): 1388 | cum.append(total) 1389 | else: 1390 | cum.append(3600) 1391 | h.close() 1392 | 1393 | avg = round((sum(cum) / 6) * 1000.0, 3) 1394 | results[avg] = server 1395 | 1396 | try: 1397 | fastest = sorted(results.keys())[0] 1398 | except IndexError: 1399 | raise SpeedtestBestServerFailure('Unable to connect to servers to ' 1400 | 'test latency.') 1401 | best = results[fastest] 1402 | best['latency'] = fastest 1403 | 1404 | self.results.ping = fastest 1405 | self.results.server = best 1406 | 1407 | self._best.update(best) 1408 | printer('Best Server:\n%r' % best, debug=True) 1409 | return best 1410 | 1411 | def download(self, callback=do_nothing): 1412 | """Test download speed against speedtest.net""" 1413 | 1414 | urls = [] 1415 | for size in self.config['sizes']['download']: 1416 | for _ in range(0, self.config['counts']['download']): 1417 | urls.append('%s/random%sx%s.jpg' % 1418 | (os.path.dirname(self.best['url']), size, size)) 1419 | 1420 | request_count = len(urls) 1421 | requests = [] 1422 | for i, url in enumerate(urls): 1423 | requests.append( 1424 | build_request(url, bump=i, secure=self._secure) 1425 | ) 1426 | 1427 | def producer(q, requests, request_count): 1428 | for i, request in enumerate(requests): 1429 | thread = HTTPDownloader( 1430 | i, 1431 | request, 1432 | start, 1433 | self.config['length']['download'], 1434 | opener=self._opener, 1435 | shutdown_event=self._shutdown_event 1436 | ) 1437 | thread.start() 1438 | q.put(thread, True) 1439 | callback(i, request_count, start=True) 1440 | 1441 | finished = [] 1442 | 1443 | def consumer(q, request_count): 1444 | while len(finished) < request_count: 1445 | thread = q.get(True) 1446 | while thread.is_alive(): 1447 | thread.join(timeout=0.1) 1448 | finished.append(sum(thread.result)) 1449 | callback(thread.i, request_count, end=True) 1450 | 1451 | q = Queue(self.config['threads']['download']) 1452 | prod_thread = threading.Thread(target=producer, 1453 | args=(q, requests, request_count)) 1454 | cons_thread = threading.Thread(target=consumer, 1455 | args=(q, request_count)) 1456 | start = timeit.default_timer() 1457 | prod_thread.start() 1458 | cons_thread.start() 1459 | while prod_thread.is_alive(): 1460 | prod_thread.join(timeout=0.1) 1461 | while cons_thread.is_alive(): 1462 | cons_thread.join(timeout=0.1) 1463 | 1464 | stop = timeit.default_timer() 1465 | self.results.bytes_received = sum(finished) 1466 | self.results.download = ( 1467 | (self.results.bytes_received / (stop - start)) * 8.0 1468 | ) 1469 | if self.results.download > 100000: 1470 | self.config['threads']['upload'] = 8 1471 | return self.results.download 1472 | 1473 | def upload(self, callback=do_nothing, pre_allocate=True): 1474 | """Test upload speed against speedtest.net""" 1475 | 1476 | sizes = [] 1477 | 1478 | for size in self.config['sizes']['upload']: 1479 | for _ in range(0, self.config['counts']['upload']): 1480 | sizes.append(size) 1481 | 1482 | # request_count = len(sizes) 1483 | request_count = self.config['upload_max'] 1484 | 1485 | requests = [] 1486 | for i, size in enumerate(sizes): 1487 | # We set ``0`` for ``start`` and handle setting the actual 1488 | # ``start`` in ``HTTPUploader`` to get better measurements 1489 | data = HTTPUploaderData( 1490 | size, 1491 | 0, 1492 | self.config['length']['upload'], 1493 | shutdown_event=self._shutdown_event 1494 | ) 1495 | if pre_allocate: 1496 | data.pre_allocate() 1497 | requests.append( 1498 | ( 1499 | build_request(self.best['url'], data, secure=self._secure), 1500 | size 1501 | ) 1502 | ) 1503 | 1504 | def producer(q, requests, request_count): 1505 | for i, request in enumerate(requests[:request_count]): 1506 | thread = HTTPUploader( 1507 | i, 1508 | request[0], 1509 | start, 1510 | request[1], 1511 | self.config['length']['upload'], 1512 | opener=self._opener, 1513 | shutdown_event=self._shutdown_event 1514 | ) 1515 | thread.start() 1516 | q.put(thread, True) 1517 | callback(i, request_count, start=True) 1518 | 1519 | finished = [] 1520 | 1521 | def consumer(q, request_count): 1522 | while len(finished) < request_count: 1523 | thread = q.get(True) 1524 | while thread.is_alive(): 1525 | thread.join(timeout=0.1) 1526 | finished.append(thread.result) 1527 | callback(thread.i, request_count, end=True) 1528 | 1529 | q = Queue(self.config['threads']['upload']) 1530 | prod_thread = threading.Thread(target=producer, 1531 | args=(q, requests, request_count)) 1532 | cons_thread = threading.Thread(target=consumer, 1533 | args=(q, request_count)) 1534 | start = timeit.default_timer() 1535 | prod_thread.start() 1536 | cons_thread.start() 1537 | while prod_thread.is_alive(): 1538 | prod_thread.join(timeout=0.1) 1539 | while cons_thread.is_alive(): 1540 | cons_thread.join(timeout=0.1) 1541 | 1542 | stop = timeit.default_timer() 1543 | self.results.bytes_sent = sum(finished) 1544 | self.results.upload = ( 1545 | (self.results.bytes_sent / (stop - start)) * 8.0 1546 | ) 1547 | return self.results.upload 1548 | 1549 | 1550 | def ctrl_c(shutdown_event): 1551 | """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded 1552 | operations 1553 | """ 1554 | def inner(signum, frame): 1555 | shutdown_event.set() 1556 | printer('\nCancelling...', error=True) 1557 | sys.exit(0) 1558 | return inner 1559 | 1560 | 1561 | def version(): 1562 | """Print the version""" 1563 | 1564 | printer(__version__) 1565 | sys.exit(0) 1566 | 1567 | 1568 | def csv_header(delimiter=','): 1569 | """Print the CSV Headers""" 1570 | 1571 | printer(SpeedtestResults.csv_header(delimiter=delimiter)) 1572 | sys.exit(0) 1573 | 1574 | 1575 | def parse_args(): 1576 | """Function to handle building and parsing of command line arguments""" 1577 | description = ( 1578 | 'Command line interface for testing internet bandwidth using ' 1579 | 'speedtest.net.\n' 1580 | '------------------------------------------------------------' 1581 | '--------------\n' 1582 | 'https://github.com/sivel/speedtest-cli') 1583 | 1584 | parser = ArgParser() 1585 | # Give optparse.OptionParser an `add_argument` method for 1586 | # compatibility with argparse.ArgumentParser 1587 | try: 1588 | parser.add_argument = parser.add_option 1589 | except AttributeError: 1590 | pass 1591 | parser.add_argument('--no-download', dest='download', default=True, 1592 | action='store_const', const=False, 1593 | help='Do not perform download test') 1594 | parser.add_argument('--no-upload', dest='upload', default=True, 1595 | action='store_const', const=False, 1596 | help='Do not perform upload test') 1597 | parser.add_argument('--bytes', dest='units', action='store_const', 1598 | const=('byte', 8), default=('bit', 1), 1599 | help='Display values in bytes instead of bits. Does ' 1600 | 'not affect the image generated by --share, nor ' 1601 | 'output from --json or --csv') 1602 | parser.add_argument('--share', action='store_true', default=True, 1603 | help='Generate and provide a URL to the speedtest.net ' 1604 | 'share results image, not displayed with --csv') 1605 | parser.add_argument('--simple', action='store_true', default=False, 1606 | help='Suppress verbose output, only show basic ' 1607 | 'information') 1608 | parser.add_argument('--csv', action='store_true', default=False, 1609 | help='Suppress verbose output, only show basic ' 1610 | 'information in CSV format. Speeds listed in ' 1611 | 'bit/s and not affected by --bytes') 1612 | parser.add_argument('--csv-delimiter', default=',', type=PARSER_TYPE_STR, 1613 | help='Single character delimiter to use in CSV ' 1614 | 'output. Default ","') 1615 | parser.add_argument('--csv-header', action='store_true', default=False, 1616 | help='Print CSV headers') 1617 | parser.add_argument('--json', action='store_true', default=False, 1618 | help='Suppress verbose output, only show basic ' 1619 | 'information in JSON format. Speeds listed in ' 1620 | 'bit/s and not affected by --bytes') 1621 | parser.add_argument('--list', action='store_true', 1622 | help='Display a list of speedtest.net servers ' 1623 | 'sorted by distance') 1624 | parser.add_argument('--server', type=PARSER_TYPE_INT, action='append', 1625 | help='Specify a server ID to test against. Can be ' 1626 | 'supplied multiple times') 1627 | parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append', 1628 | help='Exclude a server from selection. Can be ' 1629 | 'supplied multiple times') 1630 | parser.add_argument('--mini', help='URL of the Speedtest Mini server') 1631 | parser.add_argument('--source', help='Source IP address to bind to') 1632 | parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT, 1633 | help='HTTP timeout in seconds. Default 10') 1634 | parser.add_argument('--secure', action='store_true', 1635 | help='Use HTTPS instead of HTTP when communicating ' 1636 | 'with speedtest.net operated servers') 1637 | parser.add_argument('--no-pre-allocate', dest='pre_allocate', 1638 | action='store_const', default=True, const=False, 1639 | help='Do not pre allocate upload data. Pre allocation ' 1640 | 'is enabled by default to improve upload ' 1641 | 'performance. To support systems with ' 1642 | 'insufficient memory, use this option to avoid a ' 1643 | 'MemoryError') 1644 | parser.add_argument('--version', action='store_true', 1645 | help='Show the version number and exit') 1646 | parser.add_argument('--debug', action='store_true', 1647 | help=ARG_SUPPRESS, default=ARG_SUPPRESS) 1648 | 1649 | options = parser.parse_args() 1650 | if isinstance(options, tuple): 1651 | args = options[0] 1652 | else: 1653 | args = options 1654 | return args 1655 | 1656 | 1657 | def validate_optional_args(args): 1658 | """Check if an argument was provided that depends on a module that may 1659 | not be part of the Python standard library. 1660 | 1661 | If such an argument is supplied, and the module does not exist, exit 1662 | with an error stating which module is missing. 1663 | """ 1664 | optional_args = { 1665 | 'json': ('json/simplejson python module', json), 1666 | 'secure': ('SSL support', HTTPSConnection), 1667 | } 1668 | 1669 | for arg, info in optional_args.items(): 1670 | if getattr(args, arg, False) and info[1] is None: 1671 | raise SystemExit('%s is not installed. --%s is ' 1672 | 'unavailable' % (info[0], arg)) 1673 | 1674 | 1675 | 1676 | 1677 | def printer(string, quiet=False, debug=False, error=False, timer=False, **kwargs): 1678 | """Helper function print a string with various features""" 1679 | 1680 | global downloadString 1681 | 1682 | if debug and not DEBUG: 1683 | return 1684 | 1685 | if debug: 1686 | if sys.stdout.isatty(): 1687 | out = '\033[1;30mDEBUG: %s\033[0m' % string 1688 | else: 1689 | out = 'DEBUG: %s' % string 1690 | else: 1691 | out = string 1692 | 1693 | if error: 1694 | kwargs['file'] = sys.stderr 1695 | 1696 | if not quiet: 1697 | print_(out, **kwargs) 1698 | if "Download:" in out: downloadString = out 1699 | if "Upload:" in out: downloadString = downloadString + " | " + out 1700 | if downloadString != '0': label1 = downloadString 1701 | else: label1 = out 1702 | try: 1703 | if timer == True: 1704 | for i in range(0, 10): 1705 | count = i * 10 1706 | if downloadString != '0': label1 = downloadString 1707 | else: label1 = out 1708 | buf = '' 1709 | for key, value in kwargs.items(): 1710 | buf = buf + '\n' + value 1711 | dp.update(count, label1 + buf) 1712 | time.sleep(1) 1713 | timer = False 1714 | else: 1715 | buf = '' 1716 | for key, value in kwargs.items(): 1717 | buf = buf + '\n' + value 1718 | dp.update(100, label1 + buf) 1719 | 1720 | except:pass 1721 | 1722 | 1723 | def shell(): 1724 | """Run the full speedtest.net test""" 1725 | 1726 | global DEBUG 1727 | shutdown_event = threading.Event() 1728 | 1729 | args = parse_args() 1730 | 1731 | print(args) 1732 | 1733 | # Print the version and exit 1734 | if args.version: 1735 | version() 1736 | 1737 | if not args.download and not args.upload: 1738 | raise SpeedtestCLIError('Cannot supply both --no-download and ' 1739 | '--no-upload') 1740 | 1741 | if len(args.csv_delimiter) != 1: 1742 | raise SpeedtestCLIError('--csv-delimiter must be a single character') 1743 | 1744 | if args.csv_header: 1745 | csv_header(args.csv_delimiter) 1746 | 1747 | validate_optional_args(args) 1748 | 1749 | debug = getattr(args, 'debug', False) 1750 | if debug == 'SUPPRESSHELP': 1751 | debug = False 1752 | if debug: 1753 | DEBUG = True 1754 | 1755 | if args.simple or args.csv or args.json: 1756 | quiet = True 1757 | else: 1758 | quiet = False 1759 | 1760 | if args.csv or args.json: 1761 | machine_format = True 1762 | else: 1763 | machine_format = False 1764 | 1765 | # Don't set a callback if we are running quietly 1766 | if quiet or debug: 1767 | callback = do_nothing 1768 | else: 1769 | callback = print_dots(shutdown_event) 1770 | 1771 | printer('Retrieving speedtest.net configuration...', quiet) 1772 | try: 1773 | speedtest = Speedtest( 1774 | source_address=args.source, 1775 | timeout=args.timeout, 1776 | secure=args.secure 1777 | ) 1778 | except (ConfigRetrievalError,) + HTTP_ERRORS: 1779 | printer('Cannot retrieve speedtest configuration', error=True) 1780 | raise SpeedtestCLIError(get_exception()) 1781 | 1782 | if args.list: 1783 | try: 1784 | speedtest.get_servers() 1785 | except (ServersRetrievalError,) + HTTP_ERRORS: 1786 | printer('Cannot retrieve speedtest server list', error=True) 1787 | raise SpeedtestCLIError(get_exception()) 1788 | 1789 | for _, servers in sorted(speedtest.servers.items()): 1790 | for server in servers: 1791 | line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' 1792 | '[%(d)0.2f km]' % server) 1793 | try: 1794 | printer(line) 1795 | except IOError: 1796 | e = get_exception() 1797 | if e.errno != errno.EPIPE: 1798 | raise 1799 | sys.exit(0) 1800 | 1801 | printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'], 1802 | quiet) 1803 | 1804 | if not args.mini: 1805 | printer('Retrieving speedtest.net server list...', quiet) 1806 | try: 1807 | speedtest.get_servers(servers=args.server, exclude=args.exclude) 1808 | except NoMatchedServers: 1809 | raise SpeedtestCLIError( 1810 | 'No matched servers: %s' % 1811 | ', '.join('%s' % s for s in args.server) 1812 | ) 1813 | except (ServersRetrievalError,) + HTTP_ERRORS: 1814 | printer('Cannot retrieve speedtest server list', error=True) 1815 | raise SpeedtestCLIError(get_exception()) 1816 | except InvalidServerIDType: 1817 | raise SpeedtestCLIError( 1818 | '%s is an invalid server type, must ' 1819 | 'be an int' % ', '.join('%s' % s for s in args.server) 1820 | ) 1821 | 1822 | if args.server and len(args.server) == 1: 1823 | printer('Retrieving information for the selected server...', quiet) 1824 | else: 1825 | printer('Selecting best server based on ping...', quiet) 1826 | speedtest.get_best_server() 1827 | elif args.mini: 1828 | speedtest.get_best_server(speedtest.set_mini_server(args.mini)) 1829 | 1830 | results = speedtest.results 1831 | 1832 | printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' 1833 | '%(latency)s ms' % results.server, quiet) 1834 | 1835 | if args.download: 1836 | printer('Testing download speed', quiet, 1837 | end=('', '\n')[bool(debug)], error=False) 1838 | 1839 | speedtest.download(callback=callback) 1840 | printer('Download: %0.2f M%s/s' % 1841 | ((results.download / 1000.0 / 1000.0) / args.units[1], 1842 | args.units[0]), 1843 | quiet) 1844 | else: 1845 | printer('Skipping download test', quiet) 1846 | 1847 | if args.upload: 1848 | printer('Testing upload speed', quiet, 1849 | end=('', '\n')[bool(debug)]) 1850 | speedtest.upload(callback=callback, pre_allocate=args.pre_allocate) 1851 | printer('Upload: %0.2f M%s/s' % 1852 | ((results.upload / 1000.0 / 1000.0) / args.units[1], 1853 | args.units[0]), 1854 | quiet) 1855 | else: 1856 | printer('Skipping upload test', quiet) 1857 | 1858 | printer('Results:\n%r' % results.dict(), debug=True) 1859 | 1860 | if args.simple: 1861 | printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % 1862 | (results.ping, 1863 | (results.download / 1000.0 / 1000.0) / args.units[1], 1864 | args.units[0], 1865 | (results.upload / 1000.0 / 1000.0) / args.units[1], 1866 | args.units[0])) 1867 | elif args.csv: 1868 | printer(results.csv(delimiter=args.csv_delimiter)) 1869 | elif args.json: 1870 | if args.share: 1871 | results.share() 1872 | printer(results.json()) 1873 | 1874 | if args.share and not machine_format: 1875 | dp.close() 1876 | 1877 | printer('Share results: %s' % results.share()) 1878 | image = "%s" % results.share() 1879 | return image 1880 | 1881 | class PopupWindow(xbmcgui.WindowDialog): 1882 | def __init__(self, image): 1883 | self.addControl(xbmcgui.ControlImage(340 , 210 , 600 , 270 ,image)) 1884 | 1885 | def main(): 1886 | try: 1887 | image = shell() 1888 | 1889 | window = PopupWindow(image) 1890 | window.show() 1891 | time.sleep(10) 1892 | 1893 | window.close() 1894 | del window 1895 | except KeyboardInterrupt: 1896 | printer('\nCancelling...', error=True) 1897 | except (SpeedtestException, SystemExit): 1898 | e = get_exception() 1899 | # Ignore a successful exit, or argparse exit 1900 | if getattr(e, 'code', 1) not in (0, 2): 1901 | raise SystemExit('ERROR: %s' % e) 1902 | 1903 | 1904 | if __name__ == '__main__': 1905 | main() 1906 | 1907 | 1908 | -------------------------------------------------------------------------------- /resources/lib/modules/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | """ 15 | import xbmc, xbmcaddon, xbmcgui, xbmcplugin, xbmcvfs,os,sys 16 | import urllib 17 | import re 18 | import time 19 | import zipfile 20 | from math import trunc 21 | from resources.lib.modules import control 22 | from datetime import datetime 23 | from resources.lib.modules.backtothefuture import unicode, PY2 24 | 25 | if PY2: 26 | FancyURLopener = urllib.FancyURLopener 27 | translatePath = xbmc.translatePath 28 | else: 29 | FancyURLopener = urllib.request.FancyURLopener 30 | translatePath = xbmcvfs.translatePath 31 | 32 | dp = xbmcgui.DialogProgress() 33 | dialog = xbmcgui.Dialog() 34 | addonInfo = xbmcaddon.Addon().getAddonInfo 35 | 36 | AddonTitle="EZ Maintenance+" 37 | AddonID ='script.ezmaintenanceplus' 38 | 39 | 40 | def xml_data_advSettings_old(size): 41 | xml_data=""" 42 | 43 | 10 44 | 20 45 | 2 46 | %s 47 | 2 48 | 20 49 | 50 | """ % size 51 | return xml_data 52 | 53 | def xml_data_advSettings_New(size): 54 | xml_data=""" 55 | 56 | 10 57 | 20 58 | 2 59 | 60 | 61 | %s 62 | 2 63 | 20 64 | 65 | """ % size 66 | return xml_data 67 | 68 | def advancedSettings(): 69 | XML_FILE = translatePath(os.path.join('special://home/userdata' , 'advancedsettings.xml')) 70 | MEM = xbmc.getInfoLabel("System.Memory(total)") 71 | FREEMEM = xbmc.getInfoLabel("System.FreeMemory") 72 | BUFFER_F = re.sub('[^0-9]','',FREEMEM) 73 | BUFFER_F = int(BUFFER_F) / 3 74 | BUFFERSIZE = trunc(BUFFER_F * 1024 * 1024) 75 | try: KODIV = float(xbmc.getInfoLabel("System.BuildVersion")[:4]) 76 | except: KODIV = 16 77 | 78 | 79 | """,customlabel='Cancel'""" 80 | choice = dialog.yesno(AddonTitle, 'Based on your free Memory your optimal buffersize is: \n' + str(BUFFERSIZE) + ' Bytes' + ' (' + str(round(BUFFER_F)) + ' MB)' + '\n' + 'Note that your current advanced settings will be overwritten!' + '\n' + 'Choose an Option below or press ESC ESC to abort.', yeslabel='Use Optimal',nolabel='Input a Value' ) 81 | if choice == 1: 82 | with open(XML_FILE, "w") as f: 83 | if KODIV >= 17: xml_data = xml_data_advSettings_New(str(BUFFERSIZE)) 84 | else: xml_data = xml_data_advSettings_old(str(BUFFERSIZE)) 85 | 86 | f.write(xml_data) 87 | dialog.ok(AddonTitle,'Buffer Size Set to: ' + str(BUFFERSIZE) + '\n' + 'Please restart Kodi for settings to apply.') 88 | 89 | elif choice == 0: 90 | BUFFERSIZE = _get_keyboard( default=str(BUFFERSIZE), heading="INPUT BUFFER SIZE (Bytes) or ESC/Cancel to abort", cancel="-") 91 | if BUFFERSIZE != "-": 92 | with open(XML_FILE, "w") as f: 93 | if KODIV >= 17: xml_data = xml_data_advSettings_New(str(BUFFERSIZE)) 94 | else: xml_data = xml_data_advSettings_old(str(BUFFERSIZE)) 95 | f.write(xml_data) 96 | dialog.ok(AddonTitle,'Buffer Size Set to: ' + str(BUFFERSIZE) + '\n' + 'Please restart Kodi for settings to apply.') 97 | 98 | 99 | def open_Settings(): 100 | open_Settings = xbmcaddon.Addon(id=AddonID).openSettings() 101 | 102 | def _get_keyboard( default="", heading="", hidden=False, cancel="" ): 103 | """ shows a keyboard and returns a value """ 104 | if cancel == "": 105 | cancel=default 106 | keyboard = xbmc.Keyboard( default, heading, hidden ) 107 | keyboard.doModal() 108 | if ( keyboard.isConfirmed() ): 109 | return unicode( keyboard.getText()) 110 | return cancel 111 | 112 | 113 | ############################## END ######################################### -------------------------------------------------------------------------------- /resources/lib/modules/wiz.py: -------------------------------------------------------------------------------- 1 | """ 2 | This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | """ 15 | import xbmc, xbmcaddon, xbmcgui, xbmcplugin, xbmcvfs,os,sys 16 | import urllib 17 | import re 18 | import time 19 | import zipfile 20 | from resources.lib.modules import control, maintenance, tools 21 | from datetime import datetime 22 | from resources.lib.modules.backtothefuture import unicode, PY2 23 | 24 | if PY2: 25 | FancyURLopener = urllib.FancyURLopener 26 | from io import open as open 27 | translatePath = xbmc.translatePath 28 | else: 29 | FancyURLopener = urllib.request.FancyURLopener 30 | translatePath = xbmcvfs.translatePath 31 | unicode = str 32 | 33 | dp = xbmcgui.DialogProgress() 34 | dialog = xbmcgui.Dialog() 35 | addonInfo = xbmcaddon.Addon().getAddonInfo 36 | 37 | AddonTitle="EZ Maintenance+" 38 | AddonID ='script.ezmaintenanceplus' 39 | 40 | 41 | def get_Kodi_Version(): 42 | try: KODIV = float(xbmc.getInfoLabel("System.BuildVersion")[:4]) 43 | except: KODIV = 0 44 | return KODIV 45 | 46 | def open_Settings(): 47 | open_Settings = xbmcaddon.Addon(id=AddonID).openSettings() 48 | 49 | def ENABLE_ADDONS(): 50 | for root, dirs, files in os.walk(HOME_ADDONS,topdown=True): 51 | dirs[:] = [d for d in dirs] 52 | for addon_name in dirs: 53 | if not any(value in addon_name for value in EXCLUDES_ADDONS): 54 | # addLink(addon_name,'url',100,ART+'tool.png',FANART,'') 55 | try: 56 | query = '{"jsonrpc":"2.0", "method":"Addons.SetAddonEnabled","params":{"addonid":"%s","enabled":true}, "id":1}' % (addon_name) 57 | xbmc.executeJSONRPC(query) 58 | 59 | except: 60 | pass 61 | 62 | 63 | def FIX_SPECIAL(): 64 | 65 | HOME = translatePath('special://home') 66 | dp.create(AddonTitle,"Renaming paths...") 67 | url = translatePath('special://userdata') 68 | for root, dirs, files in os.walk(url): 69 | for file in files: 70 | if file.endswith(".xml"): 71 | if PY2: 72 | dp.update(0,"Fixing", "[COLOR dodgerblue]" + file + "[/COLOR]") 73 | else: 74 | dp.update(0,"Fixing" + '\n' + "[COLOR dodgerblue]" + file + "[/COLOR]") 75 | try: 76 | a = open((os.path.join(root, file)), 'r', encoding='utf-8').read() 77 | b = a.replace(HOME, 'special://home/') 78 | f = open((os.path.join(root, file)), mode='w', encoding='utf-8') 79 | f.write(unicode(b)) 80 | f.close() 81 | except: 82 | try: 83 | a = open((os.path.join(root, file)), 'r').read() 84 | b = a.replace(HOME, 'special://home/') 85 | f = open((os.path.join(root, file)), mode='w') 86 | f.write(unicode(b)) 87 | f.close() 88 | except: 89 | pass 90 | 91 | def skinswap(): 92 | 93 | skin = xbmc.getSkinDir() 94 | KODIV = get_Kodi_Version() 95 | skinswapped = 0 96 | from resources.lib.modules import skinSwitch 97 | 98 | #SWITCH THE SKIN IF THE CURRENT SKIN IS NOT CONFLUENCE 99 | if skin not in ['skin.confluence','skin.estuary']: 100 | choice = xbmcgui.Dialog().yesno(AddonTitle, 'We can try to reset to the default Kodi Skin...' + '\n' + 'Do you want to Proceed?', yeslabel='Yes',nolabel='No') 101 | if choice == 1: 102 | 103 | skin = 'skin.estuary' if KODIV >= 17 else 'skin.confluence' 104 | skinSwitch.swapSkins(skin) 105 | skinswapped = 1 106 | time.sleep(1) 107 | 108 | #IF A SKIN SWAP HAS HAPPENED CHECK IF AN OK DIALOG (CONFLUENCE INFO SCREEN) IS PRESENT, PRESS OK IF IT IS PRESENT 109 | if skinswapped == 1: 110 | if not xbmc.getCondVisibility("Window.isVisible(yesnodialog)"): 111 | xbmc.executebuiltin( "Action(Select)" ) 112 | 113 | #IF THERE IS NOT A YES NO DIALOG (THE SCREEN ASKING YOU TO SWITCH TO CONFLUENCE) THEN SLEEP UNTIL IT APPEARS 114 | if skinswapped == 1: 115 | while not xbmc.getCondVisibility("Window.isVisible(yesnodialog)"): 116 | time.sleep(1) 117 | 118 | #WHILE THE YES NO DIALOG IS PRESENT PRESS LEFT AND THEN SELECT TO CONFIRM THE SWITCH TO CONFLUENCE. 119 | if skinswapped == 1: 120 | while xbmc.getCondVisibility("Window.isVisible(yesnodialog)"): 121 | xbmc.executebuiltin( "Action(Left)" ) 122 | xbmc.executebuiltin( "Action(Select)" ) 123 | time.sleep(1) 124 | 125 | skin = xbmc.getSkinDir() 126 | 127 | #CHECK IF THE SKIN IS NOT CONFLUENCE 128 | if skin not in ['skin.confluence','skin.estuary']: 129 | choice = xbmcgui.Dialog().yesno(AddonTitle, '[COLOR lightskyblue][B]ERROR: AUTOSWITCH WAS NOT SUCCESFULL[/B][/COLOR]' + '\n' + '[COLOR lightskyblue][B]CLICK YES TO MANUALLY SWITCH TO CONFLUENCE NOW[/B][/COLOR]' + '\n' + '[COLOR lightskyblue][B]YOU CAN PRESS NO AND ATTEMPT THE AUTO SWITCH AGAIN IF YOU WISH[/B][/COLOR]', yeslabel='[B][COLOR green]YES[/COLOR][/B]',nolabel='[B][COLOR lightskyblue]NO[/COLOR][/B]') 130 | if choice == 1: 131 | xbmc.executebuiltin("ActivateWindow(appearancesettings)") 132 | return 133 | else: 134 | sys.exit(1) 135 | 136 | 137 | # BACKUP ZIP 138 | def backup(mode='full'): 139 | KODIV = get_Kodi_Version() 140 | 141 | backupdir = control.setting('download.path') 142 | if backupdir == '' or backupdir == None: 143 | control.infoDialog('Please Setup a Path for Downlads first') 144 | control.openSettings(query='1.3') 145 | return 146 | 147 | if mode == 'full': 148 | defaultName = "kodi_backup" 149 | BACKUPDATA = control.HOME 150 | getSetting = xbmcaddon.Addon().getSetting 151 | if getSetting('BackupFixSpecialHome') == 'true': 152 | FIX_SPECIAL() 153 | elif mode == 'userdata': 154 | defaultName = "kodi_settings" 155 | BACKUPDATA = control.USERDATA 156 | else: return 157 | if os.path.exists(BACKUPDATA): 158 | if not backupdir == '': 159 | name = tools._get_keyboard(default=defaultName, heading='Name your Backup', cancel="-") 160 | if name != "-": 161 | today = datetime.now().strftime('%Y%m%d%H%M') 162 | today = re.sub('[^0-9]', '', str(today)) 163 | zipDATE = "_%s.zip" % today 164 | name = re.sub(' ','_', name) + zipDATE 165 | backup_zip = translatePath(os.path.join(backupdir, name)) 166 | exclude_database = ['.pyo','.log'] 167 | 168 | try: 169 | maintenance.clearCache(mode='silent') 170 | maintenance.deleteThumbnails(mode='silent') 171 | maintenance.purgePackages(mode='silent') 172 | except:pass 173 | 174 | exclude_dirs = [''] 175 | canceled = CreateZip(BACKUPDATA, backup_zip, 'Creating Backup', 'Backing up files', exclude_dirs, exclude_database) 176 | if canceled: 177 | os.unlink(backup_zip) 178 | dialog.ok(AddonTitle,'Backup canceled') 179 | else: 180 | dialog.ok(AddonTitle,'Backup complete') 181 | else: 182 | dialog.ok(AddonTitle,'No backup location found: Please setup your Backup location') 183 | 184 | def restoreFolder(): 185 | names = [] 186 | links = [] 187 | zipFolder = control.setting('restore.path') 188 | if zipFolder == '' or zipFolder == None: 189 | control.infoDialog('Please Setup a Zip Files Location first') 190 | control.openSettings(query='2.0') 191 | return 192 | for zipFile in os.listdir(zipFolder): 193 | if zipFile.endswith(".zip"): 194 | url = translatePath(os.path.join(zipFolder, zipFile)) 195 | names.append(zipFile) 196 | links.append(url) 197 | select = control.selectDialog(names) 198 | if select != -1: restore(links[select]) 199 | 200 | def restore(zipFile): 201 | yesDialog = dialog.yesno(AddonTitle, 'This will overwrite all your current settings ... Are you sure?', yeslabel='Yes', nolabel='No') 202 | if yesDialog: 203 | try: 204 | dp = xbmcgui.DialogProgress() 205 | dp.create("Restoring File","In Progress..." + '\n' + "Please Wait") 206 | dp.update(0, "" + '\n' + "Extracting Zip Please Wait") 207 | canceled = ExtractZip(zipFile, control.HOME, dp) 208 | if canceled: 209 | dialog.ok(AddonTitle,'Restore Canceled') 210 | else: 211 | dialog.ok(AddonTitle,'Restore Complete') 212 | xbmc.executebuiltin('ShutDown') 213 | except:pass 214 | 215 | 216 | 217 | def CreateZip(folder, zip_filename, message_header, message1, exclude_dirs, exclude_files): 218 | abs_src = os.path.abspath(folder) 219 | for_progress = [] 220 | ITEM =[] 221 | dp = xbmcgui.DialogProgress() 222 | dp.create(message_header, message1) 223 | try: os.remove(zip_filename) 224 | except: pass 225 | for base, dirs, files in os.walk(folder): 226 | for file in files: ITEM.append(file) 227 | N_ITEM =len(ITEM) 228 | count = 0 229 | canceled = False 230 | zip_file = zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED, allowZip64 = True) 231 | for dirpath, dirnames, filenames in os.walk(folder): 232 | if canceled: 233 | break 234 | try: 235 | dirnames[:] = [d for d in dirnames if d not in exclude_dirs] 236 | filenames[:] = [f for f in filenames if f not in exclude_files] 237 | 238 | for file in filenames: 239 | if dp.iscanceled(): 240 | canceled = True 241 | break 242 | count += 1 243 | for_progress.append(file) 244 | progress = len(for_progress) / float(N_ITEM) * 100 245 | if PY2: 246 | dp.update(int(progress),"Backing Up", 'FILES: ' + str(count) + '/' + str(N_ITEM) + ' [COLOR lime]' + str(file) + '[/COLOR]', 'Please Wait') 247 | else: 248 | dp.update(int(progress),"Backing Up" + '\n' + 'FILES: ' + str(count) + '/' + str(N_ITEM) + ' [COLOR lime]' + str(file) + '[/COLOR]' + '\n' + 'Please Wait') 249 | file = os.path.join(dirpath, file) 250 | file = os.path.normpath(file) 251 | arcname = file[len(abs_src) + 1:] 252 | zip_file.write(file, arcname) 253 | except:pass 254 | zip_file.close() 255 | 256 | return canceled 257 | 258 | # EXTRACT ZIP 259 | def ExtractZip(_in, _out, dp=None): 260 | if dp: return ExtractWithProgress(_in, _out, dp) 261 | return ExtractNOProgress(_in, _out) 262 | 263 | def ExtractNOProgress(_in, _out): 264 | canceled = False 265 | 266 | try: 267 | zin = zipfile.ZipFile(_in, 'r') 268 | zin.extractall(_out) 269 | except Exception as e: 270 | print(str(e)) 271 | return canceled 272 | 273 | def ExtractWithProgress(_in, _out, dp): 274 | zin = zipfile.ZipFile(_in, 'r') 275 | nFiles = float(len(zin.infolist())) 276 | count = 0 277 | errors = 0 278 | canceled = False 279 | try: 280 | for item in zin.infolist(): 281 | canceled = dp.iscanceled() 282 | if canceled: 283 | break 284 | count += 1 285 | update = count / nFiles * 100 286 | try: name = os.path.basename(item.filename) 287 | except: name = item.filename 288 | label = '[COLOR skyblue][B]%s[/B][/COLOR]' % str(name) 289 | if PY2: 290 | dp.update(int(update),'Extracting... Errors: ' + str(errors) , label, '') 291 | else: 292 | dp.update(int(update),'Extracting... Errors: ' + str(errors) + '\n' + label) 293 | try: zin.extract(item, _out) 294 | except Exception as e: 295 | print ("EXTRACTING ERRORS", e) 296 | pass 297 | 298 | except Exception as e: 299 | print(str(e)) 300 | return canceled 301 | 302 | # INSTALL BUILD 303 | def buildInstaller(url): 304 | destination = dialog.browse(type=0, heading='Select Download Directory', shares='files',useThumbs=True, treatAsFolder=True, enableMultiple=False) 305 | if destination: 306 | dest = translatePath(os.path.join(destination, 'custom_build.zip')) 307 | downloader(url, dest) 308 | time.sleep(2) 309 | dp.create("Installing Build","In Progress..." + '\n' + "Please Wait") 310 | dp.update(0, "" + '\n' + "Extracting Zip Please Wait") 311 | ExtractZip(dest, control.HOME, dp) 312 | time.sleep(2) 313 | dp.close() 314 | dialog.ok(AddonTitle,'Installation Complete...' + '\n' + 'Your interface will now be reset' + '\n' + 'Click ok to Start...') 315 | xbmc.executebuiltin('LoadProfile(Master user)') 316 | # DOWNLOADER 317 | class customdownload(FancyURLopener): 318 | version = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11' 319 | 320 | def downloader(url, dest, dp = None): 321 | if not dp: 322 | dp = xbmcgui.DialogProgress() 323 | dp.create(AddonTitle) 324 | dp.update(0) 325 | start_time=time.time() 326 | customdownload().retrieve(url, dest, lambda nb, bs, fs, url=url: _pbhook(nb, bs, fs, dp, start_time)) 327 | 328 | def _pbhook(numblocks, blocksize, filesize, dp, start_time): 329 | try: 330 | percent = min(numblocks * blocksize * 100 / filesize, 100) 331 | currently_downloaded = float(numblocks) * blocksize / (1024 * 1024) 332 | kbps_speed = numblocks * blocksize / (time.time() - start_time) 333 | if kbps_speed > 0: 334 | eta = (filesize - numblocks * blocksize) / kbps_speed 335 | else: 336 | eta = 0 337 | kbps_speed = kbps_speed / 1024 338 | total = float(filesize) / (1024 * 1024) 339 | mbs = '%.02f MB of %.02f MB' % (currently_downloaded, total) 340 | e = 'Speed: %.02f Kb/s ' % kbps_speed 341 | e += 'ETA: %02d:%02d' % divmod(eta, 60) 342 | string = 'Downloading... Please Wait...' 343 | dp.update(percent, mbs + '\n' + e + '\n' + string) 344 | except: 345 | percent = 100 346 | dp.update(percent) 347 | dp.close() 348 | return 349 | 350 | if dp.iscanceled(): 351 | raise Exception("Canceled") 352 | dp.close() 353 | 354 | ############################## END ######################################### -------------------------------------------------------------------------------- /resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /resources/skins/Default/1080i/textview-skin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2 4 | 2 5 | 9000 6 | 7 | WindowOpen 8 | 9 | 0 10 | 0 11 | 12 | 13 | 14 | background image control 15 | 0 16 | 0 17 | 1920 18 | 1080 19 | true 20 | 200 21 | bg-fade.png 22 | keep 23 | 24 | 25 | 26 | Branding Logo 27 | 20 28 | 5 29 | 350 30 | 90 31 | true 32 | left 33 | top 34 | logo.png 35 | keep 36 | 37 | 38 | 39 | title box control 40 | 680 41 | 25 42 | 500 43 | 50 44 | true 45 | font20 46 | center 47 | top 48 | 49 | ff00c0ff 50 | 51 | 52 | 53 | contents box control 54 | 60 55 | 90 56 | 1750 57 | 890 58 | true 59 | font10 60 | left 61 | top 62 | 20291 63 | 20290 64 | 20293 65 | 20212 66 | FFD7F6DC 67 | 68 | 20212 69 | 70 | 71 | 72 | scroll bar vertical control 73 | 1870 74 | 90 75 | 30 76 | 890 77 | text 78 | true 79 | scrollbar-V-background.png 80 | scrollbar-V.png 81 | scrollbar-V-focus.png 82 | vertical 83 | true 84 | 85 | 86 | 20291 87 | 20215 88 | 20215 89 | 20215 90 | 91 | 92 | 93 | page scroll button group control 94 | 20302 95 | 75r 96 | 1030 97 | auto 98 | 50 99 | true 100 | 20212 101 | 20291 102 | 20290 103 | 20293 104 | 105 | 106 | list position 107 | 60r 108 | 0 109 | 500 110 | 30 111 | 112 | font10 113 | right 114 | ffffffff 115 | 116 | 117 | 118 | Page Down button control 119 | 50r 120 | 50 121 | 30 122 | FFFFFFFF 123 | page-down-focus.png 124 | page-down.png 125 | 20290 126 | 20217 127 | 20212 128 | 20291 129 | PageDown(20212) 130 | 131 | 132 | 133 | Page Up button control 134 | 0r 135 | 50 136 | 30 137 | FFFFFFFF 138 | page-up-focus.png 139 | page-up.png 140 | 20216 141 | 20293 142 | 20212 143 | 20291 144 | PageUp(20212) 145 | 146 | 147 | 148 | 149 | exit button control 150 | 95r 151 | 5 152 | 90 153 | 45 154 | true 155 | 156 | close-fo.png 157 | close-nofo.png 158 | 159 | close 160 | 20215 161 | 20212 162 | 20293 163 | 20215 164 | keep 165 | 166 | 167 | 168 | 169 | reload contents button control 170 | 20 171 | 1010 172 | 60 173 | 55 174 | true 175 | FFFFFFFF 176 | reload.png 177 | reload.png 178 | 20217 179 | 20290 180 | 20291 181 | 20291 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /resources/skins/Default/media/bg-fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/skins/Default/media/bg-fade.png -------------------------------------------------------------------------------- /resources/skins/Default/media/close-fo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/skins/Default/media/close-fo.png -------------------------------------------------------------------------------- /resources/skins/Default/media/close-nofo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/skins/Default/media/close-nofo.png -------------------------------------------------------------------------------- /resources/skins/Default/media/scrollbar-V-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/skins/Default/media/scrollbar-V-background.png -------------------------------------------------------------------------------- /resources/skins/Default/media/scrollbar-V-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/skins/Default/media/scrollbar-V-focus.png -------------------------------------------------------------------------------- /resources/skins/Default/media/scrollbar-V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peno64/script.ezmaintenanceplus/84d38337fc90217bb053b84595822b319f2195ad/resources/skins/Default/media/scrollbar-V.png -------------------------------------------------------------------------------- /service.py: -------------------------------------------------------------------------------- 1 | import xbmc, xbmcaddon, xbmcgui, xbmcplugin, os, sys, xbmcvfs, glob 2 | import shutil 3 | import urllib 4 | import re 5 | import time 6 | from resources.lib.modules.backtothefuture import PY2 7 | from resources.lib.modules import maintenance 8 | 9 | # Code to map the old translatePath 10 | if PY2: 11 | translatePath = xbmc.translatePath 12 | loglevel = xbmc.LOGNOTICE 13 | else: 14 | translatePath = xbmcvfs.translatePath 15 | loglevel = xbmc.LOGINFO 16 | 17 | AddonID ='script.ezmaintenanceplus' 18 | packagesdir = translatePath(os.path.join('special://home/addons/packages','')) 19 | thumbnails = translatePath('special://home/userdata/Thumbnails') 20 | dialog = xbmcgui.Dialog() 21 | setting = xbmcaddon.Addon().getSetting 22 | iconpath = translatePath(os.path.join('special://home/addons/' + AddonID,'icon.png')) 23 | # if setting('autoclean') == 'true': 24 | # control.clearCache() 25 | 26 | notify_mode = setting('notify_mode') 27 | auto_clean = setting('startup.cache') 28 | filesize = int(setting('filesize_alert')) 29 | filesize_thumb = int(setting('filesizethumb_alert')) 30 | maxpackage_zips = int(setting('packagenumbers_alert')) 31 | 32 | total_size2 = 0 33 | total_size = 0 34 | count = 0 35 | 36 | for dirpath, dirnames, filenames in os.walk(packagesdir): 37 | count = 0 38 | for f in filenames: 39 | count += 1 40 | fp = os.path.join(dirpath, f) 41 | total_size += os.path.getsize(fp) 42 | total_sizetext = "%.0f" % (total_size/1024000.0) 43 | 44 | if int(total_sizetext) > filesize: 45 | choice2 = xbmcgui.Dialog().yesno("[COLOR=red]Autocleaner[/COLOR]", 'The packages folder is [COLOR red]' + str(total_sizetext) +' MB [/COLOR] - [COLOR red]' + str(count) + '[/COLOR] zip files' + '\n' + 'The folder can be cleaned up without issues to save space...' + '\n' + 'Do you want to clean it now?', yeslabel='Yes',nolabel='No') 46 | if choice2 == 1: 47 | maintenance.purgePackages() 48 | 49 | for dirpath2, dirnames2, filenames2 in os.walk(thumbnails): 50 | for f2 in filenames2: 51 | fp2 = os.path.join(dirpath2, f2) 52 | total_size2 += os.path.getsize(fp2) 53 | total_sizetext2 = "%.0f" % (total_size2/1024000.0) 54 | 55 | if int(total_sizetext2) > filesize_thumb: 56 | choice2 = xbmcgui.Dialog().yesno("[COLOR=red]Autocleaner[/COLOR]", 'The images folder is [COLOR red]' + str(total_sizetext2) + ' MB [/COLOR]' + '\n' + 'The folder can be cleaned up without issues to save space...' + '\n' + 'Do you want to clean it now?', yeslabel='Yes',nolabel='No') 57 | if choice2 == 1: 58 | maintenance.deleteThumbnails() 59 | 60 | total_sizetext = "%.0f" % (total_size/1024000.0) 61 | total_sizetext2 = "%.0f" % (total_size2/1024000.0) 62 | 63 | if notify_mode == 'true': xbmc.executebuiltin('Notification(%s, %s, %s, %s)' % ('Maintenance Status', 'Packages: '+ str(total_sizetext) + ' MB' ' - Images: ' + str(total_sizetext2) + ' MB' , '5000', iconpath)) 64 | time.sleep(3) 65 | if auto_clean == 'true': maintenance.clearCache() 66 | 67 | maintenance.logMaintenance("Service started") 68 | 69 | class Monitor(xbmc.Monitor): 70 | 71 | def __init__(self): 72 | xbmc.Monitor.__init__(self) 73 | maintenance.logMaintenance("Monitor init") 74 | maintenance.determineNextMaintenance() 75 | 76 | def onSettingsChanged(self): 77 | maintenance.logMaintenance("onSettingsChanged") 78 | maintenance.determineNextMaintenance() 79 | 80 | if __name__ == '__main__': 81 | 82 | monitor = Monitor() 83 | 84 | while not monitor.abortRequested(): 85 | # Sleep/wait for abort for 10 seconds 86 | if monitor.waitForAbort(10): 87 | # Abort was requested while waiting. We should exit 88 | break 89 | maintenance.logMaintenance("monitor loop") 90 | if not xbmc.Player().isPlayingVideo(): 91 | nextMaintenance = maintenance.getNextMaintenance() 92 | maintenance.logMaintenance("time.time() = %s, nextMaintenance = %s" % (str(time.time()), str(nextMaintenance))) 93 | if nextMaintenance > 0 and time.time() >= nextMaintenance: 94 | xbmc.log("ezmaintenanceplus: AutoClean started", level=loglevel) 95 | maintenance.clearCache() 96 | xbmc.log("ezmaintenanceplus: AutoClean done", level=loglevel) 97 | maintenance.determineNextMaintenance() 98 | #xbmc.executebuiltin('Notification(%s, %s, %s, %s)' % ('Maintenance' , 'Clean Completed' , '3000', iconpath)) 99 | 100 | del monitor 101 | 102 | --------------------------------------------------------------------------------