diff --git a/.gitignore b/.gitignore index 38eb73b..f877cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,9 @@ Icon *.pyc debug +#**************** +# Ignore Linux copy +#**************** +*copy*.* + diff --git a/Contents/Code/Docs/webtools-README_DEVS.odt b/Contents/Code/Docs/webtools-README_DEVS.odt index 9936ee1..c182825 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..f6f9b68 100644 --- a/Contents/Code/__init__.py +++ b/Contents/Code/__init__.py @@ -11,38 +11,28 @@ ###################################################################################################################### #********* Constants used ********** -PREFIX = '/applications/webtools' - -NAME = 'WebTools' -ICON = 'WebTools.png' -VERSION = '2.2' -AUTHTOKEN = '' SECRETKEY = '' -DEBUGMODE = False - #********** Imports needed ********* -import os, io, time -from subprocess import call +import sys, locale from webSrv import startWeb, stopWeb -from random import randint +from random import randint #Used for Cookie generation import uuid #Used for secrectKey - - -import datetime +import time +from consts import DEBUGMODE, VERSION, PREFIX, NAME, ICON #********** Initialize ********* def Start(): global SECRETKEY - global VERSION - global DEBUGMODE - # Switch to debug mode if needed - debugFile = Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle', 'debug') - 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)) + + if DEBUGMODE: + 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"))) + Log.Debug('Locale is: ' + str(locale.getdefaultlocale())) + # TODO: Nasty workaround for issue 189 + if (Platform.OS == 'Windows' and locale.getpreferredencoding() == 'cp1251'): + sys.setdefaultencoding("cp1251") + Log.Debug("Default set to cp1251") HTTP.CacheTime = 0 DirectoryObject.thumb = R(ICON) ObjectContainer.title1 = NAME + ' V' + VERSION @@ -52,7 +42,7 @@ def Start(): # Get the secret key used to access the PMS framework ********** FUTURE USE *************** SECRETKEY = genSecretKeyAsStr() - startWeb(SECRETKEY, VERSION, DEBUGMODE) + startWeb(SECRETKEY) #################################################################################################### # Generate secret key @@ -68,7 +58,8 @@ def genSecretKeyAsStr(): ''' This will generate the default settings in the Dict if missing ''' @route(PREFIX + '/makeSettings') def makeSettings(): - Dict['SharedSecret'] = VERSION + '.' + str(randint(0,9999)) + # Used for Cookie generation + Dict['SharedSecret'] = VERSION + '.' + str(randint(0,9999)) # Set default value for http part, if run for the first time if Dict['options_hide_integrated'] == None: Dict['options_hide_integrated'] = 'false' diff --git a/Contents/Code/consts.py b/Contents/Code/consts.py new file mode 100644 index 0000000..128be84 --- /dev/null +++ b/Contents/Code/consts.py @@ -0,0 +1,75 @@ +###################################################################################################################### +# Plex2CSV module unit +# +# Author: dane22, a Plex Community member +# +# This module is for constants used by WebTools and it's modules, as well as to control developer mode +# +# For info about the debug file, see the docs +###################################################################################################################### + +import io, os, json + +DEBUGMODE = False # default for debug mode +WT_AUTH = True # validate password +VERSION = 'ERROR' # version of WebTools +UAS_URL = 'https://github.com/ukdtom/UAS2Res' # USA2 Repo branch +UAS_BRANCH = 'master' # UAS2 branch to check +PREFIX = '/applications/webtools' +NAME = 'WebTools' +ICON = 'WebTools.png' +JSONTIMESTAMP = 0 # timestamp for json export + + +class consts(object): + init_already = False # Make sure part of init only run once + # Init of the class + def __init__(self): + global DEBUGMODE + global WT_AUTH + global UAS_URL + global UAS_BRANCH + global VERSION + global JSONTIMESTAMP + + # Grap version number from the version file + versionFile = Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle', 'VERSION') + with io.open(versionFile, "rb") as version_file: + VERSION = version_file.read().replace('\n','') + + # Switch to debug mode if needed + debugFile = Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle', 'debug') + # Do we have a debug file ? + if os.path.isfile(debugFile): + DEBUGMODE = True + VERSION = VERSION + ' ****** WARNING Debug mode on *********' + try: + # Read it for params + json_file = io.open(debugFile, "rb") + debug = json_file.read() + json_file.close() + debugParams = JSON.ObjectFromString(str(debug)) + Log.Debug('Override debug params are %s' %str(debugParams)) + if 'UAS_Repo' in debugParams: + UAS_URL = debugParams['UAS_Repo'] + if 'UAS_RepoBranch' in debugParams: + UAS_BRANCH = debugParams['UAS_RepoBranch'] + if 'WT_AUTH' in debugParams: + WT_AUTH = debugParams['WT_AUTH'] + if 'JSONTIMESTAMP' in debugParams: + JSONTIMESTAMP = debugParams['JSONTIMESTAMP'] + except: + pass + Log.Debug('******** Using the following debug params ***********') + Log.Debug('DEBUGMODE: ' + str(DEBUGMODE)) + Log.Debug('UAS_Repo: ' + UAS_URL) + Log.Debug('UAS_RepoBranch: ' + UAS_BRANCH) + Log.Debug('Authenticate: ' + str(WT_AUTH)) + Log.Debug('JSON timestamp: ' + str(JSONTIMESTAMP)) + Log.Debug('*****************************************************') + else: + DEBUGMODE = False + +consts = consts() + + diff --git a/Contents/Code/findMedia.py b/Contents/Code/findMedia.py old mode 100644 new mode 100755 index 98f9874..fb80be9 --- a/Contents/Code/findMedia.py +++ b/Contents/Code/findMedia.py @@ -11,17 +11,23 @@ import urllib import unicodedata import json -import time +import time, sys, os +from consts import DEBUGMODE +from misc import misc # Consts used here -AmountOfMediasInDatabase = 0 # Int of amount of medias in a database section -mediasFromDB = [] # Files from the database -mediasFromFileSystem = [] # Files from the file system -statusMsg = 'idle' # Response to getStatus -runningState = 0 # Internal tracker of where we are -bAbort = False # Flag to set if user wants to cancel -Extras = ['behindthescenes','deleted','featurette','interview','scene','short','trailer'] # Local extras -KEYS = ['IGNORE_HIDDEN', 'IGNORED_DIRS', 'VALID_EXTENSIONS'] # Valid keys for prefs +AmountOfMediasInDatabase = 0 # Int of amount of medias in a database section +mediasFromDB = [] # Files from the database +mediasFromFileSystem = [] # Files from the file system +statusMsg = 'idle' # Response to getStatus +runningState = 0 # Internal tracker of where we are +bAbort = False # Flag to set if user wants to cancel +Extras = ['behindthescenes','deleted','featurette','interview','scene','short','trailer'] # Local extras +ExtrasDirs = ['Behind The Scenes', 'Deleted Scenes', 'Featurettes', 'Interviews', 'Scenes', 'Shorts', 'Trailers'] # Directories to be ignored +KEYS = ['IGNORE_HIDDEN', 'IGNORED_DIRS', 'VALID_EXTENSIONS'] # Valid keys for prefs +excludeElements='Actor,Collection,Country,Director,Genre,Label,Mood,Producer,Role,Similar,Writer' +excludeFields='summary,tagline' + class findMedia(object): @@ -46,11 +52,10 @@ def populatePrefs(self): Dict['findMedia'] = { 'IGNORE_HIDDEN' : True, 'IGNORED_DIRS' : [".@__thumb",".AppleDouble","lost+found"], - 'VALID_EXTENSIONS' : ['.m4v', '.3gp', '.nsv', '.ts', '.ty', '.strm', '.rm', '.rmvb', '.m3u', - '.mov', '.qt', '.divx', '.xvid', '.bivx', '.vob', '.nrg', '.img', '.iso', - '.pva', '.wmv', '.asf', '.asx', '.ogm', '.m2v', '.avi', '.bin', '.dat', - '.dvr-ms', '.mpg', '.mpeg', '.mp4', '.mkv', '.avc', '.vp3', '.svq3', '.nuv', - '.viv', '.dv', '.fli', '.flv', '.rar', '.001', '.wpl', '.zip', '.mp3'] + 'VALID_EXTENSIONS' : ['3g2', '3gp', 'asf', 'asx', 'avc', 'avi', 'avs', 'bivx', 'bup', 'divx', 'dv', 'dvr-ms', 'evo', + 'fli', 'flv', 'm2t', 'm2ts', 'm2v', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'nsv', + 'nuv', 'ogm', 'ogv', 'tp', 'pva', 'qt', 'rm', 'rmvb', 'sdp', 'svq3', 'strm', 'ts', 'ty', 'vdr', + 'viv', 'vob', 'vp3', 'wmv', 'wpl', 'wtv', 'xsp', 'xvid', 'webm'] } Dict.Save() @@ -112,7 +117,6 @@ def abort(self, req): req.clear() req.set_status(200) - # Set settings def setSetting(self, req): try: @@ -125,34 +129,20 @@ def setSetting(self, req): req.clear() req.set_status(412) req.finish("Unknown key parameter") - - - - value = req.get_argument('value', 'missing') if value == 'missing': req.clear() req.set_status(412) req.finish("Missing value parameter") - - print 'Ged2', value value = value.replace("u'", "") value = value.split(',') - for item in value: - print 'Ged3', item - - - print 'Ged4', value - -#str.replace(old, new[, max]) - - Dict['findMedia'][key] = value + Dict['findMedia'][key] = value Dict.Save() req.clear() req.set_status(200) except Exception, e: - Log.Debug('Fatal error in setSetting: ' + str(e)) + Log.Exception('Fatal error in setSetting: ' + str(e)) req.clear() req.set_status(500) req.finish("Unknown error happened in findMedia-setSetting: " + str(e)) @@ -182,6 +172,7 @@ def getResult(self, req): if 'WebTools' in retMsg: req.set_status(204) else: + Log.Info('Result is: ' + str(retMsg)) req.set_status(200) req.finish(retMsg) elif runningState == 99: @@ -221,8 +212,8 @@ def findMissingFromFS(): for item in mediasFromDB: if bAbort: raise ValueError('Aborted') - if not os.path.isfile(item): - MissingFromFS.append(item) + if item not in mediasFromFileSystem: + MissingFromFS.append(item) return MissingFromFS except ValueError: Log.Info('Aborted in findMissingFromFS') @@ -231,13 +222,14 @@ def findMissingFromDB(): global MissingFromDB Log.Debug('Finding items missing from Database') MissingFromDB = [] - try: + try: for item in mediasFromFileSystem: if bAbort: raise ValueError('Aborted') if item not in mediasFromDB: - MissingFromDB.append(item) + MissingFromDB.append(item) return MissingFromDB + except ValueError: Log.Info('Aborted in findMissingFromDB') @@ -272,7 +264,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.Exception('Exception happend in scanMedias: ' + str(e)) statusMsg = 'Idle' # Scan the file system @@ -282,45 +274,56 @@ def getFiles(filePath): global statusMsg try: runningState = -1 - Log.Debug("*********************** FileSystem Paths: *****************************************") + Log.Debug("*********************** FileSystem scan Paths: *****************************************") bScanStatusCount = 0 - files = str(filePath)[2:-2].replace("'", "").split(', ') - Log.Debug(files) - for filePath in files: + # Wondering why I do below. Part of find-unmatched, and forgot....SIGH + files = str(filePath)[2:-2].replace("'", "").split(', ') + #for filePath in files: + for filePath in filePath: # Decode filePath bScanStatusCount += 1 filePath2 = urllib.unquote(filePath).decode('utf8') - if filePath2.startswith('u'): - filePath2 = filePath2[1:] - Log.Debug("Handling file #%s: %s" %(bScanStatusCount, String.Unquote(filePath2).encode('utf8', 'ignore'))) - for root, subdirs, files in os.walk(String.Unquote(filePath2).encode('utf8', 'ignore')): + filePath2 = misc().Unicodize(filePath2) + Log.Debug("Handling filepath #%s: %s" %(bScanStatusCount, filePath2.encode('utf8', 'ignore'))) + for root, subdirs, files in os.walk(filePath2): # Need to check if directory in ignore list? if os.path.basename(root) in Dict['findMedia']['IGNORED_DIRS']: continue # Lets look at the file - for file in files: + for file in files: + file = misc().Unicodize(file).encode('utf8') if bAbort: Log.Info('Aborted in getFiles') raise ValueError('Aborted') - if os.path.splitext(file)[1] in Dict['findMedia']['VALID_EXTENSIONS']: + if os.path.splitext(file)[1].lower()[1:] in Dict['findMedia']['VALID_EXTENSIONS']: # File has a valid extention if file.startswith('.') and Dict['findMedia']['IGNORE_HIDDEN']: continue # Filter out local extras if '-' in file: - if os.path.splitext(os.path.basename(file))[0].rsplit('-', 1)[1] in Extras: + if os.path.splitext(os.path.basename(file))[0].rsplit('-', 1)[1].lower() in Extras: continue - mediasFromFileSystem.append(Core.storage.join_path(root,file)) + # filter out local extras directories + if os.path.basename(os.path.normpath(root)).lower() in ExtrasDirs: + continue + composed_file = misc().Unicodize(Core.storage.join_path(root,file)) + if Platform.OS == 'Windows': + # I hate windows + pos = composed_file.find(':') -1 + #composed_file = composed_file[4:] + composed_file = composed_file[pos:] + mediasFromFileSystem.append(composed_file) statusMsg = 'Scanning file: ' + file Log.Debug('***** Finished scanning filesystem *****') -# Log.Debug(mediasFromFileSystem) + if DEBUGMODE: + Log.Debug(mediasFromFileSystem) runningState = 2 except ValueError: statusMsg = 'Idle' runningState = 99 Log.Info('Aborted in getFiles') except Exception, e: - Log.Critical('Exception happend in getFiles: ' + str(e)) + Log.Exception('Exception happend in getFiles: ' + str(e)) runningState = 99 def scanShowDB(sectionNumber=0): @@ -343,7 +346,7 @@ def scanShowDB(sectionNumber=0): # So let's walk the library while True: # Grap shows - shows = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?X-Plex-Container-Start=' + str(iCShow) + '&X-Plex-Container-Size=' + str(self.MediaChuncks)).xpath('//Directory') + shows = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?X-Plex-Container-Start=' + str(iCShow) + '&X-Plex-Container-Size=' + str(self.MediaChuncks) + '&excludeElements=' + excludeElements + '&excludeFields=' + excludeFields).xpath('//Directory') # Grap individual show for show in shows: statusShow = show.get('title') @@ -352,7 +355,7 @@ def scanShowDB(sectionNumber=0): iCSeason = 0 # Grap seasons while True: - seasons = XML.ElementFromURL('http://127.0.0.1:32400' + show.get('key') + '?X-Plex-Container-Start=' + str(iCSeason) + '&X-Plex-Container-Size=' + str(self.MediaChuncks)).xpath('//Directory') + seasons = XML.ElementFromURL('http://127.0.0.1:32400' + show.get('key') + '?X-Plex-Container-Start=' + str(iCSeason) + '&X-Plex-Container-Size=' + str(self.MediaChuncks) + '&excludeElements=' + excludeElements + '&excludeFields=' + excludeFields).xpath('//Directory') # Grap individual season for season in seasons: if season.get('title') == 'All episodes': @@ -365,7 +368,7 @@ def scanShowDB(sectionNumber=0): iEpisode = 0 iCEpisode = 0 while True: - episodes = XML.ElementFromURL('http://127.0.0.1:32400' + season.get('key') + '?X-Plex-Container-Start=' + str(iCEpisode) + '&X-Plex-Container-Size=' + str(self.MediaChuncks)).xpath('//Part') + episodes = XML.ElementFromURL('http://127.0.0.1:32400' + season.get('key') + '?X-Plex-Container-Start=' + str(iCEpisode) + '&X-Plex-Container-Size=' + str(self.MediaChuncks) + '&excludeElements=' + excludeElements + '&excludeFields=' + excludeFields).xpath('//Part') for episode in episodes: if bAbort: raise ValueError('Aborted') @@ -388,7 +391,8 @@ def scanShowDB(sectionNumber=0): if len(shows) == 0: statusMsg = 'Scanning database: %s : Done' %(totalSize) Log.Debug('***** Done scanning the database *****') -# Log.Debug(mediasFromDB) + if DEBUGMODE: + Log.Debug(mediasFromDB) runningState = 1 break return @@ -397,7 +401,7 @@ def scanShowDB(sectionNumber=0): runningState = 99 Log.Info('Aborted in ScanShowDB') except Exception, e: - Log.Debug('Fatal error in scanShowDB: ' + str(e)) + Log.Exception('Fatal error in scanShowDB: ' + str(e)) runningState = 99 # End scanShowDB @@ -421,26 +425,27 @@ def scanMovieDb(sectionNumber=0): # So let's walk the library while True: # Grap a chunk from the server - medias = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?X-Plex-Container-Start=' + str(iStart) + '&X-Plex-Container-Size=' + str(self.MediaChuncks)).xpath('//Part') + medias = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?X-Plex-Container-Start=' + str(iStart) + '&X-Plex-Container-Size=' + str(self.MediaChuncks) + '&excludeElements=' + excludeElements + '&excludeFields=' + excludeFields).xpath('//Part') # Walk the chunk for part in medias: if bAbort: raise ValueError('Aborted') iCount += 1 filename = part.get('file') - filename = String.Unquote(filename).encode('utf8', 'ignore') + filename = unicode(misc().Unicodize(part.get('file')).encode('utf8', 'ignore')) mediasFromDB.append(filename) statusMsg = 'Scanning database: item %s of %s : Working' %(iCount, totalSize) iStart += self.MediaChuncks if len(medias) == 0: statusMsg = 'Scanning database: %s : Done' %(totalSize) Log.Debug('***** Done scanning the database *****') -# Log.Debug(mediasFromDB) + if DEBUGMODE: + Log.Debug(mediasFromDB) runningState = 1 break return except Exception, e: - Log.Debug('Fatal error in scanMovieDb: ' + str(e)) + Log.Exception('Fatal error in scanMovieDb: ' + str(e)) runningState = 99 # End scanMovieDb @@ -462,7 +467,7 @@ def scanMovieDb(sectionNumber=0): locations = response[0].xpath('//Directory[@key=' + sectionNumber + ']/Location') sectionLocations = [] for location in locations: - sectionLocations.append(location.get('path')) + sectionLocations.append(os.path.normpath(location.get('path'))) Log.Debug('Going to scan section %s with a title of %s and a type of %s and locations as %s' %(sectionNumber, sectionTitle, sectionType, str(sectionLocations))) if runningState in [0,99]: Thread.Create(scanMedias, globalize=True, sectionNumber=sectionNumber, sectionLocations=sectionLocations, sectionType=sectionType, req=req) @@ -472,7 +477,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.Exception('Fatal error happened in scanSection: ' + str(ex)) 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..b0f18bf 100644 --- a/Contents/Code/git.py +++ b/Contents/Code/git.py @@ -9,22 +9,37 @@ 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 +from consts import DEBUGMODE, UAS_URL, UAS_BRANCH, NAME 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.Exception('Exception happend when trying to force download from UASRes: ' + str(e)) + ''' Grap the tornado req, and process it for GET request''' def reqprocess(self, req): function = req.get_argument('function', 'missing') @@ -143,7 +158,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.Exception('Exception happend in downloadBundle2tmp: ' + str(e)) else: # We got a directory here Log.Debug(filename.split('/')[-2]) @@ -155,7 +170,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.Exception('Exception happend in downloadBundle2tmp: ' + str(e)) # 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,15 +189,25 @@ 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('***************************************************************') 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') Log.Critical('Do NOT FORGET!!!!') Log.Critical('We NEED this log, so please upload to Plex forums') Log.Critical('***************************************************************') + Log.Exception('The error was: ' + str(e)) return + ''' Returns commit time and Id for a git branch ''' + def getAtom_UpdateTime_Id(self, url, branch): + # Build AtomUrl + atomUrl = url + '/commits/' + branch + '.atom' + # Get Atom + atom = HTML.ElementFromURL(atomUrl) + mostRecent = atom.xpath('//entry')[0].xpath('./updated')[0].text[:-6] + commitId = atom.xpath('//entry')[0].xpath('./id')[0].text.split('/')[-1][:10] + return {'commitId' : commitId, 'mostRecent' : mostRecent} + ''' This function will return a list of bundles, where there is an update avail ''' def getUpdateList(self, req): Log.Debug('Got a call for getUpdateList') @@ -195,13 +220,37 @@ def getUpdateList(self, req): # Now walk them one by one for bundle in bundles: if bundle.startswith('https://github'): - gitTime = datetime.datetime.strptime(self.getLastUpdateTime(req, UAS=True, url=bundle), '%Y-%m-%d %H:%M:%S') - sBundleTime = Dict['installed'][bundle]['date'] - bundleTime = datetime.datetime.strptime(sBundleTime, '%Y-%m-%d %H:%M:%S') - if bundleTime < gitTime: - gitInfo = Dict['installed'][bundle] - gitInfo['gitHubTime'] = str(gitTime) - result[bundle] = gitInfo + # Going the new detection way with the commitId? + if 'CommitId' in Dict['installed'][bundle]: + if 'release' in Dict['installed'][bundle]: + relUrl = 'https://api.github.com/repos' + bundle[18:] + '/releases/latest' + Id = JSON.ObjectFromURL(relUrl)['id'] + if Dict['installed'][bundle]['CommitId'] != Id: + gitInfo = Dict['installed'][bundle] + gitInfo['gitHubTime'] = JSON.ObjectFromURL(relUrl)['published_at'] + result[bundle] = gitInfo + else: + updateInfo = self.getAtom_UpdateTime_Id(bundle, Dict['installed'][bundle]['branch']) + if Dict['installed'][bundle]['CommitId'] != updateInfo['commitId']: + gitInfo = Dict['installed'][bundle] + gitInfo['gitHubTime'] = updateInfo['mostRecent'] + result[bundle] = gitInfo + else: + # Sadly has to use timestamps + Log.Info('Using timestamps to detect avail update for ' + bundle) + gitTime = datetime.datetime.strptime(self.getLastUpdateTime(req, UAS=True, url=bundle), '%Y-%m-%d %H:%M:%S') + sBundleTime = Dict['installed'][bundle]['date'] + bundleTime = datetime.datetime.strptime(sBundleTime, '%Y-%m-%d %H:%M:%S') + if bundleTime < gitTime: + gitInfo = Dict['installed'][bundle] + gitInfo['gitHubTime'] = str(gitTime) + result[bundle] = gitInfo + else: + # Let's get a CommitId stamped for future times + updateInfo = self.getAtom_UpdateTime_Id(bundle, Dict['installed'][bundle]['branch']) + Log.Info('Stamping %s with a commitId of %s for future ref' %(bundle, updateInfo['commitId'])) + Dict['installed'][bundle]['CommitId'] = updateInfo['commitId'] + Dict.Save() Log.Debug('Updates avail: ' + str(result)) req.clear() req.set_status(200) @@ -212,7 +261,7 @@ def getUpdateList(self, req): req.clear() req.set_status(204) except Exception, e: - Log.Debug('Fatal error happened in getUpdateList: ' + str(e)) + Log.Exception('Fatal error happened in getUpdateList: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -238,7 +287,7 @@ def getUASCacheList(): results[title] = git return results except Exception, e: - Log.Debug('Exception in Migrate/getUASCacheList : ' + str(e)) + Log.Exception('Exception in Migrate/getUASCacheList : ' + str(e)) return '' # Grap indentifier from plist file and timestamp @@ -281,11 +330,6 @@ def getIdentifier(pluginDir): uasListjson = getUASCacheList() bFound = False for git in uasListjson: - ''' - if target == 'com.plexapp.plugins.Plex2csv': - print 'GED KIG HER' - continue - ''' if target == uasListjson[git]['identifier']: Log.Debug('Found %s is part of uas' %(target)) targetGit = {} @@ -343,7 +387,7 @@ 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.Exception('Fatal error happened in migrate: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -359,7 +403,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.Exception('Exception in uasTypes: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -367,9 +411,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 @@ -381,9 +428,9 @@ def updateUASCache(self, req): else: lastUpdateUAS = datetime.datetime.strptime(str(lastUpdateUAS), '%Y-%m-%d %H:%M:%S.%f') # 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') + masterUpdate = datetime.datetime.strptime(self.getLastUpdateTime(req, True, 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') @@ -399,22 +446,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.Exception('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') + zipfile = Archive.ZipFromURL(UAS_URL+ '/archive/' + UAS_BRANCH + '.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.Exception('Could not download UAS Repo from GitHub' + str(e)) + 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)) + return req for filename in zipfile: # Walk contents of the zip, and extract as needed data = zipfile[filename] @@ -426,7 +477,7 @@ def updateUASCache(self, req): Core.storage.save(path, data) except Exception, e: bError = True - Log.Critical("Unexpected Error " + str(e)) + Log.Exception("Unexpected Error " + str(e)) else: # We got a directory here Log.Debug(filename.split('/')[-2]) @@ -438,7 +489,7 @@ def updateUASCache(self, req): Core.storage.ensure_dirs(path) except Exception, e: bError = True - Log.Critical("Unexpected Error " + str(e)) + Log.Exception("Unexpected Error " + str(e)) # Update the AllBundleInfo as well pms.updateAllBundleInfoFromUAS() pms.updateUASTypesCounters() @@ -446,17 +497,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.Exception('Exception in updateUASCache ' + str(e)) + 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)) + return req ''' list will return a list of all installed gits from GitHub''' def list(self, req): @@ -520,8 +573,15 @@ def saveInstallInfo(url, bundleName, branch): # Walk the one by one, so we can handle upper/lower case for git in gits: if url.upper() == git['repo'].upper(): + # Needs to seperate between release downloads, and branch downloads + if 'RELEASE' in branch.upper(): + relUrl = 'https://api.github.com/repos' + url[18:] + '/releases/latest' + Id = JSON.ObjectFromURL(relUrl)['id'] + else: + Id = HTML.ElementFromURL(url + '/commits/' + branch + '.atom').xpath('//entry')[0].xpath('./id')[0].text.split('/')[-1][:10] key = git['repo'] del git['repo'] + git['CommitId'] = Id git['branch'] = branch git['date'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") Dict['installed'][key] = git @@ -533,9 +593,12 @@ def saveInstallInfo(url, bundleName, branch): break if bNotInUAS: key = url + # Get the last Commit Id of the branch + Id = HTML.ElementFromURL(url + '/commits/master.atom').xpath('//entry')[0].xpath('./id')[0].text.split('/')[-1][:10] pFile = Core.storage.join_path(self.PLUGIN_DIR, bundleName, 'Contents', 'Info.plist') pl = plistlib.readPlist(pFile) git = {} + git['CommitId'] = Id git['title'] = os.path.basename(bundleName)[:-7] git['description'] = '' git['branch'] = branch @@ -552,6 +615,26 @@ def saveInstallInfo(url, bundleName, branch): Dict.Save() return + ''' Get latest Release version ''' + def getLatestRelease(url): + # Get release info if present + try: + relUrl = 'https://api.github.com/repos' + url[18:] + '/releases/latest' + relInfo = JSON.ObjectFromURL(relUrl) + downloadUrl = None + for asset in relInfo['assets']: + if asset['name'].upper() == Dict['PMS-AllBundleInfo'][url]['release'].upper(): + downloadUrl = asset['browser_download_url'] + continue + if downloadUrl: + return downloadUrl + else: + raise "Download URL not found" + except Exception, ex: + Log.Critical('Release info not found on Github: ' + relUrl) + pass + return + ''' Download the bundle ''' def downloadBundle2tmp(url, bundleName, branch): # Helper function @@ -576,12 +659,15 @@ def removeEmptyFolders(path, removeRoot=True): # Get the dict with the installed bundles, and init it if it doesn't exists if not 'installed' in Dict: Dict['installed'] = {} - zipPath = url + '/archive/' + branch + '.zip' + if 'RELEASE' in branch.upper(): + zipPath = getLatestRelease(url) + else: + zipPath = url + '/archive/' + branch + '.zip' try: # Grap file from Github zipfile = Archive.ZipFromURL(zipPath) except Exception, e: - Log.Critical('Exception in downloadBundle2tmp while downloading from GitHub: ' + str(e)) + Log.Exception('Exception in downloadBundle2tmp while downloading from GitHub: ' + str(e)) return False # Create base directory Core.storage.ensure_dirs(Core.storage.join_path(self.PLUGIN_DIR, bundleName)) @@ -610,7 +696,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.Exception('Exception in downloadBundle2tmp while walking the downloaded file to find the plist: ' + str(e)) 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 +747,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.Exception('Exception happend in downloadBundle2tmp: ' + str(e)) else: if cutStr not in filename: continue @@ -676,7 +762,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.Exception('Exception happend in downloadBundle2tmp: ' + str(e)) if not bError and bUpgrade: # Copy files that should be kept between upgrades ("keepFiles") @@ -724,7 +810,7 @@ def removeEmptyFolders(path, removeRoot=True): shutil.move(extractDir, bundleName) except Exception, e: bError = True - Log.Critical('Unable to update plugin: ' + str(e)) + Log.Exception('Unable to update plugin: ' + str(e)) # Delete temporary directory try: @@ -753,14 +839,23 @@ def removeEmptyFolders(path, removeRoot=True): pass return True except Exception, e: - Log.Critical('Exception in downloadBundle2tmp: ' + str(e)) + Log.Exception('Exception in downloadBundle2tmp: ' + str(e)) return False # Starting install main Log.Debug('Starting install') req.clear() url = req.get_argument('url', 'missing') + # Set branch to url argument, or master if missing branch = req.get_argument('branch', 'master') + # Got a release url, and if not, go for what's in the dict for branch + try: + branch = Dict['PMS-AllBundleInfo'][url]['release']+'_WTRELEASE' + except: + try: + branch = Dict['PMS-AllBundleInfo'][url]['branch'] + except: + pass if url == 'missing': req.set_status(412) req.finish("Missing url of git") @@ -798,7 +893,6 @@ def getLastUpdateTime(self, req, UAS=False, url=''): req.set_status(404) req.finish("Missing url of git") return req - # Retrieve current branch name if Dict['installed'].get(url, {}).get('branch'): # Use installed branch name @@ -806,15 +900,22 @@ def getLastUpdateTime(self, req, UAS=False, url=''): elif Dict['PMS-AllBundleInfo'].get(url, {}).get('branch'): # Use branch name from bundle info branch = Dict['PMS-AllBundleInfo'][url]['branch'] + # UAS branch override ? + elif url == UAS_URL : + branch = UAS_BRANCH else: # Otherwise fallback to the "master" branch branch = 'master' - # Check for updates try: - url += '/commits/%s.atom' % branch - Log.Debug('URL is: ' + url) - response = Datetime.ParseDate(HTML.ElementFromURL(url).xpath('//entry')[0].xpath('./updated')[0].text).strftime("%Y-%m-%d %H:%M:%S") + if '_WTRELEASE' in branch: + url = 'https://api.github.com/repos' + url[18:] + '/releases/latest' + Log.Debug('URL is: ' + url) + response = JSON.ObjectFromURL(url)['published_at'] + else: + url += '/commits/%s.atom' % branch + Log.Debug('URL is: ' + url) + response = Datetime.ParseDate(HTML.ElementFromURL(url).xpath('//entry')[0].xpath('./updated')[0].text).strftime("%Y-%m-%d %H:%M:%S") Log.Debug('Last update for: ' + url + ' is: ' + str(response)) if UAS: return response @@ -824,7 +925,7 @@ 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.Exception('Fatal error happened in getLastUpdateTime for :' + url + ' was: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') diff --git a/Contents/Code/jsonExporter.py b/Contents/Code/jsonExporter.py new file mode 100644 index 0000000..7a23a7d --- /dev/null +++ b/Contents/Code/jsonExporter.py @@ -0,0 +1,270 @@ +###################################################################################################################### +# json Exporter module for WebTools +# +# Author: dane22, a Plex Community member +# +###################################################################################################################### + +import os, io +from consts import DEBUGMODE, JSONTIMESTAMP +import datetime +import json +from shutil import move + +FILEEXT = '.json' + +statusMsg = 'idle' # Response to getStatus +runningState = 0 # Internal tracker of where we are +bAbort = False # Flag to set if user wants to cancel + +class jsonExporter(object): + init_already = False # Make sure init only run once + bResultPresent = False # Do we have a result to present + + # Defaults used by the rest of the class + def __init__(self): + self.MediaChuncks = 40 + self.CoreUrl = 'http://127.0.0.1:32400/library/sections/' + # Only init once during the lifetime of this + if not jsonExporter.init_already: + jsonExporter.init_already = True + self.populatePrefs() + Log.Debug('******* Starting jsonExporter *******') + + ''' Populate the defaults, if not already there ''' + def populatePrefs(self): + if Dict['jsonExportTimeStamps'] == None: + Dict['jsonExportTimeStamps'] = {} + Dict.Save() + + ''' Grap the tornado req, and process it for a POST request''' + def reqprocessPost(self, req): + function = req.get_argument('function', 'missing') + if function == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing function parameter") + elif function == 'export': + return self.export(req) + else: + req.clear() + req.set_status(412) + req.finish("Unknown function call") + + def export(self, req): + ''' Return the type of the section ''' + def getSectionType(section): + url = 'http://127.0.0.1:32400/library/sections/' + section + '/all?X-Plex-Container-Start=1&X-Plex-Container-Size=0' + try: + return XML.ElementFromURL(url).xpath('//MediaContainer/@viewGroup')[0] + except: + return "None" + + ''' Create a simple entry in the videoDetails tree ''' + def makeSimpleEntry(media, videoDetails, el): + try: + entry = unicode(videoDetails.get(el)) + if entry != 'None': + media[el] = entry + except: + pass + + ''' Create an array based entry, based on the tag attribute ''' + def makeArrayEntry(media, videoDetails, el): + try: + Entries = videoDetails.xpath('//' + el) + EntryList = [] + for Entry in Entries: + try: + EntryList.append(unicode(Entry.xpath('@tag')[0])) + except: + pass + media[el] = EntryList + except: + pass + + ''' Export the actual .json file, as well as poster and fanart ''' + def makeFiles(ratingKey): + videoDetails = XML.ElementFromURL('http://127.0.0.1:32400/library/metadata/' + ratingKey).xpath('//Video')[0] + try: + media = {} + ''' Now digest the media, and add to the XML ''' + # Id +# try: +# media['guid'] = videoDetails.get('guid') +# except: +# pass + media['About This File'] = 'JSON Export Made with WebTools for Plex' + # Simple entries + elements = ['guid', 'title', 'originalTitle', 'titleSort', 'type', 'summary', 'duration', 'rating', 'ratingImage', 'contentRating', 'studio', 'year', 'tagline', 'originallyAvailableAt', 'audienceRatingImage', 'audienceRating'] + for element in elements: + makeSimpleEntry(media, videoDetails, element) + arrayElements = ['Genre', 'Collection', 'Director', 'Writer', 'Producer', 'Country', 'Label'] + for element in arrayElements: + makeArrayEntry(media, videoDetails, element) + # Locked fields + Locked = [] + try: + Fields = videoDetails.xpath('//Field') + for Field in Fields: + try: + if Field.xpath('@locked')[0] == '1': + Locked.append(unicode(Field.xpath('@name')[0])) + except: + pass + media['Field'] = Locked + except: + pass + # Role aka actor + try: + Roles = videoDetails.xpath('//Role') + orderNo = 1 + Actors = [] + for Role in Roles: + Actor = {} + try: + Actor['name'] = unicode(Role.xpath('@tag')[0]) + except: + pass + try: + Actor['role'] = unicode(Role.xpath('@role')[0]) + except: + pass + try: + Actor['order'] = orderNo + orderNo += 1 + except: + pass + try: + Actor['thumb'] = Role.xpath('@thumb')[0] + except: + pass + Actors.append(Actor) + media['Role'] = Actors + except Exception, e: + Log.Exception('Ged Exception', str(e)) + pass + # Let's start by grapping relevant files for this movie + fileNames = videoDetails.xpath('//Part') + for fileName in fileNames: + filename = fileName.xpath('@file')[0] + filename = String.Unquote(filename).encode('utf8', 'ignore') + # Get name of json file + plexJSON = os.path.splitext(filename)[0] + FILEEXT + Log.Debug('Name and path to plexJSON file is: ' + plexJSON) + try: + with io.open(plexJSON, 'w', encoding='utf-8') as outfile: + outfile.write(unicode(json.dumps(media, indent=4, sort_keys=True))) + except Exception, e: + Log.Debug('Exception happend during saving %s. Exception was: %s' %(plexJSON, str(e))) + # Make poster + posterUrl = 'http://127.0.0.1:32400' + videoDetails.get('thumb') + targetFile = os.path.splitext(filename)[0] + '-poster.jpg' + response = HTTP.Request(posterUrl) + with io.open( targetFile, 'wb' ) as fo: + fo.write( response.content ) + Log.Debug('Poster saved as %s' %targetFile) + # Make fanart + posterUrl = 'http://127.0.0.1:32400' + videoDetails.get('art') + targetFile = os.path.splitext(filename)[0] + '-fanart.jpg' + response = HTTP.Request(posterUrl) + with io.open( targetFile, 'wb' ) as fo: + fo.write( response.content ) + Log.Debug('FanArt saved as %s' %targetFile) + except Exception, e: + Log.Exception('Exception happend in generating json file: ' + str(e)) + + ''' Scan a movie section ''' + def scanMovieSection(req, sectionNumber): + Log.Debug('Starting scanMovieSection') + global AmountOfMediasInDatabase + global mediasFromDB + global statusMsg + global runningState + try: + # Start by getting the last timestamp for a scanning: + if sectionNumber in Dict['jsonExportTimeStamps'].keys(): + timeStamp = Dict['jsonExportTimeStamps'][sectionNumber] + else: + # Setting key for section to epoch start + Dict['jsonExportTimeStamps'][sectionNumber] = 0 + Dict.Save() + timeStamp = 0 + # Debug mode? + if JSONTIMESTAMP != 0: + timeStamp = JSONTIMESTAMP + now = int((datetime.datetime.now()-datetime.datetime(1970,1,1)).total_seconds()) + Log.Debug('Starting scanMovieDb for section %s' %(sectionNumber)) + Log.Debug('Only grap medias updated since: ' + datetime.datetime.fromtimestamp(int(timeStamp)).strftime('%Y-%m-%d %H:%M:%S')) + runningState = -1 + statusMsg = 'Starting to scan database for section %s' %(sectionNumber) + # Start by getting the totals of this section + totalSize = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?updatedAt>=' + str(timeStamp) + '&X-Plex-Container-Start=1&X-Plex-Container-Size=0').get('totalSize') + AmountOfMediasInDatabase = totalSize + Log.Debug('Total size of medias are %s' %(totalSize)) + if totalSize == '0': + # Stamp dict with new timestamp + Dict['jsonExportTimeStamps'][sectionNumber] = now + Dict.Save() + Log.Debug('Nothing to process...Exiting') + return + iStart = 0 + iCount = 0 + statusMsg = 'Scanning database item %s of %s : Working' %(iCount, totalSize) + # So let's walk the library + while True: + # Grap a chunk from the server + videos = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?updatedAt>=' + str(timeStamp) + '&X-Plex-Container-Start=' + str(iStart) + '&X-Plex-Container-Size=' + str(self.MediaChuncks)).xpath('//Video') + # Walk the chunk + for video in videos: + if bAbort: + raise ValueError('Aborted') + iCount += 1 + makeFiles(video.get('ratingKey')) + statusMsg = 'Scanning database: item %s of %s : Working' %(iCount, totalSize) + iStart += self.MediaChuncks + if len(videos) == 0: + statusMsg = 'Scanning database: %s : Done' %(totalSize) + Log.Debug('***** Done scanning the database *****') + runningState = 1 + break + # Stamp dict with new timestamp + Dict['jsonExportTimeStamps'][sectionNumber] = now + Dict.Save() + return + except Exception, e: + Log.Exception('Fatal error in scanMovieDb: ' + str(e)) + runningState = 99 + # End scanMovieDb + + + def scanShowSection(req, sectionNumber): + print 'Ged1 scanShowSection' + + + # ********** Main function ************** + Log.Debug('json export called') + try: + section = req.get_argument('section', '_export_missing_') + if section == '_export_missing_': + req.clear() + req.set_status(412) + req.finish("Missing section parameter") + if getSectionType(section) == 'movie': + scanMovieSection(req, section) + elif getSectionType(section) == 'show': + scanShowSection(req, section) + else: + Log.Debug('Unknown section type for section:' + section + ' type: ' + getSectionType(section)) + req.clear() + req.set_status(404) + req.finish("Unknown sectiontype or sectiion") + except Exception, e: + Log.Exception('Exception in json export' + str(e)) + + + + + + + diff --git a/Contents/Code/logs.py b/Contents/Code/logs.py index 820966c..c5f978f 100644 --- a/Contents/Code/logs.py +++ b/Contents/Code/logs.py @@ -9,7 +9,7 @@ import shutil import time import json -import os, sys +import os, sys, io import zipfile class logs(object): @@ -28,10 +28,10 @@ def __init__(self): self.LOGDIR = os.path.join(os.environ[key], 'Plex Media Server', 'Logs') else: self.LOGDIR = os.path.join(os.environ['HOME'], 'Library', 'Logs', 'Plex Media Server') - if not os.direxists(self.LOGDIR): + if not os.path.isdir(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.Exception('Fatal error happened in Logs list: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -84,7 +84,7 @@ 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.Exception('Fatal error happened in Logs entry: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -113,7 +113,7 @@ 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.Exception('Fatal error happened in Logs list: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -134,7 +134,7 @@ def show(self, req): else: file = os.path.join(self.LOGDIR, fileName) retFile = [] - with io.open(file, 'rb') as content_file: + with io.open(file, 'r', errors='ignore') as content_file: content = content_file.readlines() for line in content: line = line.replace('\n', '') @@ -146,7 +146,7 @@ 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.Exception('Fatal error happened in Logs show: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -171,8 +171,10 @@ def download(self, req): param, value = fullFileName.split(self.LOGDIR,1) myZip.write(os.path.join(root, filename), arcname=value) myZip.close() - req.set_header('Content-Type', 'application/force-download') - req.set_header ('Content-Disposition', 'attachment; filename=' + downFile) + req.set_header ('Content-Disposition', 'attachment; filename="' + downFile + '"') + req.set_header('Cache-Control', 'no-cache') + req.set_header('Pragma', 'no-cache') + req.set_header('Content-Type', 'application/zip') with io.open(zipFileName, 'rb') as f: try: while True: @@ -186,7 +188,7 @@ def download(self, req): os.remove(zipFileName) return req except Exception, e: - Log.Debug('Fatal error happened in Logs download: ' + str(e)) + Log.Exception('Fatal error happened in Logs download: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -198,26 +200,28 @@ def download(self, req): else: file = os.path.join(self.LOGDIR, fileName) retFile = [] - with io.open(file, 'rb') as content_file: + with io.open(file, 'r', errors='ignore') as content_file: content = content_file.readlines() for line in content: line = line.replace('\n', '') line = line.replace('\r', '') retFile.append(line) - req.set_header('Content-Type', 'application/force-download') - req.set_header ('Content-Disposition', 'attachment; filename=' + fileName) + req.set_header ('Content-Disposition', 'attachment; filename="' + fileName + '"') + req.set_header('Content-Type', 'application/text/plain') + req.set_header('Cache-Control', 'no-cache') + req.set_header('Pragma', 'no-cache') for line in retFile: req.write(line + '\n') req.finish() return req except Exception, e: - Log.Debug('Fatal error happened in Logs download: ' + str(e)) + Log.Exception('Fatal error happened in Logs download: ' + str(e)) 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)) except Exception, e: - Log.Debug('Fatal error happened in Logs download: ' + str(e)) + Log.Exception('Fatal error happened in Logs download: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') diff --git a/Contents/Code/misc.py b/Contents/Code/misc.py new file mode 100644 index 0000000..4c842c2 --- /dev/null +++ b/Contents/Code/misc.py @@ -0,0 +1,52 @@ +###################################################################################################################### +# Plex2CSV module unit +# +# Author: dane22, a Plex Community member +# +# This module is for misc utils +# +# Path of code here shamelessly stolen from Plex scanner bundle +# +###################################################################################################################### + +import os, re, string, unicodedata, sys + +RE_UNICODE_CONTROL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \ + u'|' + \ + u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \ + ( + unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), + unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), + unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff) + ) + + + +class misc(object): + init_already = False # Make sure part of init only run once + # Init of the class + def __init__(self): + return + + ''' Below function shamefully stolen from the scanner.bundle, yet modified a bit ''' + # Safely return Unicode. + def Unicodize(self, s): + # Precompose. + try: s = unicodedata.normalize('NFKC', s.decode('utf-8')) + except: + try: s = unicodedata.normalize('NFKC', s.decode(sys.getdefaultencoding())) + except: + try: s = unicodedata.normalize('NFKC', s.decode(sys.getfilesystemencoding())) + except: + try: s = unicodedata.normalize('NFKC', s.decode('ISO-8859-1')) + except: + try: s = unicodedata.normalize('NFKC', s) + except Exception, e: + Log(type(e).__name__ + ' exception precomposing: ' + str(e)) + # Strip control characters. + s = re.sub(RE_UNICODE_CONTROL, '', s) + return s + + + + diff --git a/Contents/Code/plex2csv.py b/Contents/Code/plex2csv.py index 9d4cfcc..4ef37d0 100644 --- a/Contents/Code/plex2csv.py +++ b/Contents/Code/plex2csv.py @@ -25,16 +25,44 @@ 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 scanSection + # 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] ''' @@ -53,18 +81,16 @@ def getMovieListbyIdx(req): 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): def getFullMovieFieldsList(req): - print 'Ged 1' 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() diff --git a/Contents/Code/plextvhelper.py b/Contents/Code/plextvhelper.py index de846fa..5ba05ca 100644 --- a/Contents/Code/plextvhelper.py +++ b/Contents/Code/plextvhelper.py @@ -3,9 +3,10 @@ # # Author: dane22, a Plex Community member # -# NAME variable must be defined in the calling unit, and is the name of the application -# ###################################################################################################################### +import sys + +from consts import VERSION, PREFIX, NAME class plexTV(object): # Defaults used by the rest of the class @@ -16,49 +17,26 @@ def __init__(self): self.serverUrl = self.mainUrl + '/pms/servers' self.resourceURL = self.mainUrl + '/pms/resources.xml' # Mandentory headers + id = self.get_thisPMSIdentity() self.myHeader = {} - self.myHeader['X-Plex-Client-Identifier'] = NAME + '-' + self.get_thisPMSIdentity() - self.myHeader['Accept'] = 'application/xml' + self.myHeader['X-Plex-Client-Identifier'] = NAME + '-' + id + self.myHeader['Accept'] = 'application/json' self.myHeader['X-Plex-Product'] = NAME - self.myHeader['X-Plex-Device-Name'] = NAME + '-' + self.get_thisPMSIdentity() self.myHeader['X-Plex-Version'] = VERSION self.myHeader['X-Plex-Platform'] = Platform.OS # 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: - token = XML.ElementFromURL(self.loginUrl, headers=self.myHeader, method='POST').xpath('//user')[0].get('authenticationToken') + token = JSON.ObjectFromURL(self.loginUrl + '.json', headers=self.myHeader, method='POST')['user']['authToken'] 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.Exception('Login error: ' + str(e)) + return None ''' Is user the owner of the server? user identified by token @@ -87,7 +65,7 @@ def isServerOwner(self, token): Log.Debug('Server %s was found @ plex.tv, but user is not the owner' %(PMSId)) return 2 except Ex.HTTPError, e: - Log.Debug('Unknown exception was: %s' %(e)) + Log.Exception('Unknown exception was: %s' %(e)) return -1 ''' will return the machineIdentity of this server ''' diff --git a/Contents/Code/pms.py b/Contents/Code/pms.py index 05cb92a..3a52f4a 100644 --- a/Contents/Code/pms.py +++ b/Contents/Code/pms.py @@ -4,14 +4,13 @@ # # Author: dane22, a Plex Community member # -# ###################################################################################################################### +from consts import NAME 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.Exception('Fatal error happened in updateUASTypesCounters: ' + str(e)) #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.Exception('Critical error in updateInstallDict while walking the gits: ' + str(e)) return try: @@ -98,31 +89,28 @@ def updateInstallDict(): installBranch = '' # Check if already present, and if an install date also is there installDate = "" + CommitId = "" if key in Dict['PMS-AllBundleInfo']: 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'] + if 'CommitId' in jsonPMSAllBundleInfo: + CommitId = Dict['PMS-AllBundleInfo'][key]['CommitId'] del git['repo'] # Add/Update our Dict Dict['PMS-AllBundleInfo'][key] = git - Dict['PMS-AllBundleInfo'][key]['branch'] = installBranch Dict['PMS-AllBundleInfo'][key]['date'] = installDate + Dict['PMS-AllBundleInfo'][key]['CommitId'] = CommitId + except Exception, e: - Log.Critical('Critical error in updateInstallDict while walking the gits: ' + str(e)) + Log.Exception('Critical error in updateAllBundleInfoFromUAS1 while walking the gits: ' + str(e)) 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.Exception('Fatal error happened in updateAllBundleInfoFromUAS: ' + str(e)) class pms(object): # Defaults used by the rest of the class @@ -147,6 +135,8 @@ def reqprocess(self, req): return self.getSubtitles(req) elif function == 'showSubtitle': return self.showSubtitle(req) + elif function == 'downloadSubtitle': + return self.downloadSubtitle(req) elif function == 'tvShow': return self.TVshow(req) elif function == 'getAllBundleInfo': @@ -157,6 +147,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 +196,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.Exception('Fatal error happened in search: ' + str(e)) + 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)) + ''' 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 +248,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,7 +280,7 @@ 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.Exception('Fatal error happened in getParts: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -283,7 +311,7 @@ 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.Exception('Fatal error happened in uploadFile: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -299,7 +327,7 @@ 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.Exception('Fatal error happened in getAllBundleInfo: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -324,7 +352,7 @@ 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.Exception("Unable to remove the bundle directory: " + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -376,7 +404,7 @@ 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.Exception('Fatal error happened in removeBundle: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -409,8 +437,8 @@ def removeBundle(bundleName, bundleIdentifier, url): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Bundle %s was removed' %(bundleName)) - except: - Log.Debug('Fatal error happened in delBundle') + except Exception, e: + Log.Exception('Fatal error happened in delBundle: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -450,15 +478,18 @@ def delSub(self, req): if filePath.startswith('media://'): # Path to symblink filePath = filePath.replace('media:/', os.path.join(Core.app_support_path, 'Media', 'localhost')) - # Subtitle name - agent, sub = filePath.split('_') - tmp, agent = agent.split('com.') - # Agent used - agent = 'com.' + agent - filePath2 = filePath.replace('Contents', os.path.join('Contents', 'Subtitle Contributions')) - filePath2, language = filePath2.split('Subtitles') - language = language[1:3] - filePath3 = os.path.join(filePath2[:-1], agent, language, sub) + try: + # Subtitle name + agent, sub = filePath.rsplit('_',1) + tmp, agent = agent.split('com.') + # Agent used + agent = 'com.' + agent + filePath2 = filePath.replace('Contents', os.path.join('Contents', 'Subtitle Contributions')) + filePath2, language = filePath2.split('Subtitles') + language = language[1:3] + filePath3 = os.path.join(filePath2[:-1], agent, language, sub) + except Exception, e: + Log.Exception('Exception in delSub generation file Path: ' + str(e)) subtitlesXMLPath, tmp = filePath.split('Contents') agentXMLPath = os.path.join(subtitlesXMLPath, 'Contents', 'Subtitle Contributions', agent + '.xml') subtitlesXMLPath = os.path.join(subtitlesXMLPath, 'Contents', 'Subtitles.xml') @@ -475,7 +506,7 @@ 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.Exception('Exception while deleting an agent based sub: ' + str(e)) req.clear() req.set_status(404) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -503,7 +534,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.Exception('Fatal error happened in delSub, when deleting ' + filePath + ' : ' + str(e)) req.clear() req.set_status(404) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -516,7 +547,7 @@ 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.Exception('Fatal error happened in delSub: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -547,8 +578,8 @@ def getSeason(req, key): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(mySeason)) - except: - Log.Debug('Fatal error happened in TV-Show while fetching season') + except Exception, e: + Log.Exception('Fatal error happened in TV-Show while fetching season: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -574,8 +605,8 @@ def getSeasons(req, key): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(str(json.dumps(mySeasons))) - except: - Log.Debug('Fatal error happened in TV-Show while fetching seasons') + except Exception, e: + Log.Exception('Fatal error happened in TV-Show while fetching seasons: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -595,7 +626,7 @@ def getSize(req, key): req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(size) except: - Log.Debug('Fatal error happened in TV-Show while fetching size') + Log.Exception('Fatal error happened in TV-Show while fetching size %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -637,8 +668,8 @@ def getContents(req, key): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(episodes)) - except: - Log.Debug('Fatal error happened in TV-Show while fetching contents') + except Exception, e: + Log.Exception('Fatal error happened in TV-Show while fetching contents %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -676,8 +707,8 @@ def getContents(req, key): req.set_status(412) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Unknown action for TVshow') - except: - Log.Debug('Fatal error happened in TVshow') + except Exception, e: + Log.Exception('Fatal error happened in TVshow: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -705,19 +736,76 @@ def showSubtitle(self, req): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(response)) - except: - Log.Debug('Fatal error happened in showSubtitle') + except Exception, e: + Log.Exception('Fatal error happened in showSubtitle: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Fatal error happened in showSubtitle') - except: - Log.Debug('Fatal error happened in showSubtitle') + except Exception, e: + Log.Exception('Fatal error happened in showSubtitle: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Fatal error happened in showSubtitle') + ''' Download Subtitle ''' + def downloadSubtitle(self, req): + Log.Debug('Download Subtitle requested') + try: + key = req.get_argument('key', 'missing') + Log.Debug('Subtitle key is %s' %(key)) + if key == 'missing': + req.clear() + req.set_status(412) + req.finish('Missing key of subtitle') + return req + myURL='http://127.0.0.1:32400/library/streams/' + key + try: + # Grab the subtitle + try: + response = HTML.StringFromElement(HTML.ElementFromURL(myURL)) + except Exception, e: + Log.Exception('Fatal error happened in downloadSubtitle: ' + str(e)) + req.clear() + req.set_status(401) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Fatal error happened in downloadSubtitle: ' + str(e)) + # Make it nicer + response = response.replace('

', '',1) + response = response.replace('

', '',1) + response = response.replace('>', '>') + response = response.split('\n') + # Prep the download http headers + req.set_header ('Content-Disposition', 'attachment; filename="subtitle.srt"') + req.set_header('Cache-Control', 'no-cache') + req.set_header('Pragma', 'no-cache') + req.set_header('Content-Type', 'application/text/plain') + # Download the sub + try: + for line in response: + req.write(line + '\n') + req.finish() + return req + except Exception, e: + Log.Exception('Fatal error happened in downloadSubtitle: ' + str(e)) + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Fatal error happened in downloadSubtitle: ' + str(e)) + except Exception, e: + Log.Exception('Fatal error happened in downloadSubtitle: %s' %(e)) + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Fatal error happened in showSubtitle') + except Exception, e: + Log.Exception('Fatal error happened in downloadSubtitle: %s' %(e)) + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Fatal error happened in downloadSubtitle') + ''' get Subtitles ''' def getSubtitles(self, req, mediaKey=''): Log.Debug('Subtitles requested') @@ -765,15 +853,15 @@ def getSubtitles(self, req, mediaKey=''): for mediaStream in MediaStreams: if mediaStream.get('id') == subInfo['key']: subInfo['url'] = mediaStream.get('url') - except: - Log.Debug('Fatal error happened in getSubtitles') + except Exception, e: + Log.Exception('Fatal error happened in getSubtitles: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Fatal error happened in getSubtitles') mediaInfo.append(subInfo) - except: - Log.Debug('Fatal error happened in getSubtitles') + except Exception, e: + Log.Exception('Fatal error happened in getSubtitles %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -785,8 +873,8 @@ def getSubtitles(self, req, mediaKey=''): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(mediaInfo)) - except: - Log.Debug('Fatal error happened in getSubtitles') + except Exception, e: + Log.Exception('Fatal error happened in getSubtitles: %s' %(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -816,7 +904,7 @@ 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.Exception('Fatal error happened in getSectionLetterList: %s ' %(str(e))) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -873,13 +961,13 @@ 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.Exception('Fatal error happened in getSectionByLetter: ' + str(e)) 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)) except Exception, e: - Log.Debug('Fatal error happened in getSectionByLetter: ' + str(e)) + Log.Exception('Fatal error happened in getSectionByLetter: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -928,14 +1016,14 @@ def getSection(self,req): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(Section)) - except: - Log.Debug('Fatal error happened in getSection') + except Exception, e: + Log.Exception('Fatal error happened in getSection %s' %(str(e))) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Fatal error happened in getSection') - except: - Log.Debug('Fatal error happened in getSection') + except Exception, e: + Log.Exception('Fatal error happened in getSection: %s' %(str(e))) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -955,8 +1043,8 @@ def getSectionsList(self,req): req.set_status(200) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish(json.dumps(Sections)) - except: - Log.Debug('Fatal error happened in getSectionsList') + except Exception, e: + Log.Exception('Fatal error happened in getSectionsList: %s' %(str(e))) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') @@ -981,14 +1069,14 @@ def getSectionSize(self, req): req.clear() req.set_status(200) req.finish(section.get('totalSize')) - except: - Log.Debug('Fatal error happened in GetSectionSize') + except Exception, e: + Log.Exception('Fatal error happened in GetSectionSize: %s' %(str(e))) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') req.finish('Fatal error happened in GetSectionSize') - except: - Log.Debug('Fatal error happened in getSectionSize') + except Exception, e: + Log.Exception('Fatal error happened in getSectionSize: %s' %(str(e))) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') diff --git a/Contents/Code/scheduler.py b/Contents/Code/scheduler.py new file mode 100644 index 0000000..2013adc --- /dev/null +++ b/Contents/Code/scheduler.py @@ -0,0 +1,109 @@ +###################################################################################################################### +# Plex2CSV module unit +# +# Author: dane22, a Plex Community member +# +# This module is for schedules, if needed +# +###################################################################################################################### + + +#TODO This module is not working yet + +import io, os, json + + +class scheduler(object): + init_already = False # Make sure part of init only run once + # Init of the class + def __init__(self): + + return + + ''' Grap the tornado req, and process it for a GET request''' + def reqprocess(self, req): + function = req.get_argument('function', 'missing') + if function == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing function parameter") + elif function == 'getSchedules': + req.clear() + req.finish(self.getSchedules(req)) + elif function == 'getSchedule': + #TODO Add a getSchedule method + return + else: + req.clear() + req.set_status(412) + req.finish("Unknown function call") + + ''' Grap the tornado req, and process it for a POST request''' + def reqprocessPost(self, req): + function = req.get_argument('function', 'missing') + if function == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing function parameter") + elif function == 'setSchedule': + return self.setSchedule(req) + else: + req.clear() + req.set_status(412) + req.finish("Unknown function call") + + ''' Add a schedule ''' + def setSchedule(self, req): + print 'Ged setSchedule' + Log.Info('setSchedule called') + try: + name = req.get_argument('name', '_schedule_missing_') + if name == '_schedule_missing_': + req.clear() + req.set_status(412) + req.finish("Missing name parameter") + startTime = req.get_argument('startTime', '_schedule_missing_startTime') + if startTime == '_schedule_missing_startTime': + req.clear() + req.set_status(412) + req.finish("Missing startTime parameter") + repeatHours = req.get_argument('startTime', '_schedule_missing_repeatHours') + if repeatHours == '_schedule_missing_repeatHours': + req.clear() + req.set_status(412) + req.finish("Missing repeatHours parameter") + command = req.get_argument('startTime', '_schedule_missing_command') + if command == '_schedule_missing_command': + req.clear() + req.set_status(412) + req.finish("Missing command parameter") + usePMS = req.get_argument('startTime', '_schedule_missing_usePMS') + if usePMS == '_schedule_missing_usePMS': + req.clear() + req.set_status(412) + req.finish("Missing usePMS parameter") + + + + except Exception, e: + Log.Exception('Exception in setSchedule' + str(e)) + + + + + + + ''' returns a json of scheduled tasks ''' + def getSchedules(self, req): + # Got any schedules + if 'schedules' in Dict: + req.set_status(200) + return Dict['schedules'] + else: + req.set_status(404) + return + + + + + diff --git a/Contents/Code/settings.py b/Contents/Code/settings.py index a30bfe2..fc791fb 100644 --- a/Contents/Code/settings.py +++ b/Contents/Code/settings.py @@ -5,7 +5,7 @@ # ###################################################################################################################### -import json +import json, sys class settings(object): @@ -80,9 +80,10 @@ def setPwd(self, req): req.finish("Old Password did not match") return req except Ex.HTTPError, e: + Log.Exception('Error in setPwd: ' + str(e)) req.clear() req.set_status(e.code) - req.finish(e) + req.finish(str(e)) return req # Return the value of a specific setting @@ -106,10 +107,11 @@ def putSetting(self, req): req.finish("Setting saved") return req except Ex.HTTPError, e: + Log.Exception('Error in putSetting: ' + str(e)) req.clear() req.set_status(e.code) - req.finish(e) - return req + req.finish(str(e)) + # Return the value of a specific setting def getSetting(self, req): @@ -133,10 +135,11 @@ def getSetting(self, req): req.finish(json.dumps('Setting not found')) return req except Ex.HTTPError, e: + Log.Exception('Error in getSetting: ' + str(e)) req.clear() req.set_status(e.code) - req.finish(e) - return req + req.finish(str(e)) + # Return all settings from the Dict def getSettings(self, req): @@ -155,16 +158,8 @@ def getSettings(self, req): req.finish(json.dumps(mySetting)) return req except Ex.HTTPError, e: + Log.Exception('Error in getSettings: ' + str(e)) req.clear() req.set_status(e.code) - req.finish(e) - return req - - - - - - - - + req.finish(str(e)) diff --git a/Contents/Code/webSrv.py b/Contents/Code/webSrv.py index 135fed9..70b7220 100644 --- a/Contents/Code/webSrv.py +++ b/Contents/Code/webSrv.py @@ -6,6 +6,7 @@ # ###################################################################################################################### +from consts import DEBUGMODE, WT_AUTH, VERSION, NAME import sys # Add modules dir to search path modules = Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle', 'Contents', 'Code', 'modules') @@ -17,6 +18,7 @@ from tornado.escape import json_encode, xhtml_escape import threading +import os, sys # Migrated to new way from plextvhelper import plexTV @@ -28,9 +30,8 @@ from language import language from plex2csv import plex2csv from wt import wt - - -import os +from scheduler import scheduler +from jsonExporter import jsonExporter # Below used to find path of this file from inspect import getsourcefile @@ -76,7 +77,6 @@ def __init__(self): # Not used yet return - ''' Return version number, and other info ''' def getVersion(self): retVal = {'version': VERSION, @@ -123,7 +123,6 @@ def get(self): def post(self): global AUTHTOKEN - # Check for an auth header, in case a frontend wanted to use that # Header has precedence compared to params auth_header = self.request.headers.get('Authorization', None) @@ -149,57 +148,104 @@ def post(self): Log.Info('User is: ' + user) # Allow no password when in debug mode if DEBUGMODE: - 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 - 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: + if not WT_AUTH: self.allow() - Log.Info('Local password accepted') + Log.Info('All is good, we are authenticated') 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) + 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.Exception('Exception in Login: ' + str(e)) + self.clear() + self.set_status(e.code) + self.finish(str(e)) + return self + 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.Exception('Exception in Login: ' + str(e)) + self.clear() + self.set_status(e.code) + self.finish(str(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) def allow(self): self.set_secure_cookie(NAME, Hash.MD5(Dict['SharedSecret']+Dict['password']), expires_days = None) @@ -213,7 +259,8 @@ class webTools2Handler(BaseHandler): # Disable auth when debug def prepare(self): if DEBUGMODE: - self.set_secure_cookie(NAME, Hash.MD5(Dict['SharedSecret']+Dict['password']), expires_days = None) + if not WT_AUTH: + self.set_secure_cookie(NAME, Hash.MD5(Dict['SharedSecret']+Dict['password']), expires_days = None) #******* GET REQUEST ********* @authenticated @@ -255,6 +302,9 @@ def get(self, **params): self = plex2csv().reqprocess(self) elif module == 'wt': self = wt().reqprocess(self) + elif module == 'scheduler': + print 'Ged WebSrv Scheduler' + self = scheduler().reqprocess(self) else: self.clear() self.set_status(412) @@ -284,6 +334,10 @@ def post(self, **params): self = findMedia().reqprocessPost(self) elif module == 'wt': self = wt().reqprocessPost(self) + elif module == 'scheduler': + self = scheduler().reqprocessPost(self) + elif module == 'jsonExporter': + self = jsonExporter().reqprocessPost(self) else: self.clear() self.set_status(412) @@ -370,8 +424,6 @@ def start_tornado(): ssl_options={ "certfile": os.path.join(Core.bundle_path, 'Contents', 'Code', 'Certificate', 'WebTools.crt'), "keyfile": os.path.join(Core.bundle_path, 'Contents', 'Code', 'Certificate', 'WebTools.key')}) - - # Set web server port to the setting in the channel prefs port = int(Prefs['WEB_Port_http']) ports = int(Prefs['WEB_Port_https']) @@ -380,7 +432,7 @@ def start_tornado(): Log.Debug('Starting tornado on ports %s and %s' %(port, ports)) IOLoop.instance().start() - Log.Debug('Shutting down tornado') + Log.Debug('Started') ''' Stop the actual instance of tornado ''' def stopWeb(): @@ -388,14 +440,11 @@ def stopWeb(): Log.Debug('Asked Tornado to exit') ''' Main call ''' -def startWeb(secretKey, version, debugmode): +#def startWeb(secretKey, version): +def startWeb(secretKey): global SECRETKEY # Set the secret key for use by other calls in the future maybe? SECRETKEY = secretKey - global VERSION - VERSION = version - global DEBUGMODE - DEBUGMODE = debugmode stopWeb() Log.Debug('tornado is handling the following URI: %s' %(handlers)) t = threading.Thread(target=start_tornado) diff --git a/Contents/Code/wt.py b/Contents/Code/wt.py index c272823..9a6b51b 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,14 +62,14 @@ 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.Exception('Fatal error happened in wt.reset: ' + str(e)) 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: %s' %(str(e))) # Get a list of all css files in http/custom_themes - def getCSS(self,req): + def getCSS(self,req): Log.Debug('getCSS requested') try: targetDir = Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name, 'WebTools.bundle', 'http', 'custom_themes') @@ -82,7 +87,7 @@ def getCSS(self,req): 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.Exception('Fatal error happened in getCSS: ' + str(e)) req.clear() req.set_status(500) req.set_header('Content-Type', 'application/json; charset=utf-8') diff --git a/README.md b/README.md index 5d804f8..fb20192 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ WebTools.bundle =============== [![GitHub issues](https://img.shields.io/github/issues/dagalufh/WebTools.bundle.svg?style=flat)](https://github.com/dagalufh/WebTools.bundle/issues) [![](https://img.shields.io/github/release/dagalufh/WebTools.bundle.svg?style=flat)](https://github.com/dagalufh/WebTools.bundle/releases) [![Download of latest release](https://img.shields.io/github/downloads/dagalufh/WebTools.bundle/latest/total.svg?style=flat)](https://github.com/dagalufh/WebTools.bundle/releases/latest) +[![master](https://img.shields.io/badge/master-stable-green.svg?maxAge=2592000)]() +[![Maintenance](https://img.shields.io/maintenance/yes/2016.svg?maxAge=2592000)]() Please see the wiki for futher information https://github.com/dagalufh/WebTools.bundle/wiki +To download, go here: +https://github.com/dagalufh/WebTools.bundle/releases/latest + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..bb576db --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.3 diff --git a/http/changelog.txt b/http/changelog.txt index 98df709..de29955 100644 --- a/http/changelog.txt +++ b/http/changelog.txt @@ -1,3 +1,30 @@ +V2.3 Release: + Fix: + #110 UAS: Allow it to fetch from release info as well + #134 UAS: Wrong time for Latest Update on Github + #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 + #184 WT: Logging can fail on Mac OS + #187 UAS: Fix for correct sorting. Now sorts alphabetical without concerning about capital/lower case letters. + #188 UAS: New way with consts and debug broken on Windows + #189 WT: Fix for startup issue if on Windows and lang is set to CP1251 + #190 LV: Fix for issue with cyrilian characters, when not running UTF-8 + #197 SUB: Error while deleting any subtitle (Sub-Zero, and file stored within metadata) + + 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 + #185 WT: Enhance developer/debug mode + #191 WT: Version numbering moved to VERSION file in the root of the bundle + #204 FM: New Modul: FindMedia + + +#### + V2.2 Release: BACKEND: diff --git a/http/credits.txt b/http/credits.txt index 38017de..abff6b7 100644 --- a/http/credits.txt +++ b/http/credits.txt @@ -5,10 +5,16 @@ Dagalufh (JS/HTML, Frontend) Custom theme's by: trumpy81 +Help file by: +trumpy81 + Beta Testers: OttoKerner sa2000 trumpy81 Xandi92 +FindMedia: +Regarding the FindMedia module, the FindMissing and threading functionallity was developed by SRazer + And NEVER forget The idea to an UAS was made by mikedm139, the original diff --git a/http/icons/plex_movie.png b/http/icons/plex_movie.png new file mode 100644 index 0000000..9449cdf Binary files /dev/null and b/http/icons/plex_movie.png differ diff --git a/http/icons/plex_show.png b/http/icons/plex_show.png new file mode 100644 index 0000000..82d9f6a Binary files /dev/null and b/http/icons/plex_show.png differ diff --git a/http/icons/plex_video.png b/http/icons/plex_video.png new file mode 100644 index 0000000..fa88f26 Binary files /dev/null and b/http/icons/plex_video.png differ 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 +