├── .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 |
--------------------------------------------------------------------------------