diff --git a/Contents/Code/Docs/webtools-README_DEVS.odt b/Contents/Code/Docs/webtools-README_DEVS.odt index 7244cac..6845f46 100644 Binary files a/Contents/Code/Docs/webtools-README_DEVS.odt and b/Contents/Code/Docs/webtools-README_DEVS.odt differ diff --git a/Contents/Code/__init__.py b/Contents/Code/__init__.py index eb4b69d..5651733 100644 --- a/Contents/Code/__init__.py +++ b/Contents/Code/__init__.py @@ -15,7 +15,7 @@ NAME = 'WebTools' ICON = 'WebTools.png' -VERSION = '2.2' +VERSION = '2.3 DEV' AUTHTOKEN = '' SECRETKEY = '' DEBUGMODE = False @@ -28,8 +28,9 @@ from random import randint import uuid #Used for secrectKey - import datetime +import time + #********** Initialize ********* def Start(): @@ -41,8 +42,8 @@ def Start(): DEBUGMODE = os.path.isfile(debugFile) if DEBUGMODE: VERSION = VERSION + ' ****** WARNING Debug mode on *********' - print("******** Started %s on %s **********" %(NAME + ' V' + VERSION, Platform.OS)) - Log.Debug("******* Started %s on %s ***********" %(NAME + ' V' + VERSION, Platform.OS)) + print("******** Started %s on %s at %s **********" %(NAME + ' V' + VERSION, Platform.OS, time.strftime("%Y-%m-%d %H:%M"))) + Log.Debug("******* Started %s on %s at %s ***********" %(NAME + ' V' + VERSION, Platform.OS, time.strftime("%Y-%m-%d %H:%M"))) HTTP.CacheTime = 0 DirectoryObject.thumb = R(ICON) ObjectContainer.title1 = NAME + ' V' + VERSION diff --git a/Contents/Code/findMedia.py b/Contents/Code/findMedia.py index 98f9874..b6b9011 100644 --- a/Contents/Code/findMedia.py +++ b/Contents/Code/findMedia.py @@ -11,7 +11,7 @@ import urllib import unicodedata import json -import time +import time, sys # Consts used here AmountOfMediasInDatabase = 0 # Int of amount of medias in a database section @@ -152,10 +152,10 @@ def setSetting(self, req): req.clear() req.set_status(200) except Exception, e: - Log.Debug('Fatal error in setSetting: ' + str(e)) + Log.Debug('Fatal error in setSetting: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) - req.finish("Unknown error happened in findMedia-setSetting: " + str(e)) + req.finish("Unknown error happened in findMedia-setSetting: " + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Reset settings to default @@ -272,7 +272,7 @@ def scanMedias(sectionNumber, sectionLocations, sectionType, req): except ValueError: Log.Info('Aborted in ScanMedias') except Exception, e: - Log.Critical('Exception happend in scanMedias: ' + str(e)) + Log.Critical('Exception happend in scanMedias: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) statusMsg = 'Idle' # Scan the file system @@ -320,7 +320,7 @@ def getFiles(filePath): runningState = 99 Log.Info('Aborted in getFiles') except Exception, e: - Log.Critical('Exception happend in getFiles: ' + str(e)) + Log.Critical('Exception happend in getFiles: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) runningState = 99 def scanShowDB(sectionNumber=0): @@ -397,7 +397,7 @@ def scanShowDB(sectionNumber=0): runningState = 99 Log.Info('Aborted in ScanShowDB') except Exception, e: - Log.Debug('Fatal error in scanShowDB: ' + str(e)) + Log.Debug('Fatal error in scanShowDB: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) runningState = 99 # End scanShowDB @@ -440,7 +440,7 @@ def scanMovieDb(sectionNumber=0): break return except Exception, e: - Log.Debug('Fatal error in scanMovieDb: ' + str(e)) + Log.Debug('Fatal error in scanMovieDb: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) runningState = 99 # End scanMovieDb @@ -472,7 +472,7 @@ def scanMovieDb(sectionNumber=0): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Scanning already in progress') except Exception, ex: - Log.Debug('Fatal error happened in scanSection: ' + str(ex)) + Log.Debug('Fatal error happened in scanSection: ' + str(ex) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') diff --git a/Contents/Code/git.py b/Contents/Code/git.py index d351731..e3aec82 100644 --- a/Contents/Code/git.py +++ b/Contents/Code/git.py @@ -9,22 +9,36 @@ import datetime # Used for a timestamp in the dict import json -import io, os, shutil +import io, os, shutil, sys import plistlib import pms import tempfile class git(object): - # Defaults used by the rest of the class + init_already = False # Make sure part of init only run once + + # Init of the class def __init__(self): - Log.Debug('******* Starting git *******') self.url = '' self.PLUGIN_DIR = Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name) self.UAS_URL = 'https://github.com/ukdtom/UAS2Res' self.IGNORE_BUNDLE = ['WebTools.bundle', 'SiteConfigurations.bundle', 'Services.bundle'] self.OFFICIAL_APP_STORE = 'https://nine.plugins.plexapp.com' - Log.Debug("Plugin directory is: %s" %(self.PLUGIN_DIR)) + # Only init this part once during the lifetime of this + if not git.init_already: + git.init_already = True + Log.Debug('******* Starting git *******') + Log.Debug("Plugin directory is: %s" %(self.PLUGIN_DIR)) + # See a few times, that the json file was missing, so here we check, and if not then force a download + try: + jsonFileName = Core.storage.join_path(self.PLUGIN_DIR, NAME + '.bundle', 'http', 'uas', 'Resources', 'plugin_details.json') + if not os.path.isfile(jsonFileName): + Log.Critical('UAS dir was missing the json, so doing a forced download here') + self.updateUASCache(None, cliForce = True) + except Exception, e: + Log.Critical('Exception happend when trying to force download from UASRes: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) + ''' Grap the tornado req, and process it for GET request''' def reqprocess(self, req): function = req.get_argument('function', 'missing') @@ -143,7 +157,7 @@ def removeEmptyFolders(path, removeRoot=True): Core.storage.save(path, data) except Exception, e: bError = True - Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e)) + Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) else: # We got a directory here Log.Debug(filename.split('/')[-2]) @@ -155,7 +169,7 @@ def removeEmptyFolders(path, removeRoot=True): Core.storage.ensure_dirs(path) except Exception, e: bError = True - Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e)) + Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Now we need to nuke files that should no longer be there! for root, dirs, files in os.walk(bundleName): for fname in files: @@ -174,7 +188,7 @@ def removeEmptyFolders(path, removeRoot=True): except Exception, e: Log.Critical('***************************************************************') Log.Critical('Error when updating WebTools') - Log.Critical('The error was: ' + str(e)) + Log.Critical('The error was: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) Log.Critical('***************************************************************') Log.Critical('DARN....When we tried to upgrade WT, we had an error :-(') Log.Critical('Only option now might be to do a manual install, like you did the first time') @@ -212,7 +226,7 @@ def getUpdateList(self, req): req.clear() req.set_status(204) except Exception, e: - Log.Debug('Fatal error happened in getUpdateList: ' + str(e)) + Log.Critical('Fatal error happened in getUpdateList: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -238,7 +252,7 @@ def getUASCacheList(): results[title] = git return results except Exception, e: - Log.Debug('Exception in Migrate/getUASCacheList : ' + str(e)) + Log.Critical('Exception in Migrate/getUASCacheList : ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) return '' # Grap indentifier from plist file and timestamp @@ -343,11 +357,11 @@ def getIdentifier(pluginDir): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(migratedBundles)) except Exception, e: - Log.Critical('Fatal error happened in migrate: ' + str(e)) + Log.Critical('Fatal error happened in migrate: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in migrate: ' + str(e)) + req.finish('Fatal error happened in migrate: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) return req ''' This will return a list of UAS bundle types from the UAS Cache ''' @@ -359,7 +373,7 @@ def uasTypes(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(Dict['uasTypes'])) except Exception, e: - Log.Critical('Exception in uasTypes: ' + str(e)) + Log.Critical('Exception in uasTypes: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -367,9 +381,12 @@ def uasTypes(self, req): return req ''' This will update the UAS Cache directory from GitHub ''' - def updateUASCache(self, req): + def updateUASCache(self, req, cliForce= False): Log.Debug('Starting to update the UAS Cache') - debugForce = ('false' != req.get_argument('debugForce', 'false')) + if not cliForce: + Force = ('false' != req.get_argument('Force', 'false')) + else: + Force = True # Main call try: # Start by getting the time stamp for the last update @@ -383,7 +400,7 @@ def updateUASCache(self, req): # Now get the last update time from the UAS repository on GitHub masterUpdate = datetime.datetime.strptime(self.getLastUpdateTime(req, True, self.UAS_URL), '%Y-%m-%d %H:%M:%S') # Do we need to update the cache, and add 2 min. tolerance here? - if ((masterUpdate - lastUpdateUAS) > datetime.timedelta(seconds = 120) or debugForce): + if ((masterUpdate - lastUpdateUAS) > datetime.timedelta(seconds = 120) or Force): # We need to update UAS Cache # Target Directory targetDir = Core.storage.join_path(self.PLUGIN_DIR, NAME + '.bundle', 'http', 'uas') @@ -391,7 +408,7 @@ def updateUASCache(self, req): try: Core.storage.ensure_dirs(targetDir) except Exception, e: - errMsg = str(e) + errMsg = str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno) if 'Errno 13' in errMsg: errMsg = errMsg + '\n\nLooks like permissions are not correct, cuz we where denied access\n' errMsg = errMsg + 'to create a needed directory.\n\n' @@ -399,22 +416,26 @@ def updateUASCache(self, req): errMsg = errMsg + 'sudo chown plex:plex ./WebTools.bundle -R\n' errMsg = errMsg + 'And if on Synology, the command is:\n' errMsg = errMsg + 'sudo chown plex:users ./WebTools.bundle -R\n' - Log.Critical('Exception in updateUASCache ' + str(e)) - req.clear() - req.set_status(500) - req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Exception in updateUASCache: ' + errMsg) - return req + Log.Critical('Exception in updateUASCache ' + errMsg) + if not cliForce: + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Exception in updateUASCache: ' + errMsg) + return req + else: + return # Grap file from Github try: zipfile = Archive.ZipFromURL(self.UAS_URL+ '/archive/master.zip') except Exception, e: - Log.Critical('Could not download UAS Repo from GitHub') - req.clear() - req.set_status(500) - req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Exception in updateUASCache while downloading UAS repo from Github: ' + str(e)) - return req + Log.Critical('Could not download UAS Repo from GitHub' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) + if not cliForce: + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Exception in updateUASCache while downloading UAS repo from Github: ' + str(e)+ ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) + return req for filename in zipfile: # Walk contents of the zip, and extract as needed data = zipfile[filename] @@ -426,7 +447,7 @@ def updateUASCache(self, req): Core.storage.save(path, data) except Exception, e: bError = True - Log.Critical("Unexpected Error " + str(e)) + Log.Critical("Unexpected Error " + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) else: # We got a directory here Log.Debug(filename.split('/')[-2]) @@ -438,7 +459,7 @@ def updateUASCache(self, req): Core.storage.ensure_dirs(path) except Exception, e: bError = True - Log.Critical("Unexpected Error " + str(e)) + Log.Critical("Unexpected Error " + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Update the AllBundleInfo as well pms.updateAllBundleInfoFromUAS() pms.updateUASTypesCounters() @@ -446,17 +467,19 @@ def updateUASCache(self, req): Log.Debug('UAS Cache already up to date') # Set timestamp in the Dict Dict['UAS'] = datetime.datetime.now() - req.clear() - req.set_status(200) - req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('UASCache is up to date') + if not cliForce: + req.clear() + req.set_status(200) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('UASCache is up to date') except Exception, e: - Log.Critical('Exception in updateUASCache ' + str(e)) - req.clear() - req.set_status(500) - req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Exception in updateUASCache ' + str(e)) - return req + Log.Critical('Exception in updateUASCache ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) + if not cliForce: + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Exception in updateUASCache ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) + return req ''' list will return a list of all installed gits from GitHub''' def list(self, req): @@ -581,7 +604,7 @@ def removeEmptyFolders(path, removeRoot=True): # Grap file from Github zipfile = Archive.ZipFromURL(zipPath) except Exception, e: - Log.Critical('Exception in downloadBundle2tmp while downloading from GitHub: ' + str(e)) + Log.Critical('Exception in downloadBundle2tmp while downloading from GitHub: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) return False # Create base directory Core.storage.ensure_dirs(Core.storage.join_path(self.PLUGIN_DIR, bundleName)) @@ -610,7 +633,7 @@ def removeEmptyFolders(path, removeRoot=True): Log.Debug('Install is an upgrade') break except Exception, e: - Log.Critical('Exception in downloadBundle2tmp while walking the downloaded file to find the plist: ' + str(e)) + Log.Critical('Exception in downloadBundle2tmp while walking the downloaded file to find the plist: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) return False if bUpgrade: # Since this is an upgrade, we need to check, if the dev wants us to delete the Cache directory @@ -661,7 +684,7 @@ def removeEmptyFolders(path, removeRoot=True): Core.storage.save(path, data) except Exception, e: bError = True - Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e)) + Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) else: if cutStr not in filename: continue @@ -676,7 +699,7 @@ def removeEmptyFolders(path, removeRoot=True): Core.storage.ensure_dirs(path) except Exception, e: bError = True - Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e)) + Log.Critical('Exception happend in downloadBundle2tmp: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) if not bError and bUpgrade: # Copy files that should be kept between upgrades ("keepFiles") @@ -724,7 +747,7 @@ def removeEmptyFolders(path, removeRoot=True): shutil.move(extractDir, bundleName) except Exception, e: bError = True - Log.Critical('Unable to update plugin: ' + str(e)) + Log.Critical('Unable to update plugin: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Delete temporary directory try: @@ -753,7 +776,7 @@ def removeEmptyFolders(path, removeRoot=True): pass return True except Exception, e: - Log.Critical('Exception in downloadBundle2tmp: ' + str(e)) + Log.Critical('Exception in downloadBundle2tmp: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) return False # Starting install main @@ -824,11 +847,11 @@ def getLastUpdateTime(self, req, UAS=False, url=''): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(str(response)) except Exception, e: - Log.Critical('Fatal error happened in getLastUpdateTime for :' + url + ' was: ' + str(e)) + Log.Critical('Fatal error happened in getLastUpdateTime for :' + url + ' was: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getLastUpdateTime for :' + url + ' was: ' + str(e)) + req.finish('Fatal error happened in getLastUpdateTime for :' + url + ' was: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' Get list of avail bundles in the UAS ''' def getListofBundles(self, req): diff --git a/Contents/Code/logs.py b/Contents/Code/logs.py index 820966c..6ebbc4b 100644 --- a/Contents/Code/logs.py +++ b/Contents/Code/logs.py @@ -31,11 +31,11 @@ def __init__(self): if not os.direxists(self.LOGDIR): self.LOGDIR = os.path.join(Core.app_support_path, 'Logs') except Exception, e: - Log.Debug('Fatal error happened in Logs list: ' + str(e)) + Log.Debug('Fatal error happened in Logs list: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs list: ' + str(e)) + req.finish('Fatal error happened in Logs list: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) Log.Debug('Log Root dir is: ' + self.LOGDIR) ''' Grap the tornado req for a Get, and process it ''' @@ -84,11 +84,11 @@ def entry(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Entry logged') except Exception, e: - Log.Debug('Fatal error happened in Logs entry: ' + str(e)) + Log.Debug('Fatal error happened in Logs entry: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs entry: ' + str(e)) + req.finish('Fatal error happened in Logs entry: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' This metode will return a list of logfiles. accepts a filter parameter ''' def list(self, req): @@ -113,11 +113,11 @@ def list(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(sorted(retFiles))) except Exception, e: - Log.Debug('Fatal error happened in Logs list: ' + str(e)) + Log.Debug('Fatal error happened in Logs list: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs list: ' + str(e)) + req.finish('Fatal error happened in Logs list: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' This will return contents of the logfile as an array. Req. a parameter named fileName ''' def show(self, req): @@ -146,11 +146,11 @@ def show(self, req): req.finish(json.dumps(retFile)) return req except Exception, e: - Log.Debug('Fatal error happened in Logs show: ' + str(e)) + Log.Debug('Fatal error happened in Logs show: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs show: ' + str(e)) + req.finish('Fatal error happened in Logs show: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' This will download a zipfile with the complete log directory. if parameter fileName is specified, only that file will be downloaded, and not zipped''' def download(self, req): @@ -186,11 +186,11 @@ def download(self, req): os.remove(zipFileName) return req except Exception, e: - Log.Debug('Fatal error happened in Logs download: ' + str(e)) + Log.Debug('Fatal error happened in Logs download: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs download: ' + str(e)) + req.finish('Fatal error happened in Logs download: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) else: try: if 'com.plexapp' in fileName: @@ -211,15 +211,15 @@ def download(self, req): req.finish() return req except Exception, e: - Log.Debug('Fatal error happened in Logs download: ' + str(e)) + Log.Debug('Fatal error happened in Logs download: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs download: ' + str(e)) + req.finish('Fatal error happened in Logs download: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) except Exception, e: - Log.Debug('Fatal error happened in Logs download: ' + str(e)) + Log.Debug('Fatal error happened in Logs download: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in Logs download: ' + str(e)) + req.finish('Fatal error happened in Logs download: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) diff --git a/Contents/Code/modules/plex2csv_moviefields.py b/Contents/Code/modules/plex2csv_moviefields.py index c78551a..57a0c5d 100644 --- a/Contents/Code/modules/plex2csv_moviefields.py +++ b/Contents/Code/modules/plex2csv_moviefields.py @@ -111,31 +111,113 @@ "Subtitle Stream Selected" : {"field" : "Media/Part/Stream[@streamType=3]/@selected", "ReqLevel" : 2, "id" : 97} } +fieldsbyID = { + 1 : "Media ID", + 2 : "Title", + 3 : "Sort title", + 4 : "Studio", + 5 : "Content Rating", + 6 : "Year", + 7 : "Rating", + 8 : "Summary", + 9 : "Genres", + 10 : "View Count", + 11 : "Last Viewed at", + 12 : "Tagline", + 13 : "Release Date", + 14 : "Writers", + 15 : "Country", + 16 : "Duration", + 17 : "Directors", + 18 : "Roles", + 19 : "IMDB Id", + 20 : "Labels", + 21 : "Locked Fields", + 22 : "Extras", + 23 : "Collections", + 24 : "Original Title", + 25 : "Added", + 26 : "Updated", + 27 : "Audio Languages", + 28 : "Audio Title", + 29 : "Subtitle Languages", + 30 : "Subtitle Title", + 31 : "Subtitle Codec", + 32 : "Accessible", + 33 : "Exists", + 34 : "Video Resolution", + 35 : "Bitrate", + 36 : "Width", + 37 : "Height", + 38 : "Aspect Ratio", + 39 : "Audio Channels", + 40 : "Audio Codec", + 41 : "Video Codec", + 42 : "Container", + 43 : "Video FrameRate", + 44 : "Part File", + 45 : "Part Size", + 46 : "Part Indexed", + 47 : "Part Duration", + 48 : "Part Container", + 49 : "Part Optimized for Streaming", + 50 : "Video Stream Title", + 51 : "Video Stream Default", + 52 : "Video Stream Index", + 53 : "Video Stream Pixel Format", + 54 : "Video Stream Profile", + 55 : "Video Stream Ref Frames", + 56 : "Video Stream Scan Type", + 57 : "Video Stream Stream Identifier", + 58 : "Video Stream Width", + 59 : "Video Stream Pixel Aspect Ratio", + 60 : "Video Stream Height", + 61 : "Video Stream Has Scaling Matrix", + 62 : "Video Stream Frame Rate Mode", + 63 : "Video Stream Frame Rate", + 64 : "Video Stream Codec", + 65 : "Video Stream Codec ID", + 66 : "Video Stream Chroma Sub Sampling", + 67 : "Video Stream Cabac", + 68 : "Video Stream Anamorphic", + 69 : "Video Stream Language Code", + 70 : "Video Stream Language", + 71 : "Video Stream Bitrate", + 72 : "Video Stream Bit Depth", + 73 : "Video Stream Duration", + 74 : "Video Stream Level", + 75 : "Audio Stream Selected", + 76 : "Audio Stream Default", + 77 : "Audio Stream Codec", + 78 : "Audio Stream Index", + 79 : "Audio Stream Channels", + 80 : "Audio Stream Bitrate", + 81 : "Audio Stream Language", + 82 : "Audio Stream Language Code", + 83 : "Audio Stream Audio Channel Layout", + 84 : "Audio Stream Bit Depth", + 85 : "Audio Stream Bitrate Mode", + 86 : "Audio Stream Codec ID", + 87 : "Audio Stream Duration", + 88 : "Audio Stream Profile", + 89 : "Audio Stream Sampling Rate", + 90 : "Subtitle Stream Codec", + 91 : "Subtitle Stream Index", + 92 : "Subtitle Stream Language", + 93 : "Subtitle Stream Language Code", + 94 : "Subtitle Stream Codec ID", + 95 : "Subtitle Stream Format", + 96 : "Subtitle Stream Title", + 97 : "Subtitle Stream Selected" +} + levels = { - "Level_1" : ['Media ID', 'Title', 'Sort title', 'Studio', 'Content Rating', - 'Year', 'Rating', 'Summary', 'Genres'], - "Level_2" : ['View Count', 'Last Viewed at', 'Tagline', 'Release Date', - 'Writers', 'Country', 'Duration', 'Directors', 'Roles', 'IMDB Id'], - "Level_3" : ['Labels', 'Locked Fields', 'Extras', 'Collections', 'Original Title', - 'Added', 'Updated', 'Audio Languages', 'Audio Title', 'Subtitle Languages', - 'Subtitle Title', 'Subtitle Codec', 'Accessible', 'Exists'], - "Level_4" : ['Video Resolution', 'Bitrate', 'Width', 'Height', 'Aspect Ratio', - 'Audio Channels', 'Audio Codec', 'Video Codec', 'Container', 'Video FrameRate'], - "Level_5" : ['Part File', 'Part Size', 'Part Indexed', 'Part Duration', 'Part Container', - 'Part Optimized for Streaming'], - "Level_6" : ['Video Stream Title', 'Video Stream Default', 'Video Stream Index','Video Stream Pixel Format', - 'Video Stream Profile', 'Video Stream Ref Frames', 'Video Stream Scan Type', - 'Video Stream Stream Identifier', 'Video Stream Width', 'Video Stream Pixel Aspect Ratio', - 'Video Stream Height', 'Video Stream Has Scaling Matrix', 'Video Stream Frame Rate Mode', - 'Video Stream Frame Rate', 'Video Stream Codec', 'Video Stream Codec ID', - 'Video Stream Chroma Sub Sampling', 'Video Stream Cabac', 'Video Stream Anamorphic', - 'Video Stream Language Code', 'Video Stream Language', 'Video Stream Bitrate', - 'Video Stream Bit Depth', 'Video Stream Duration', 'Video Stream Level', - 'Audio Stream Selected', 'Audio Stream Default', 'Audio Stream Codec', - 'Audio Stream Index', 'Audio Stream Channels', 'Audio Stream Bitrate', 'Audio Stream Language', - 'Audio Stream Language Code', 'Audio Stream Audio Channel Layout', 'Audio Stream Bit Depth', - 'Audio Stream Bitrate Mode', 'Audio Stream Codec ID', 'Audio Stream Duration', - 'Audio Stream Profile', 'Audio Stream Sampling Rate', 'Subtitle Stream Codec', - 'Subtitle Stream Index', 'Subtitle Stream Language', 'Subtitle Stream Language Code', - 'Subtitle Stream Codec ID', 'Subtitle Stream Format', 'Subtitle Stream Title', 'Subtitle Stream Selected'] + "Level_1" : [1, 2, 3, 4, 5, 6, 7, 8, 9], + "Level_2" : [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], + "Level_3" : [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33], + "Level_4" : [34, 35, 36, 37, 38, 39, 40, 41, 42, 43], + "Level_5" : [44, 45, 46, 47, 48, 49], + "Level_6" : [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, + 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, 96, 97] } diff --git a/Contents/Code/plex2csv.py b/Contents/Code/plex2csv.py index 486cdc1..4ef37d0 100644 --- a/Contents/Code/plex2csv.py +++ b/Contents/Code/plex2csv.py @@ -10,6 +10,7 @@ ###################################################################################################################### import plex2csv_moviefields +import json class plex2csv(object): # Defaults used by the rest of the class @@ -24,22 +25,79 @@ def reqprocess(self, req): req.set_status(412) req.finish("Missing function parameter") elif function == 'getFields': - # Call scanSection + # Call getFields return self.getFields(req) + elif function == 'getFieldListbyIdx': + # Call getFieldListbyIdx + return self.getFieldListbyIdx(req) + elif function == 'getDefaultLevels': + # Call getDefaultLevels + return self.getDefaultLevels(req) else: req.clear() req.set_status(412) req.finish("Unknown function call") + ''' Returns a jason with the build-in levels + Param needed is type=[movie,show,audio,picture] + ''' + def getDefaultLevels(self, req): + def getMovieDefLevels(req): + myResult = [] + fields = json.dumps(plex2csv_moviefields.movieDefaultLevels, sort_keys=True) + print 'Ged1', fields + print 'Ged2' + for key, value in fields: + print 'Ged2', key + myResult.append(key) + req.clear() + req.set_status(200) + req.finish(json.dumps(myResult)) + + # Main code + type = req.get_argument('type', 'missing') + if type == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing type parameter") + if type=='movie': + getMovieDefLevels(req) + + ''' Returns an array of possible fields for a section type. + Param needed is type=[movie,show,audio,picture] + ''' + def getFieldListbyIdx(self, req): + def getMovieListbyIdx(req): + req.clear() + req.set_status(200) + req.finish(json.dumps(plex2csv_moviefields.fieldsbyID)) + + # Main code + type = req.get_argument('type', 'missing') + if type == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing type parameter") + if type=='movie': + getMovieListbyIdx(req) + ''' This will return a list of fields avail Param needed is type=[movie,show,audio,picture] ''' def getFields(self, req): - print 'Ged her' + def getFullMovieFieldsList(req): + req.clear() + req.set_status(200) + req.finish(json.dumps(plex2csv_moviefields.fields)) + + # Main code type = req.get_argument('type', 'missing') if type == 'missing': req.clear() req.set_status(412) req.finish("Missing type parameter") - return + if type=='movie': + getFullMovieFieldsList(req) + + diff --git a/Contents/Code/plextvhelper.py b/Contents/Code/plextvhelper.py index de846fa..1ef6d44 100644 --- a/Contents/Code/plextvhelper.py +++ b/Contents/Code/plextvhelper.py @@ -6,6 +6,7 @@ # NAME variable must be defined in the calling unit, and is the name of the application # ###################################################################################################################### +import sys class plexTV(object): # Defaults used by the rest of the class @@ -27,26 +28,6 @@ def __init__(self): # Login to Plex.tv def login(self, user, pwd): Log.Info('Start to auth towards plex.tv') - - ''' - user = req.get_argument('user', '') - if user == '': - Log.Error('Missing username') - req.clear() - req.set_status(412) - req.finish("Missing username") - return req - pwd = req.get_argument('pwd', '') - if pwd == '': - Log.Error('Missing password') - req.clear() - req.set_status(412) - req.finish("Missing password") - return req - ''' - - - # Got what we needed, so let's logon authString = String.Base64Encode('%s:%s' % (user, pwd)) self.myHeader['Authorization'] = 'Basic ' + authString try: @@ -54,11 +35,8 @@ def login(self, user, pwd): Log.Info('Authenticated towards plex.tv with success') return token except Ex.HTTPError, e: - Log.Critical('Login error: ' + str(e)) - req.clear() - req.set_status(e.code) - req.finish(e) - return (req, '') + Log.Critical('Login error: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) + return None ''' Is user the owner of the server? user identified by token diff --git a/Contents/Code/pms.py b/Contents/Code/pms.py index 05cb92a..38fc759 100644 --- a/Contents/Code/pms.py +++ b/Contents/Code/pms.py @@ -8,10 +8,9 @@ ###################################################################################################################### import shutil, os import time, json -import io +import io, sys from xml.etree import ElementTree - # Undate uasTypesCounters def updateUASTypesCounters(): try: @@ -38,46 +37,38 @@ def updateUASTypesCounters(): counter[bundleType] = {'installed': 1, 'total' : 1} Dict['uasTypes'] = counter Dict.Save() - except Exception, e: - print 'Fatal error happened in updateUASTypesCounters: ' + str(e) - Log.Debug('Fatal error happened in updateUASTypesCounters: ' + str(e)) + except Exception, e: + Log.Debug('Fatal error happened in updateUASTypesCounters: ' + str(e) + ' on line {}'.format(sys.exc_info()[-1].tb_lineno)) #TODO fix updateAllBundleInfo # updateAllBundleInfo def updateAllBundleInfoFromUAS(): - def updateInstallDict(): - ''' - # Debugging stuff - print 'Ged debugging stuff' - Dict['PMS-AllBundleInfo'].pop('https://github.com/ukdtom/plex2csv.bundle', None) - Dict['installed'].clear() - Dict.Save() - #Debug end - ''' - - - + def updateInstallDict(): # Start by creating a fast lookup cache for all uas bundles uasBundles = {} bundles = Dict['PMS-AllBundleInfo'] for bundle in bundles: uasBundles[bundles[bundle]['identifier']] = bundle # Now walk the installed ones - for installedBundle in Dict['installed']: - if not installedBundle.startswith('https://'): - Log.Info('Checking unknown bundle: ' + installedBundle + ' to see if it is part of UAS now') - if installedBundle in uasBundles: - # Get the installed date of the bundle formerly known as unknown :-) - installedBranch = Dict['installed'][installedBundle]['branch'] - installedDate = Dict['installed'][installedBundle]['date'] - # Add updated stuff to the dicts - Dict['PMS-AllBundleInfo'][uasBundles[installedBundle]]['branch'] = installedBranch - Dict['PMS-AllBundleInfo'][uasBundles[installedBundle]]['date'] = installedDate - Dict['installed'][uasBundles[installedBundle]] = Dict['PMS-AllBundleInfo'][uasBundles[installedBundle]] - # Remove old stuff from the Ditcs - Dict['PMS-AllBundleInfo'].pop(installedBundle, None) - Dict['installed'].pop(installedBundle, None) - Dict.Save() + try: + installed = Dict['installed'].copy() + for installedBundle in installed: + if not installedBundle.startswith('https://'): + Log.Info('Checking unknown bundle: ' + installedBundle + ' to see if it is part of UAS now') + if installedBundle in uasBundles: + # Get the installed date of the bundle formerly known as unknown :-) + installedBranch = Dict['installed'][installedBundle]['branch'] + installedDate = Dict['installed'][installedBundle]['date'] + # Add updated stuff to the dicts + Dict['PMS-AllBundleInfo'][uasBundles[installedBundle]]['branch'] = installedBranch + Dict['PMS-AllBundleInfo'][uasBundles[installedBundle]]['date'] = installedDate + Dict['installed'][uasBundles[installedBundle]] = Dict['PMS-AllBundleInfo'][uasBundles[installedBundle]] + # Remove old stuff from the Dict + Dict['PMS-AllBundleInfo'].pop(installedBundle, None) + Dict['installed'].pop(installedBundle, None) + Dict.Save() + except Exception, e: + Log.Critical('Critical error in updateInstallDict while walking the gits: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) return try: @@ -102,11 +93,6 @@ def updateInstallDict(): jsonPMSAllBundleInfo = Dict['PMS-AllBundleInfo'][key] if 'branch' in jsonPMSAllBundleInfo: installBranch = Dict['PMS-AllBundleInfo'][key]['branch'] - - - - Log.Debug('Ged1: ' + installBranch) - if 'date' in jsonPMSAllBundleInfo: installDate = Dict['PMS-AllBundleInfo'][key]['date'] del git['repo'] @@ -115,14 +101,14 @@ def updateInstallDict(): Dict['PMS-AllBundleInfo'][key]['branch'] = installBranch Dict['PMS-AllBundleInfo'][key]['date'] = installDate except Exception, e: - Log.Critical('Critical error in updateInstallDict while walking the gits: ' + str(e)) + Log.Critical('Critical error in updateAllBundleInfoFromUAS1 while walking the gits: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) Dict.Save() updateUASTypesCounters() updateInstallDict() else: Log.Debug('UAS was sadly not present') except Exception, e: - Log.Critical('Fatal error happened in updateAllBundleInfoFromUAS: ' + str(e)) + Log.Critical('Fatal error happened in updateAllBundleInfoFromUAS: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) class pms(object): # Defaults used by the rest of the class @@ -157,6 +143,8 @@ def reqprocess(self, req): return self.getSectionLetterList(req) elif function == 'getSectionByLetter': return self.getSectionByLetter(req) + elif function == 'search': + return self.search(req) else: req.clear() req.set_status(412) @@ -204,6 +192,48 @@ def reqprocessPost(self, req): req.set_status(412) req.finish("Unknown function call") + ''' Search for a title ''' + def search(self, req): + Log.Info('Search called') + try: + title = req.get_argument('title', '_WT_missing_') + if title == '_WT_missing_': + req.clear() + req.set_status(412) + req.finish("Missing title parameter") + else: + url = 'http://127.0.0.1:32400/search?query=' + String.Quote(title) + result = {} + # Fetch search result from PMS + foundMedias = XML.ElementFromURL(url) + # Grap all movies from the result + for media in foundMedias.xpath('//Video'): + value = {} + value['title'] = media.get('title') + value['type'] = media.get('type') + value['section'] = media.get('librarySectionID') + key = media.get('ratingKey') + result[key] = value + # Grap results for TV-Shows + for media in foundMedias.xpath('//Directory'): + value = {} + value['title'] = media.get('title') + value['type'] = media.get('type') + value['section'] = media.get('librarySectionID') + key = media.get('ratingKey') + result[key] = value + Log.Info('Search returned: %s' %(result)) + req.clear() + req.set_status(200) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish(json.dumps(result)) + except Exception, e: + Log.Debug('Fatal error happened in search: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Fatal error happened in search: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) + ''' Delete from an XML file ''' def DelFromXML(self, fileName, attribute, value): Log.Debug('Need to delete element with an attribute named "%s" with a value of "%s" from file named "%s"' %(attribute, value, fileName)) @@ -214,16 +244,10 @@ def DelFromXML(self, fileName, attribute, value): for Subtitles in root.findall("Language[Subtitle]"): for node in Subtitles.findall("Subtitle"): myValue = node.attrib.get(attribute) - - print 'Ged9', myValue - if myValue: if '_' in myValue: drop, myValue = myValue.split("_") if myValue == value: - - print 'Ged10', value - Subtitles.remove(node) tree.write(fileName, encoding='utf-8', xml_declaration=True) return @@ -252,11 +276,11 @@ def getParts(self, req): self.set_status(e.code) self.finish(e) except Exception, e: - Log.Debug('Fatal error happened in getParts: ' + str(e)) + Log.Debug('Fatal error happened in getParts: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getParts: ' + str(e)) + req.finish('Fatal error happened in getParts: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # uploadFile def uploadFile(self, req): @@ -283,11 +307,11 @@ def uploadFile(self, req): req.set_status(200) req.finish("Upload ok") except Exception, e: - Log.Debug('Fatal error happened in uploadFile: ' + str(e)) + Log.Debug('Fatal error happened in uploadFile: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in uploadFile: ' + str(e)) + req.finish('Fatal error happened in uploadFile: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # getAllBundleInfo def getAllBundleInfo(self, req): @@ -299,11 +323,11 @@ def getAllBundleInfo(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(Dict['PMS-AllBundleInfo'])) except Exception, e: - Log.Debug('Fatal error happened in getAllBundleInfo: ' + str(e)) + Log.Debug('Fatal error happened in getAllBundleInfo: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getAllBundleInfo' + str(e)) + req.finish('Fatal error happened in getAllBundleInfo' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Delete Bundle def delBundle(self, req): @@ -324,11 +348,11 @@ def removeBundle(bundleName, bundleIdentifier, url): Log.Debug('Bundle directory name digested as: %s' %(bundleInstallDir)) shutil.rmtree(bundleInstallDir) except Exception, e: - Log.Critical("Unable to remove the bundle directory: " + str(e)) + Log.Critical("Unable to remove the bundle directory: " + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened when trying to remove the bundle directory: ' + str(e)) + req.finish('Fatal error happened when trying to remove the bundle directory: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) try: shutil.rmtree(bundleDataDir) except: @@ -376,11 +400,11 @@ def removeBundle(bundleName, bundleIdentifier, url): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Fatal error happened when trying to restart the system.bundle') except Exception, e: - Log.Debug('Fatal error happened in removeBundle: ' + str(e)) + Log.Debug('Fatal error happened in removeBundle: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in removeBundle' + str(e)) + req.finish('Fatal error happened in removeBundle' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Main function try: @@ -475,11 +499,11 @@ def delSub(self, req): url = 'http://127.0.0.1:32400/library/metadata/' + key + '/refresh?force=1' HTTP.Request(url, cacheTime=0, immediate=True, method="PUT") except Exception, e: - Log.Critical('Exception while deleting an agent based sub: ' + str(e)) + Log.Critical('Exception while deleting an agent based sub: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(404) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Exception while deleting an agent based sub: ' + str(e)) + req.finish('Exception while deleting an agent based sub: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) retValues = {} retValues['FilePath']=filePath3 retValues['SymbLink']=filePath @@ -503,7 +527,7 @@ def delSub(self, req): req.finish(json.dumps(retVal)) except Exception, e: # Could not find req. subtitle - Log.Debug('Fatal error happened in delSub, when deleting %s : %s' %(filePath, str(e))) + Log.Debug('Fatal error happened in delSub, when deleting ' + filePath + ' : ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(404) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -516,11 +540,11 @@ def delSub(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Could not find req. subtitle') except Exception, e: - Log.Debug('Fatal error happened in delSub: ' + str(e)) + Log.Debug('Fatal error happened in delSub: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in delSub: ' + str(e)) + req.finish('Fatal error happened in delSub: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' TVShow ''' def TVshow(self, req): @@ -816,11 +840,11 @@ def getSectionLetterList(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(resultJson, sort_keys=True)) except Exception, e: - Log.Debug('Fatal error happened in getSectionLetterList ' + str(e)) + Log.Debug('Fatal error happened in getSectionLetterList ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getSectionLetterList: ' + str(e)) + req.finish('Fatal error happened in getSectionLetterList: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' get getSectionByLetter ''' def getSectionByLetter(self,req): @@ -873,17 +897,17 @@ def getSectionByLetter(self,req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(Section)) except Exception, e: - Log.Debug('Fatal error happened in getSectionByLetter: ' + str(e)) + Log.Debug('Fatal error happened in getSectionByLetter: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getSectionByLetter: ' + str(e)) + req.finish('Fatal error happened in getSectionByLetter: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) except Exception, e: - Log.Debug('Fatal error happened in getSectionByLetter: ' + str(e)) + Log.Debug('Fatal error happened in getSectionByLetter: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getSectionByLetter: ' + str(e)) + req.finish('Fatal error happened in getSectionByLetter: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) ''' get section ''' def getSection(self,req): diff --git a/Contents/Code/settings.py b/Contents/Code/settings.py index a30bfe2..cae94e9 100644 --- a/Contents/Code/settings.py +++ b/Contents/Code/settings.py @@ -5,7 +5,7 @@ # ###################################################################################################################### -import json +import json, sys class settings(object): @@ -80,6 +80,7 @@ def setPwd(self, req): req.finish("Old Password did not match") return req except Ex.HTTPError, e: + Log.Critical('Error in setPwd: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(e.code) req.finish(e) @@ -106,6 +107,7 @@ def putSetting(self, req): req.finish("Setting saved") return req except Ex.HTTPError, e: + Log.Critical('Error in putSetting: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(e.code) req.finish(e) @@ -133,6 +135,7 @@ def getSetting(self, req): req.finish(json.dumps('Setting not found')) return req except Ex.HTTPError, e: + Log.Critical('Error in getSetting: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(e.code) req.finish(e) @@ -155,6 +158,7 @@ def getSettings(self, req): req.finish(json.dumps(mySetting)) return req except Ex.HTTPError, e: + Log.Critical('Error in getSettings: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(e.code) req.finish(e) diff --git a/Contents/Code/webSrv.py b/Contents/Code/webSrv.py index 135fed9..f108234 100644 --- a/Contents/Code/webSrv.py +++ b/Contents/Code/webSrv.py @@ -30,7 +30,7 @@ from wt import wt -import os +import os, sys # Below used to find path of this file from inspect import getsourcefile @@ -152,54 +152,62 @@ def post(self): self.allow() Log.Info('All is good, we are authenticated') self.redirect('/') - # Let's start by checking if the server is online - if plexTV().auth2myPlex(): - token = '' - try: - # Authenticate - retVal = plexTV().isServerOwner(plexTV().login(user, pwd)) - self.clear() - if retVal == 0: - # All is good + else: + # Let's start by checking if the server is online + if plexTV().auth2myPlex(): + token = '' + try: + # Authenticate + login_token = plexTV().login(user, pwd) + if login_token == None: + Log.ERROR('Bad credentials detected, denying access') + self.clear() + self.set_status(401) + self.finish('Authentication error') + return self + retVal = plexTV().isServerOwner(login_token) + self.clear() + if retVal == 0: + # All is good + self.allow() + Log.Info('All is good, we are authenticated') + self.redirect('/') + elif retVal == 1: + # Server not found + Log.Info('Server not found on plex.tv') + self.set_status(404) + elif retVal == 2: + # Not the owner + Log.Info('USer is not the server owner') + self.set_status(403) + else: + # Unknown error + Log.Critical('Unknown error, when authenticating') + self.set_status(403) + except Ex.HTTPError, e: + Log.Critical('Exception in Login: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) + self.clear() + self.set_status(e.code) + self.finish(e) + return self + else: + Log.Info('Server is not online according to plex.tv') + # Server is offline + if Dict['password'] == '': + Log.Info('First local login, so we need to set the local password') + Dict['password'] = pwd + Dict['pwdset'] = True + Dict.Save self.allow() - Log.Info('All is good, we are authenticated') self.redirect('/') - elif retVal == 1: - # Server not found - Log.Info('Server not found on plex.tv') - self.set_status(404) - elif retVal == 2: - # Not the owner - Log.Info('USer is not the server owner') - self.set_status(403) - else: - # Unknown error - Log.Critical('Unknown error, when authenticating') - self.set_status(403) - except Ex.HTTPError, e: - Log.Critical('Exception in Login: ' + str(e)) - self.clear() - self.set_status(e.code) - self.finish(e) - return self - else: - Log.Info('Server is not online according to plex.tv') - # Server is offline - if Dict['password'] == '': - Log.Info('First local login, so we need to set the local password') - Dict['password'] = pwd - Dict['pwdset'] = True - Dict.Save - self.allow() - self.redirect('/') - elif Dict['password'] == pwd: - self.allow() - Log.Info('Local password accepted') - self.redirect('/') - elif Dict['password'] != pwd: - Log.Critical('Either local login failed, or PMS lost connection to plex.tv') - self.clear() - self.set_status(401) + elif Dict['password'] == pwd: + self.allow() + Log.Info('Local password accepted') + self.redirect('/') + elif Dict['password'] != pwd: + Log.Critical('Either local login failed, or PMS lost connection to plex.tv') + self.clear() + self.set_status(401) def allow(self): self.set_secure_cookie(NAME, Hash.MD5(Dict['SharedSecret']+Dict['password']), expires_days = None) diff --git a/Contents/Code/wt.py b/Contents/Code/wt.py index c96615b..7b90183 100644 --- a/Contents/Code/wt.py +++ b/Contents/Code/wt.py @@ -10,7 +10,7 @@ import glob import json -import shutil +import shutil, sys class wt(object): @@ -46,10 +46,15 @@ def reqprocessPost(self, req): # Reset WT to factory settings def reset(self, req): try: + Log.Info('Factory Reset called') cachePath = Core.storage.join_path(Core.app_support_path, 'Plug-in Support', 'Caches', 'com.plexapp.plugins.WebTools') dataPath = Core.storage.join_path(Core.app_support_path, 'Plug-in Support', 'Data', 'com.plexapp.plugins.WebTools') shutil.rmtree(cachePath) - shutil.rmtree(dataPath) + try: +# shutil.rmtree(dataPath) + Dict.Reset() + except: + Log.Critical('Fatal error in clearing dict during reset') # Restart system bundle HTTP.Request('http://127.0.0.1:32400/:/plugins/com.plexapp.plugins.WebTools/restart', cacheTime=0, immediate=True) req.clear() @@ -57,11 +62,11 @@ def reset(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('WebTools has been reset') except Exception, e: - Log.Debug('Fatal error happened in wt.reset: ' + str(e)) + Log.Debug('Fatal error happened in wt.reset: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in wt.reset: ' + str(e)) + req.finish('Fatal error happened in wt.reset: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) # Get a list of all css files in http/custom_themes def getCSS(self,req): @@ -74,18 +79,19 @@ def getCSS(self,req): req.set_status(204) else: for n,item in enumerate(myList): - myList[n] = item.replace(targetDir + '/','') + myList[n] = item.replace(targetDir,'') + myList[n] = myList[n][1:] Log.Debug('Returning %s' %(myList)) req.clear() req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(myList)) except Exception, e: - Log.Debug('Fatal error happened in getCSS: ' + str(e)) + Log.Debug('Fatal error happened in getCSS: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') - req.finish('Fatal error happened in getCSS: ' + str(e)) + req.finish('Fatal error happened in getCSS: ' + str(e) + 'on line {}'.format(sys.exc_info()[-1].tb_lineno)) diff --git a/http/changelog.txt b/http/changelog.txt index 5228753..f242062 100644 --- a/http/changelog.txt +++ b/http/changelog.txt @@ -1,6 +1,21 @@ -V2.2-DEV: -Internal Note: - Here we collect stuff that needs to go into the real released changelog, aka acumulated, and only since 2.1 changes here ;-) +V2.3 DEV: + Fix: + #165 LV: Fixed issue with spaces in filenames + #166 UAS: Removed highlight of All after selecting category + #172 WT: Fixed issue with Factory reset + #176 UAS: Critical error in updateInstallDict + #178 WT: Fixed issue with intruder detection + + New: + #170 WT: Added a user guide from Trumpy81. URL is /manual/WebTools-User-Manual.pdf + #171 PMS: Added Search to the backend + #175 PMS: Autodownload Repo if json is missing + + +FRONTEND: + +V2.2 Release: + BACKEND: Fix: #115 UAS: Migration fails, if plugin directory contains hidden folder @@ -22,7 +37,7 @@ FRONTEND: #131 UAS: After changin category, focus is no longer set on inputbox #139 UAS: Unknown bundles without url won't be able to be uninstalled or re-installed. #140 UAS: Migrate now always goes back to showing ALL available bundles - #### Subtitlemgmt: Added more logging for uploads. And fixed error reported by Dane22 about uploads. (2016-03-04) + ## Subtitlemgmt: Added more logging for uploads. And fixed bug reported by Dane22 about uploads. #145 Subtitlemgmt: Fixed display of undefined when deleting sidecars. #159 LogViewer: Fix for incorrect highlights when searching for "Warn" #161 UAS: Partial fix, now retains correct coloring. @@ -32,12 +47,11 @@ FRONTEND: #142 LogViewer: Search for keywords, highlighting of the same. Jump to top. #133 Subtitlemgmt: Added GUI for uploading subtitles #112 Subtitlemgmt: Allow delete/view for agent subtitles aswell. - #### Subtitlemgmt: Now uses the new Language Module + ### Subtitlemgmt: Now uses the new Language Module #93 WT: Now has support for custom themes! First out is a draft of trumpy81 that needs to be finetuned by trumpy81 #160 WT: Implemented Factory Reset functionality. Available through the Options menu. #149 Subtitlemgmt: Implemented letters instead of straight up numbers for paging. - #### WT: Removed LogFiles from top menu, redirecting users to LogViewer instead. - #### WT: Themes are now applied on loginscreen aswell. + ### WT: Removed LogFiles from top menu, redirecting users to LogViewer instead. #### diff --git a/http/credits.txt b/http/credits.txt index 16b706d..38017de 100644 --- a/http/credits.txt +++ b/http/credits.txt @@ -2,6 +2,9 @@ Main Developers: Dane22 (Python, Backend) Dagalufh (JS/HTML, Frontend) +Custom theme's by: +trumpy81 + Beta Testers: OttoKerner sa2000 trumpy81 Xandi92 diff --git a/http/custom_themes/trumpy81-Aussie.css b/http/custom_themes/trumpy81-Aussie.css index f6e19b1..e731f08 100644 --- a/http/custom_themes/trumpy81-Aussie.css +++ b/http/custom_themes/trumpy81-Aussie.css @@ -1,10 +1,4 @@ -.color-primary-0 { color: #0033CC } /* Main Primary color */ -.color-primary-1 { color: #18150F } -.color-primary-2 { color: #666563 } -.color-primary-3 { color: #975C00 } -.color-primary-4 { color: #3F2700 } - .panel-default>.panel-heading {background-color: #f6894b; color: #68452c;} .navbar {margin-bottom: 0px;} @@ -33,6 +27,7 @@ th {background-color: #c28a44; color: #68452c;} .subtitle {padding: 0px;} .btn-default:hover {background-color: #f6894b; color: #68452c;} +.btn-default:focus {background-color: #f6894b; color: #68452c;} .btn-active {background-color: #f6894b; color: #68452c;} .modal-header {background-color: #f6894b; color: #68452c;} body {background-color: #f2e1d8; padding-top:70px; padding-bottom:70px;} @@ -41,6 +36,9 @@ body {background-color: #f2e1d8; padding-top:70px; padding-bottom:70px;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #f2e1d8; color: #000000;} +input {background-color: #f2e1d8; color: #000000;} + .pagination>.active>span {background-color: #f6894b;color: #68452c; cursor: pointer;} .pagination>.active>span:hover {background-color: #68452c; color: #f6894b;cursor: pointer;} .pagination>li>span:hover {background-color: #f6894b; color: #68452c;cursor: pointer;} diff --git a/http/custom_themes/trumpy81-ItsBlue.css b/http/custom_themes/trumpy81-ItsBlue.css index 49149fd..1c3771b 100644 --- a/http/custom_themes/trumpy81-ItsBlue.css +++ b/http/custom_themes/trumpy81-ItsBlue.css @@ -1,10 +1,4 @@ -.color-primary-0 { color: #0033CC } /* Main Primary color */ -.color-primary-1 { color: #18150F } -.color-primary-2 { color: #666563 } -.color-primary-3 { color: #975C00 } -.color-primary-4 { color: #3F2700 } - .panel-default>.panel-heading {background-color: #002db3; color: #00ccff;} .navbar {margin-bottom: 0px;} @@ -33,6 +27,7 @@ th {background-color: #0033CC; color: #66CCFF;} .subtitle {padding: 0px;} .btn-default:hover {background-color: #66CCFF; color: #0033CC;} +.btn-default:focus {background-color: #FFFFFF; color: #000000;} .btn-active {background-color: #0033CC; color: #66CCFF;} .modal-header {background-color: #0033CC; color: #66CCFF;} body {background-color: #002699; padding-top:70px; padding-bottom:70px;} @@ -41,6 +36,9 @@ body {background-color: #002699; padding-top:70px; padding-bottom:70px;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #e6f7ff; color: #000000;} +input {background-color: #e6f7ff; color: #000000;} + .pagination {margin: 0px;} .pagination>li>span {height: 31px;} .pagination>.active>span {background-color: #0033CC;color: #000099; cursor: pointer;} diff --git a/http/custom_themes/trumpy81-ItsGreen.css b/http/custom_themes/trumpy81-ItsGreen.css index 266a1eb..88df122 100644 --- a/http/custom_themes/trumpy81-ItsGreen.css +++ b/http/custom_themes/trumpy81-ItsGreen.css @@ -1,10 +1,4 @@ -.color-primary-0 { color: #009933 } /* Main Primary color */ -.color-primary-1 { color: #00FF00 } -.color-primary-2 { color: #66FF66 } -.color-primary-3 { color: #006600 } -.color-primary-4 { color: #009900 } - .panel-default>.panel-heading {background-color: #66FF66; color: #006600;} .navbar {margin-bottom: 0px;} @@ -33,6 +27,7 @@ th {background-color: #006600; color: #00FF00;} .subtitle {padding: 0px;} .btn-default:hover {background-color: #006600; color: #00FF00;} +.btn-default:focus {background-color: #FFFFFF; color: #000000;} .btn-active {background-color: #006600; color: #00FF00;} .modal-header {background-color: #006600; color: #00FF00;} body {background-color: #005500; padding-top:70px; padding-bottom:70px;} @@ -41,6 +36,9 @@ body {background-color: #005500; padding-top:70px; padding-bottom:70px;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #CCFFCC; color: #000000;} +input {background-color: #CCFFCC; color: #000000;} + .pagination {margin: 0px;} .pagination>li>span {height: 31px;} .pagination>.active>span {background-color: #006600;color: #00FF00; cursor: pointer;} diff --git a/http/custom_themes/trumpy81-ItsPink.css b/http/custom_themes/trumpy81-ItsPink.css index e8e1347..c81dfd7 100644 --- a/http/custom_themes/trumpy81-ItsPink.css +++ b/http/custom_themes/trumpy81-ItsPink.css @@ -1,10 +1,4 @@ -.color-primary-0 { color: #ff4dd2 } /* Main Primary color */ -.color-primary-1 { color: #18150F } -.color-primary-2 { color: #666563 } -.color-primary-3 { color: #975C00 } -.color-primary-4 { color: #3F2700 } - .panel-default>.panel-heading {background-color: #ff4dd2; color: #800060;} .navbar {margin-bottom: 0px;} @@ -33,6 +27,7 @@ th {background-color: #ff4dd2; color: #800060;} .subtitle {padding: 0px;} .btn-default:hover {background-color: #ff4dd2; color: #cc0099;} +.btn-default:focus {background-color: #FFFFFF; color: #000000;} .btn-active {background-color: #ff4dd2; color: #800060;} .modal-header {background-color: #ff4dd2; color: #800060;} body {background-color: #ff80df; padding-top:70px; padding-bottom:70px;} @@ -41,6 +36,9 @@ body {background-color: #ff80df; padding-top:70px; padding-bottom:70px;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #ffccf2; color: #000000;} +input {background-color: #ffccf2; color: #000000;} + .pagination {margin: 0px;} .pagination>li>span {height: 31px;} .pagination>.active>span {background-color: #ff4dd2;color: #ff80df; cursor: pointer;} diff --git a/http/custom_themes/trumpy81-ItsRed.css b/http/custom_themes/trumpy81-ItsRed.css index 01fb0eb..4f5da44 100644 --- a/http/custom_themes/trumpy81-ItsRed.css +++ b/http/custom_themes/trumpy81-ItsRed.css @@ -1,21 +1,15 @@ -.color-primary-0 { color: #FE9B00 } /* Main Primary color */ -.color-primary-1 { color: #18150F } -.color-primary-2 { color: #666563 } -.color-primary-3 { color: #975C00 } -.color-primary-4 { color: #3F2700 } - -.panel-default>.panel-heading {background-color: #ff0000; color: #330000;} +.panel-default>.panel-heading {background-color: #ff0000; color: #FFFFFF;} .navbar {margin-bottom: 0px;} -.navbar-default {background-color: #FF0000; color: #330000; border: 0px solid #e6e6e6;} +.navbar-default {background-color: #FF0000; color: #FFFFFF; border: 0px solid #e6e6e6;} -.navbar-default .navbar-nav>li>a {color: #330000;} +.navbar-default .navbar-nav>li>a {color: #FFFFFF;} .navbar-default .navbar-nav>li>a:hover{background-color: #ff6666; color: #d9d9d9;} .navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {background-color: #ff6666; color: #d9d9d9;} -.navbar-default .navbar-brand {color: #330000;} +.navbar-default .navbar-brand {color: #FFFFFF;} .navbar-default .navbar-brand:hover {color: #d9d9d9; cursor: pointer;} .navbar-default .navbar-nav>.active>a {background-color: #555555; color: #022B53;} @@ -32,9 +26,10 @@ th {background-color: #FF0000; color: #d9d9d9;} .subtitle {padding: 0px;} -.btn-default:hover {background-color: #ff6666; color: #d9d9d9;} -.btn-active {background-color: #ff6666; color: #d9d9d9;} -.modal-header {background-color: #ff6666; color: #d9d9d9;} +.btn-default:hover {background-color: #ff0000; color: #d9d9d9;} +.btn-default:focus {background-color: #FFFFFF; color: #000000;} +.btn-active {background-color: #FF0000; color: #FFFFFF;} +.modal-header {background-color: #ff0000; color: #FFFFFF;} body {background-color: #ff6666; padding-top:70px; padding-bottom:70px;} .customlink {cursor: pointer;} @@ -42,6 +37,9 @@ body {background-color: #ff6666; padding-top:70px; padding-bottom:70px;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #ffe6e6; color: #000000;} +input {background-color: #ffe6e6; color: #000000;} + .pagination {margin: 0px;} .pagination>li>span {height: 31px;} .pagination>.active>span {background-color: #ff6666;color: #d9d9d94; cursor: pointer;} diff --git a/http/custom_themes/trumpy81-Plex.css b/http/custom_themes/trumpy81-Plex.css index 2e9cee1..b35e785 100644 --- a/http/custom_themes/trumpy81-Plex.css +++ b/http/custom_themes/trumpy81-Plex.css @@ -1,10 +1,4 @@ -.color-primary-0 { color: #fe9b00 } /* Main Primary color */ -.color-primary-1 { color: #18150F } -.color-primary-2 { color: #666563 } -.color-primary-3 { color: #975C00 } -.color-primary-4 { color: #3F2700 } - .panel-default>.panel-heading {background-color: #262626; color: #fe9b00;} .navbar {margin-bottom: 0px;} @@ -32,15 +26,19 @@ th {background-color: #fe9b00; color: #262626;} .subtitle {padding: 0px;} -.btn-default:hover {background-color: #fe9b00; color: #FFFFFF;} -.btn-active {background-color: #fe9b00; color: #262626;} -.modal-header {background-color: #fe9b00; color: #262626;} +.btn-default:hover {background-color: #262626; color: #cc7e00;} +.btn-default:focus {background-color: #ffffff; color: #262626;} +.btn-active {background-color: #262626; color: #fe9b00;} +.modal-header {background-color: #262626; color: #fe9b00;} body {background-color: #1f1f1f; padding-top:70px; padding-bottom:70px;} .customlink {cursor: pointer;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #b3b3b3; color: #000000;} +input {background-color: #b3b3b3; color: #000000;} + .pagination {margin: 0px;} .pagination>li>span {height: 31px;} .pagination>.active>span {background-color: #fe9b00;color: #262626; cursor: pointer;} diff --git a/http/custom_themes/trumpy81-Teal.css b/http/custom_themes/trumpy81-Teal.css index 7a8b2e8..f3b628a 100644 --- a/http/custom_themes/trumpy81-Teal.css +++ b/http/custom_themes/trumpy81-Teal.css @@ -1,11 +1,5 @@ -.color-primary-0 { color: #0082e6 } /* Main Primary color */ -.color-primary-1 { color: #18150F } -.color-primary-2 { color: #666563 } -.color-primary-3 { color: #975C00 } -.color-primary-4 { color: #3F2700 } - -.panel-default>.panel-heading {background-color: #0082e6; color: #003a66;} +.panel-default>.panel-heading {background-color: #0082e6; color: #ffff99;} .navbar {margin-bottom: 0px;} @@ -19,7 +13,7 @@ .navbar-default .navbar-brand:hover {color: #ffff99; cursor: pointer;} .navbar-default .navbar-nav>.active>a {background-color: #0082e6; color: #ffff99;} -.navbar-default .navbar-nav>.active>a:hover {background-color: #0A457F; color: #FFFFFF;} +.navbar-default .navbar-nav>.active>a:hover {background-color: #0082e6; color: #FFFFFF;} .panel-body {background-color: #FFFFFF; padding: 8px;} @@ -33,14 +27,18 @@ th {background-color: #00477e; color: #FFFFFF;} .subtitle {padding: 0px;} .btn-default:hover {background-color: #0082e6; color: #ffff99;} +.btn-default:focus {background-color: #FFFFFF; color: #000000;} .btn-active {background-color: #0082e6; color: #ffff99;} -.modal-header {background-color: #0082e6; color: #003a66;} +.modal-header {background-color: #0082e6; color: #ffff99;} body {background-color: #0082e6; padding-top:70px; padding-bottom:70px;} .customlink {cursor: pointer;} .table {margin-bottom:0px;} .smallfont {font-size: 10pt;} +select {background-color: #cce9ff; color: #000000;} +input {background-color: #cce9ff; color: #000000;} + .pagination {margin: 0px;} .pagination>li>span {height: 31px;} .pagination>.active>span {background-color: #0082e6;color: #003a66; cursor: pointer;} diff --git a/http/index.html b/http/index.html index b5063bb..9324fda 100755 --- a/http/index.html +++ b/http/index.html @@ -32,19 +32,32 @@ - Webtools - + WebTools +