diff --git a/Contents/Code/Docs/webtools-README_DEVS.odt b/Contents/Code/Docs/webtools-README_DEVS.odt index ae58ff7..5d0b382 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/findMedia.py b/Contents/Code/findMedia.py index 89431e3..98f9874 100644 --- a/Contents/Code/findMedia.py +++ b/Contents/Code/findMedia.py @@ -14,19 +14,15 @@ import time # 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 -Extras = ['behindthescenes', - 'deleted', - 'featurette', - 'interview', - 'scene', - 'short', - 'trailer' - ] # Local extras +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 + class findMedia(object): init_already = False # Make sure init only run once @@ -49,7 +45,7 @@ def populatePrefs(self): if Dict['findMedia'] == None: Dict['findMedia'] = { 'IGNORE_HIDDEN' : True, - 'IGNORED_DIRS' : ['.@__thumb', '.AppleDouble', 'lost+found'], + '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', @@ -95,40 +91,66 @@ def reqprocessPost(self, req): elif function == 'setSetting': # Call resetSetting return self.setSetting(req) + elif function == 'abort': + # Call abort + return self.abort(req) else: req.clear() req.set_status(412) req.finish("Unknown function call") + # Abort + def abort(self, req): + global bAbort + abort = req.get_argument('abort', 'missing') + if abort == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing abort parameter") + if abort == 'true': + bAbort = True + req.clear() + req.set_status(200) + + # Set settings def setSetting(self, req): -# print 'Ged' try: key = req.get_argument('key', 'missing') if key == 'missing': req.clear() req.set_status(412) req.finish("Missing key parameter") - if key not in ['IGNORE_HIDDEN', 'IGNORED_DIRS', 'VALID_EXTENSIONS']: + if key not in KEYS: 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 + 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.Save() req.clear() req.set_status(200) - - - - except Exception, e: Log.Debug('Fatal error in setSetting: ' + str(e)) req.clear() @@ -136,10 +158,6 @@ def setSetting(self, req): req.finish("Unknown error happened in findMedia-setSetting: " + str(e)) - - - - # Reset settings to default def resetSettings(self, req): Dict['findMedia'] = None @@ -163,15 +181,17 @@ def getResult(self, req): req.set_header('Content-Type', 'application/json; charset=utf-8') if 'WebTools' in retMsg: req.set_status(204) - req.finish('Got Nothing to tell yet') else: req.set_status(200) req.finish(retMsg) + elif runningState == 99: + if bAbort: + req.set_status(204) + else: + req.set_status(204) else: - print 'Not Idle mode' req.clear() req.set_status(204) - req.finish("Still running") return # Return current status @@ -189,50 +209,71 @@ def getStatus(self, req): def scanSection(self, req): global AmountOfMediasInDatabase global retMsg + global bAbort retMsg = ['WebTools'] + bAbort = False def findMissingFromFS(): global MissingFromFS Log.Debug('Finding items missing from FileSystem') MissingFromFS = [] - for item in mediasFromDB: - if not os.path.isfile(item): - MissingFromFS.append(item) - return MissingFromFS + try: + for item in mediasFromDB: + if bAbort: + raise ValueError('Aborted') + if not os.path.isfile(item): + MissingFromFS.append(item) + return MissingFromFS + except ValueError: + Log.Info('Aborted in findMissingFromFS') def findMissingFromDB(): global MissingFromDB Log.Debug('Finding items missing from Database') MissingFromDB = [] - for item in mediasFromFileSystem: - if item not in mediasFromDB: - MissingFromDB.append(item) - return MissingFromDB + try: + for item in mediasFromFileSystem: + if bAbort: + raise ValueError('Aborted') + if item not in mediasFromDB: + MissingFromDB.append(item) + return MissingFromDB + except ValueError: + Log.Info('Aborted in findMissingFromDB') # Scan db and files. Must run as a thread def scanMedias(sectionNumber, sectionLocations, sectionType, req): global runningState global statusMsg global retMsg - -# print 'Ged Type', sectionType - if sectionType == 'movie': - scanMovieDb(sectionNumber=sectionNumber) - elif sectionType == 'show': - scanShowDB(sectionNumber=sectionNumber) - else: - req.clear() - req.set_status(400) - req.finish('Unknown Section Type') - - getFiles(sectionLocations) - retMsg = {} - statusMsg = 'Get missing from File System' - retMsg["MissingFromFS"] = findMissingFromFS() - statusMsg = 'Get missing from database' - retMsg["MissingFromDB"] = findMissingFromDB() - runningState = 0 - statusMsg = 'done' + try: + if sectionType == 'movie': + scanMovieDb(sectionNumber=sectionNumber) + elif sectionType == 'show': + scanShowDB(sectionNumber=sectionNumber) + else: + req.clear() + req.set_status(400) + req.finish('Unknown Section Type') + if bAbort: + raise ValueError('Aborted') + getFiles(sectionLocations) + if bAbort: + raise ValueError('Aborted') + retMsg = {} + statusMsg = 'Get missing from File System' + retMsg["MissingFromFS"] = findMissingFromFS() + if bAbort: + raise ValueError('Aborted') + statusMsg = 'Get missing from database' + retMsg["MissingFromDB"] = findMissingFromDB() + runningState = 0 + statusMsg = 'done' + except ValueError: + Log.Info('Aborted in ScanMedias') + except Exception, e: + Log.Critical('Exception happend in scanMedias: ' + str(e)) + statusMsg = 'Idle' # Scan the file system def getFiles(filePath): @@ -258,6 +299,9 @@ def getFiles(filePath): continue # Lets look at the file for file in files: + if bAbort: + Log.Info('Aborted in getFiles') + raise ValueError('Aborted') if os.path.splitext(file)[1] in Dict['findMedia']['VALID_EXTENSIONS']: # File has a valid extention if file.startswith('.') and Dict['findMedia']['IGNORE_HIDDEN']: @@ -269,10 +313,15 @@ def getFiles(filePath): mediasFromFileSystem.append(Core.storage.join_path(root,file)) statusMsg = 'Scanning file: ' + file Log.Debug('***** Finished scanning filesystem *****') - Log.Debug(mediasFromFileSystem) +# 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)) + runningState = 99 def scanShowDB(sectionNumber=0): global AmountOfMediasInDatabase @@ -305,15 +354,11 @@ def scanShowDB(sectionNumber=0): 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') # Grap individual season - for season in seasons: - + for season in seasons: if season.get('title') == 'All episodes': iSeason += 1 continue - - - statusSeason = ' ' + season.get('title') - + statusSeason = ' ' + season.get('title') statusMsg = statusShows + statusShow + statusSeason iSeason += 1 # Grap Episodes @@ -322,6 +367,8 @@ def scanShowDB(sectionNumber=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') for episode in episodes: + if bAbort: + raise ValueError('Aborted') filename = episode.get('file') filename = String.Unquote(filename).encode('utf8', 'ignore') mediasFromDB.append(filename) @@ -341,10 +388,14 @@ def scanShowDB(sectionNumber=0): if len(shows) == 0: statusMsg = 'Scanning database: %s : Done' %(totalSize) Log.Debug('***** Done scanning the database *****') - Log.Debug(mediasFromDB) +# Log.Debug(mediasFromDB) runningState = 1 break return + except ValueError: + statusMsg = 'Idle' + runningState = 99 + Log.Info('Aborted in ScanShowDB') except Exception, e: Log.Debug('Fatal error in scanShowDB: ' + str(e)) runningState = 99 @@ -373,6 +424,8 @@ def scanMovieDb(sectionNumber=0): medias = XML.ElementFromURL(self.CoreUrl + sectionNumber + '/all?X-Plex-Container-Start=' + str(iStart) + '&X-Plex-Container-Size=' + str(self.MediaChuncks)).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') @@ -382,7 +435,7 @@ def scanMovieDb(sectionNumber=0): if len(medias) == 0: statusMsg = 'Scanning database: %s : Done' %(totalSize) Log.Debug('***** Done scanning the database *****') - Log.Debug(mediasFromDB) +# Log.Debug(mediasFromDB) runningState = 1 break return diff --git a/Contents/Code/git.py b/Contents/Code/git.py index 6f7d9e1..d351731 100644 --- a/Contents/Code/git.py +++ b/Contents/Code/git.py @@ -396,7 +396,9 @@ def updateUASCache(self, req): errMsg = errMsg + '\n\nLooks like permissions are not correct, cuz we where denied access\n' errMsg = errMsg + 'to create a needed directory.\n\n' errMsg = errMsg + 'If running on Linux, you might have to issue:\n' - errMsg = errMsg + 'sudo chown plex:plex ./WebTools.bundle -R' + 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) diff --git a/Contents/Code/modules/plex2csv_moviefields.py b/Contents/Code/modules/plex2csv_moviefields.py new file mode 100644 index 0000000..c78551a --- /dev/null +++ b/Contents/Code/modules/plex2csv_moviefields.py @@ -0,0 +1,141 @@ +###################################################################################################################### +# Plex WebTools helper unit for Plex2CSV module +# +# Author: dane22, a Plex Community member +# +###################################################################################################################### + +# Fields that contains a timestamp and should return a date +dateTimeFields = ['addedAt', 'updatedAt', 'lastViewedAt'] + +# Fields that contains a timestamp and should return a time +timeFields =['duration'] + +fields = { + "Media ID" : {"field" : "@ratingKey", "ReqLevel" : 1, "id" : 1}, + "Title" : {"field" : "@title", "ReqLevel" : 1, "id" : 2}, + "Sort title" : {"field" : "@titleSort", "ReqLevel" : 1, "id" : 3}, + "Studio" : {"field" : "@studio", "ReqLevel" : 1, "id" : 4}, + "Content Rating" : {"field" : "@contentRating", "ReqLevel" : 1, "id" : 5}, + "Year" : {"field" : "@year", "ReqLevel" : 1, "id" : 6}, + "Rating" : {"field" : "@rating", "ReqLevel" : 1, "id" : 7}, + "Summary" : {"field" : "@summary", "ReqLevel" : 1, "id" : 8}, + "Genres" : {"field" : "Genre/@tag", "ReqLevel" : 1, "id" : 9}, + "View Count" : {"field" : "@viewCount", "ReqLevel" : 1, "id" : 10}, + "Last Viewed at" : {"field" : "@lastViewedAt", "ReqLevel" : 1, "id" : 11}, + "Tagline" : {"field" : "@tagline", "ReqLevel" : 1, "id" : 12}, + "Release Date" : {"field" : "@originallyAvailableAt", "ReqLevel" : 1, "id" : 13}, + "Writers" : {"field" : "Writer/@tag", "ReqLevel" : 1, "id" : 14}, + "Country" : {"field" : "Country/@tag", "ReqLevel" : 1, "id" : 15}, + "Duration" : {"field" : "@duration", "ReqLevel" : 1, "id" : 16}, + "Directors" : {"field" : "Director/@tag", "ReqLevel" : 1, "id" : 17}, + "Roles" : {"field" : "Role/@tag", "ReqLevel" : 1, "id" : 18}, + "IMDB Id" : {"field" : "@guid", "ReqLevel" : 1, "id" : 19}, + "Labels" : {"fields" : "Label/@tag", "ReqLevel" : 2, "id" : 20}, + "Locked Fields" : {"field" : "Field/@name", "ReqLevel" : 2, "id" : 21}, + "Extras" : {"field" : "Extras/@size", "ReqLevel" : 2, "id" : 22}, + "Collections" : {"field" : "Collection/@tag", "ReqLevel" : 2, "id" : 23}, + "Original Title" : {"field" : "@originalTitle", "ReqLevel" : 2, "id" : 24}, + "Added" : {"field": "@addedAt", "ReqLevel" : 2, "id" : 25}, + "Updated" : {"field" : "@updatedAt", "ReqLevel" : 2, "id" : 26}, + "Audio Languages" : {"field" : "Media/Part/Stream[@streamType=2]/@languageCode", "ReqLevel" : 2, "id" : 27}, + "Audio Title" : {"field" : "Media/Part/Stream[@streamType=2]/@title", "ReqLevel" : 2, "id" : 28}, + "Subtitle Languages" : {"field" : "Media/Part/Stream[@streamType=3]/@languageCode", "ReqLevel" : 2, "id" : 29}, + "Subtitle Title" : {"field" : "Media/Part/Stream[@streamType=3]/@title", "ReqLevel" : 2, "id" : 30}, + "Subtitle Codec" : {"field" : "Media/Part/Stream[@streamType=3]/@codec", "ReqLevel" : 2, "id" : 31}, + "Accessible" : {"field" : "Media/Part/@accessible", "ReqLevel" : 2, "id" : 32}, + "Exists" : {"field" : "Media/Part/@exists", "ReqLevel" : 2, "id" : 33}, + "Video Resolution" : {"field" : "Media/@videoResolution", "ReqLevel" : 2, "id" : 34}, + "Bitrate" : {"field" : "Media/@bitrate", "ReqLevel" : 2, "id" : 35}, + "Width" : {"field" : "Media/@width", "ReqLevel" : 2, "id" : 36}, + "Height" : {"field" : "Media/@height", "ReqLevel" : 2, "id" : 37}, + "Aspect Ratio" : {"field" : "Media/@aspectRatio", "ReqLevel" : 2, "id" : 38}, + "Audio Channels" : {"field" : "Media/@audioChannels", "ReqLevel" : 2, "id" : 39}, + "Audio Codec" : {"field" : "Media/@audioCodec", "ReqLevel" : 2, "id" : 40}, + "Video Codec" : {"field" : "Media/@videoCodec", "ReqLevel" : 2, "id" : 41}, + "Container" : {"field" : "Media/@container", "ReqLevel" : 2, "id" : 42}, + "Video FrameRate" : {"field" : "Media/@videoFrameRate", "ReqLevel" : 2, "id" : 43}, + "Part File" : {"field" : "Media/Part/@file", "ReqLevel" : 2, "id" : 44}, + "Part Size" : {"field" : "Media/Part/@size", "ReqLevel" : 2, "id" : 45}, + "Part Indexed" : {"field" : "Media/Part/@indexes", "ReqLevel" : 2, "id" : 46}, + "Part Duration" : {"field" : "Media/Part/@duration", "ReqLevel" : 2, "id" : 47}, + "Part Container" : {"field" : "Media/Part/@container", "ReqLevel" : 2, "id" : 48}, + "Part Optimized for Streaming" : {"field" : "Media/Part/@optimizedForStreaming", "ReqLevel" : 2, "id" : 49}, + "Video Stream Title" : {"field" : "Media/Part/Stream[@streamType=1]/@title", "ReqLevel" : 2, "id" : 50}, + "Video Stream Default" : {"field" : "Media/Part/Stream[@streamType=1]/@default", "ReqLevel" : 2, "id" : 51}, + "Video Stream Index" : {"field" : "Media/Part/Stream[@streamType=1]/@index", "ReqLevel" : 2, "id" : 52}, + "Video Stream Pixel Format" : {"field" : "Media/Part/Stream[@streamType=1]/@pixelFormat", "ReqLevel" : 2, "id" : 53}, + "Video Stream Profile" : {"field" : "Media/Part/Stream[@streamType=1]/@profile", "ReqLevel" : 2, "id" : 54}, + "Video Stream Ref Frames" : {"field" : "Media/Part/Stream[@streamType=1]/@refFrames", "ReqLevel" : 2, "id" : 55}, + "Video Stream Scan Type" : {"field" : "Media/Part/Stream[@streamType=1]/@scanType", "ReqLevel" : 2, "id" : 56}, + "Video Stream Stream Identifier" : {"field" : "Media/Part/Stream[@streamType=1]/@streamIdentifier", "ReqLevel" : 2, "id" : 57}, + "Video Stream Width" : {"field" : "Media/Part/Stream[@streamType=1]/@width", "ReqLevel" : 2, "id" : 58}, + "Video Stream Pixel Aspect Ratio" : {"field" : "Media/Part/Stream[@streamType=1]/@pixelAspectRatio", "ReqLevel" : 2, "id" : 59}, + "Video Stream Height" : {"field" : "Media/Part/Stream[@streamType=1]/@height", "ReqLevel" : 2, "id" : 60}, + "Video Stream Has Scaling Matrix" : {"field" : "Media/Part/Stream[@streamType=1]/@hasScalingMatrix", "ReqLevel" : 2, "id" : 61}, + "Video Stream Frame Rate Mode" : {"field" : "Media/Part/Stream[@streamType=1]/@frameRateMode", "ReqLevel" : 2, "id" : 62}, + "Video Stream Frame Rate" : {"field" : "Media/Part/Stream[@streamType=1]/@frameRate", "ReqLevel" : 2, "id" : 63}, + "Video Stream Codec" : {"field" : "Media/Part/Stream[@streamType=1]/@codec", "ReqLevel" : 2, "id" : 64}, + "Video Stream Codec ID" : {"field" : "Media/Part/Stream[@streamType=1]/@codecID", "ReqLevel" : 2, "id" : 65}, + "Video Stream Chroma Sub Sampling" : {"field" : "Media/Part/Stream[@streamType=1]/@chromaSubsampling", "ReqLevel" : 2, "id" : 66}, + "Video Stream Cabac" : {"field" : "Media/Part/Stream[@streamType=1]/@cabac", "ReqLevel" : 2, "id" : 67}, + "Video Stream Anamorphic" : {"field" : "Media/Part/Stream[@streamType=1]/@anamorphic", "ReqLevel" : 2, "id" : 68}, + "Video Stream Language Code" : {"field" : "Media/Part/Stream[@streamType=1]/@languageCode", "ReqLevel" : 2, "id" : 69}, + "Video Stream Language" : {"field" : "Media/Part/Stream[@streamType=1]/@language", "ReqLevel" : 2, "id" : 70}, + "Video Stream Bitrate" : {"field" : "Media/Part/Stream[@streamType=1]/@bitrate", "ReqLevel" : 2, "id" : 71}, + "Video Stream Bit Depth" : {"field" : "Media/Part/Stream[@streamType=1]/@bitDepth'", "ReqLevel" : 2, "id" : 72}, + "Video Stream Duration" : {"field" : "Media/Part/Stream[@streamType=1]/@duration", "ReqLevel" : 2, "id" : 73}, + "Video Stream Level" : {"field" : "Media/Part/Stream[@streamType=1]/@level", "ReqLevel" : 2, "id" : 74}, + "Audio Stream Selected" : {"field" : "Media/Part/Stream[@streamType=2]/@selected", "ReqLevel" : 2, "id" : 75}, + "Audio Stream Default" : {"field" : "Media/Part/Stream[@streamType=2]/@default", "ReqLevel" : 2, "id" : 76}, + "Audio Stream Codec" : {"field" : "Media/Part/Stream[@streamType=2]/@codec", "ReqLevel" : 2, "id" : 77}, + "Audio Stream Index" : {"field" : "Media/Part/Stream[@streamType=2]/@index", "ReqLevel" : 2, "id" : 78}, + "Audio Stream Channels" : {"field" : "Media/Part/Stream[@streamType=2]/@channels", "ReqLevel" : 2, "id" : 79}, + "Audio Stream Bitrate" : {"field" : "Media/Part/Stream[@streamType=2]/@bitrate", "ReqLevel" : 2, "id" : 80}, + "Audio Stream Language" : {"field" : "Media/Part/Stream[@streamType=2]/@language", "ReqLevel" : 2, "id" : 81}, + "Audio Stream Language Code" : {"field" : "Media/Part/Stream[@streamType=2]/@languageCode", "ReqLevel" : 2, "id" : 82}, + "Audio Stream Audio Channel Layout" : {"field" : "Media/Part/Stream[@streamType=2]/@audioChannelLayout", "ReqLevel" : 2, "id" : 83}, + "Audio Stream Bit Depth" : {"field" : "Media/Part/Stream[@streamType=2]/@bitDepth", "ReqLevel" : 2, "id" : 84}, + "Audio Stream Bitrate Mode" : {"field" : "Media/Part/Stream[@streamType=2]/@bitrateMode", "ReqLevel" : 2, "id" : 85}, + "Audio Stream Codec ID" : {"field" : "Media/Part/Stream[@streamType=2]/@codecID", "ReqLevel" : 2, "id" : 86}, + "Audio Stream Duration" : {"field" : "Media/Part/Stream[@streamType=2]/@duration", "ReqLevel" : 2, "id" : 87}, + "Audio Stream Profile" : {"field" : "Media/Part/Stream[@streamType=2]/@profile", "ReqLevel" : 2, "id" : 88}, + "Audio Stream Sampling Rate" : {"field" : "Media/Part/Stream[@streamType=2]/@samplingRate", "ReqLevel" : 2, "id" : 89}, + "Subtitle Stream Codec" : {"field" : "Media/Part/Stream[@streamType=3]/@codec", "ReqLevel" : 2, "id" : 90}, + "Subtitle Stream Index" : {"field" : "Media/Part/Stream[@streamType=3]/@index", "ReqLevel" : 2, "id" : 91}, + "Subtitle Stream Language" : {"field" : "Media/Part/Stream[@streamType=3]/@language", "ReqLevel" : 2, "id" : 92}, + "Subtitle Stream Language Code" : {"field" : "Media/Part/Stream[@streamType=3]/@languageCode", "ReqLevel" : 2, "id" : 93}, + "Subtitle Stream Codec ID" : {"field" : "Media/Part/Stream[@streamType=3]/@codecID", "ReqLevel" : 2, "id" : 94}, + "Subtitle Stream Format" : {"field" : "Media/Part/Stream[@streamType=3]/@format", "ReqLevel" : 2, "id" : 95}, + "Subtitle Stream Title" : {"field" : "Media/Part/Stream[@streamType=3]/@title", "ReqLevel" : 2, "id" : 96}, + "Subtitle Stream Selected" : {"field" : "Media/Part/Stream[@streamType=3]/@selected", "ReqLevel" : 2, "id" : 97} +} + +levels = { + "Level_1" : ['Media ID', 'Title', 'Sort title', 'Studio', 'Content Rating', + 'Year', 'Rating', 'Summary', 'Genres'], + "Level_2" : ['View Count', 'Last Viewed at', 'Tagline', 'Release Date', + 'Writers', 'Country', 'Duration', 'Directors', 'Roles', 'IMDB Id'], + "Level_3" : ['Labels', 'Locked Fields', 'Extras', 'Collections', 'Original Title', + 'Added', 'Updated', 'Audio Languages', 'Audio Title', 'Subtitle Languages', + 'Subtitle Title', 'Subtitle Codec', 'Accessible', 'Exists'], + "Level_4" : ['Video Resolution', 'Bitrate', 'Width', 'Height', 'Aspect Ratio', + 'Audio Channels', 'Audio Codec', 'Video Codec', 'Container', 'Video FrameRate'], + "Level_5" : ['Part File', 'Part Size', 'Part Indexed', 'Part Duration', 'Part Container', + 'Part Optimized for Streaming'], + "Level_6" : ['Video Stream Title', 'Video Stream Default', 'Video Stream Index','Video Stream Pixel Format', + 'Video Stream Profile', 'Video Stream Ref Frames', 'Video Stream Scan Type', + 'Video Stream Stream Identifier', 'Video Stream Width', 'Video Stream Pixel Aspect Ratio', + 'Video Stream Height', 'Video Stream Has Scaling Matrix', 'Video Stream Frame Rate Mode', + 'Video Stream Frame Rate', 'Video Stream Codec', 'Video Stream Codec ID', + 'Video Stream Chroma Sub Sampling', 'Video Stream Cabac', 'Video Stream Anamorphic', + 'Video Stream Language Code', 'Video Stream Language', 'Video Stream Bitrate', + 'Video Stream Bit Depth', 'Video Stream Duration', 'Video Stream Level', + 'Audio Stream Selected', 'Audio Stream Default', 'Audio Stream Codec', + 'Audio Stream Index', 'Audio Stream Channels', 'Audio Stream Bitrate', 'Audio Stream Language', + 'Audio Stream Language Code', 'Audio Stream Audio Channel Layout', 'Audio Stream Bit Depth', + 'Audio Stream Bitrate Mode', 'Audio Stream Codec ID', 'Audio Stream Duration', + 'Audio Stream Profile', 'Audio Stream Sampling Rate', 'Subtitle Stream Codec', + 'Subtitle Stream Index', 'Subtitle Stream Language', 'Subtitle Stream Language Code', + 'Subtitle Stream Codec ID', 'Subtitle Stream Format', 'Subtitle Stream Title', 'Subtitle Stream Selected'] +} diff --git a/Contents/Code/plex2csv.py b/Contents/Code/plex2csv.py new file mode 100644 index 0000000..486cdc1 --- /dev/null +++ b/Contents/Code/plex2csv.py @@ -0,0 +1,45 @@ +###################################################################################################################### +# Plex2CSV module unit +# +# Author: dane22, a Plex Community member +# +# NAME variable must be defined in the calling unit, and is the name of the application +# +# This module is the main module for Plex2CSV +# +###################################################################################################################### + +import plex2csv_moviefields + +class plex2csv(object): + # Defaults used by the rest of the class + def __init__(self): + return + + ''' Grap the tornado req, and process it ''' + 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 == 'getFields': + # Call scanSection + return self.getFields(req) + else: + req.clear() + req.set_status(412) + req.finish("Unknown function call") + + ''' This will return a list of fields avail + Param needed is type=[movie,show,audio,picture] + ''' + def getFields(self, req): + print 'Ged her' + type = req.get_argument('type', 'missing') + if type == 'missing': + req.clear() + req.set_status(412) + req.finish("Missing type parameter") + return + diff --git a/Contents/Code/webSrv.py b/Contents/Code/webSrv.py index 3a45fd4..b4aaa9f 100644 --- a/Contents/Code/webSrv.py +++ b/Contents/Code/webSrv.py @@ -2,12 +2,15 @@ # WebTools helper unit # # Runs a seperate webserver on a specified port -# -# Author: dagaluf, a Plex Community member # Author: dane22, a Plex Community member # ###################################################################################################################### +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') +sys.path.append(modules) + from tornado.web import * from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop @@ -23,6 +26,9 @@ from settings import settings from findMedia import findMedia from language import language +from plex2csv import plex2csv +from wt import wt + import os @@ -30,6 +36,7 @@ from inspect import getsourcefile from os.path import abspath + # TODO #from importlib import import_module # SNIFF....Tornado is V1.0.0, meaning no WebSocket :-( @@ -58,6 +65,8 @@ def isCorrectPath(req): req.clear() req.set_status(404) req.finish(msg) + else: + Log.Info('Verified a correct install path as: ' + targetPath) #************** webTools functions ****************************** ''' Here we have the supported functions ''' @@ -214,7 +223,7 @@ def get(self, **params): if module == 'missing': self.clear() self.set_status(404) - self.finish("Missing function call") + self.finish('Missing function call') return else: Log.Debug('Recieved a get call for module: ' + module) @@ -242,10 +251,14 @@ def get(self, **params): self = findMedia().reqprocess(self) elif module == 'language': self = language().reqprocess(self) + elif module == 'plex2csv': + self = plex2csv().reqprocess(self) + elif module == 'wt': + self = wt().reqprocess(self) else: self.clear() self.set_status(412) - self.finish("Unknown module call") + self.finish('Unknown module call') return @@ -269,10 +282,12 @@ def post(self, **params): self = pms().reqprocessPost(self) elif module == 'findMedia': self = findMedia().reqprocessPost(self) + elif module == 'wol': + self = wol().reqprocessPost(self) else: self.clear() self.set_status(412) - self.finish("Unknown module call") + self.finish('Unknown module call') return #******* DELETE REQUEST ********* diff --git a/Contents/Code/wt.py b/Contents/Code/wt.py new file mode 100644 index 0000000..a2810ed --- /dev/null +++ b/Contents/Code/wt.py @@ -0,0 +1,56 @@ +###################################################################################################################### +# WT unit +# A WebTools bundle plugin +# +# Used for internal functions to WebTools +# +# Author: dane22, a Plex Community member +# +###################################################################################################################### + +import glob +import json + +class wt(object): + + ''' Grap the tornado req, and process it ''' + 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 == 'getCSS': + # Call getCSS + return self.getCSS(req) + else: + req.clear() + req.set_status(412) + req.finish("Unknown function call") + + # Get a list of all css files in http/custom_themes + 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') + myList = glob.glob(targetDir + '/*.css') + if len(myList) == 0: + req.clear() + req.set_status(204) + else: + for n,item in enumerate(myList): + myList[n] = item.replace(targetDir + '/','') + Log.Debug('Returning %s' %(myList)) + req.clear() + req.set_status(200) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish(json.dumps(myList)) + except Exception, e: + Log.Debug('Fatal error happened in getCSS: ' + str(e)) + req.clear() + req.set_status(500) + req.set_header('Content-Type', 'application/json; charset=utf-8') + req.finish('Fatal error happened in getCSS: ' + str(e)) + + + diff --git a/Contents/Resources/WebTools.png b/Contents/Resources/WebTools.png index b3a7f65..bbef059 100644 Binary files a/Contents/Resources/WebTools.png and b/Contents/Resources/WebTools.png differ diff --git a/http/changelog.txt b/http/changelog.txt index 5394ffb..d8d86ec 100644 --- a/http/changelog.txt +++ b/http/changelog.txt @@ -29,6 +29,7 @@ FRONTEND: #133 Subtitlemgmt: Added GUI for uploading subtitles #112 Subtitlemgmt: Allow delete/view for agent subtitles aswell. #### Subtitlemgmt: Now uses the new Language Module + #93 WT: Now has support for custom themes! First out is a draft of trumpy81 that needs to be finetuned by trumpy81 #### diff --git a/http/css/custom.css b/http/css/default.css similarity index 100% rename from http/css/custom.css rename to http/css/default.css diff --git a/http/custom_themes/trumpy81-theme1.css b/http/custom_themes/trumpy81-theme1.css new file mode 100644 index 0000000..15635a3 --- /dev/null +++ b/http/custom_themes/trumpy81-theme1.css @@ -0,0 +1 @@ +.panel-default>.panel-heading {background-color: #242424; color: #fe9b00;} \ No newline at end of file diff --git a/http/index.html b/http/index.html index 65731d9..0b304da 100755 --- a/http/index.html +++ b/http/index.html @@ -5,7 +5,7 @@ - + diff --git a/http/jscript/functions.js b/http/jscript/functions.js index 9776642..fcf05cb 100644 --- a/http/jscript/functions.js +++ b/http/jscript/functions.js @@ -10,6 +10,8 @@ var webtools = { ['logviewer', 'LogViewer/Downloader Tool'], ['install', 'Unsupported AppStore'] ], + stylesheets: [], + active_stylesheet: '', active_module: '', functions: {}, version: 0, @@ -38,7 +40,9 @@ var webtools = { searchkeyword: function () {}, next: function () {}, previous: function () {}, - clearresult: function () {} + clearresult: function () {}, + change_theme_display: function () {}, + change_theme_work: function() {} }; // Webtools function @@ -59,6 +63,7 @@ webtools.list_modules.inline([ $('#OptionsMenu').append('
  • Change Password
  • '); } $('#OptionsMenu').append('
  • Check for Webtools Updates
  • '); + $('#OptionsMenu').append('
  • Change Theme
  • '); $('#OptionsMenu').append('
  • View Changelog
  • '); webtools.defaultoptionsmenu = $('#OptionsMenu').html(); callback('VersionFetch:Success', activatemodulename); @@ -70,6 +75,56 @@ webtools.list_modules.inline([ } }); }, + function (callback, activatemodulename) { + + $.ajax({ + ///webtools2?module=settings&function=getSettings + url: '/webtools2?module=wt&function=getCSS', + cache: false, + dataType: 'JSON', + success: function(data,textStatus, xhr) { + if (xhr.status == 200) { + webtools.stylesheets = data; + } else { + webtools.stylesheets = []; + } + + callback('SettingsFetch:Success'); + }, + error: function(data) { + webtools.active_stylesheet = 'default.css'; + webtools.log('No custom CSSTheme setting found.','Core'); + callback('SettingsFetch:Success'); + } + }); + + // callback('StylesheetSettings:Success', activatemodulename); + }, + function (callback, activatemodulename) { + + $.ajax({ + ///webtools2?module=settings&function=getSettings + url: '/webtools2?module=settings&function=getSetting&name=wt_csstheme', + cache: false, + dataType: 'JSON', + success: function(data) { + webtools.active_stylesheet = data; + if (data !== 'default.css') { + webtools.log('Found custom CSSTheme file setting (custom_themes/' + data + '). Loading it.','Core'); + $('head').append(''); + } + + callback('SettingsFetch:Success'); + }, + error: function(data) { + webtools.active_stylesheet = 'default.css'; + webtools.log('No custom CSSTheme setting found.','Core'); + callback('SettingsFetch:Success'); + } + }); + + // callback('StylesheetSettings:Success', activatemodulename); + }, function(callback, activatemodulename) { //Name:VersionFetch webtools.loading(); @@ -79,7 +134,7 @@ webtools.list_modules.inline([ dataType: 'JSON', success: function(data) { webtools.languagecodes = data; - console.log(webtools.languagecodes); + //console.log(webtools.languagecodes); callback('LanguageCodes:Success', activatemodulename); }, error: function(data) { @@ -276,6 +331,9 @@ webtools.log = function(LogEntry, Source) { if (typeof(Source) == 'undefined') { Source = webtools.active_module; } + if (Source.length === 0) { + Source = 'Core'; + } $.ajax({ url: '/webtools2?module=logs&function=entry&text=[' + Source + '] ' + encodeURIComponent(LogEntry), @@ -560,12 +618,21 @@ webtools.wait_update = function () { $(function(ready) { $('#myModal').on('hidden.bs.modal', function(e) { - console.log('myModal Hidden'); + //console.log('myModal Hidden'); }) + + $(document).on('change','#custom_theme_selectbox',function() { + + + $('#custom_theme_stylesheet').remove(); + if ( $('#custom_theme_selectbox').val() !== 'default.css') { + $('head').append(''); + } + }); }) webtools.loading = function(CustomMessage) { - console.log('myModal Requested'); + //console.log('myModal Requested'); $('#myModalLabel').html('Loading'); if (typeof(CustomMessage) == 'undefined') { $('#myModalBody').html('Loading, please wait.'); @@ -579,7 +646,7 @@ webtools.loading = function(CustomMessage) { keyboard: false, backdrop: 'static' }); - console.log('myModal, to be shown NOW'); + //console.log('myModal, to be shown NOW'); $('#myModal').modal('show'); } } @@ -712,7 +779,7 @@ webtools.previous = function () { } } -webtools.jumptotop = function() { +webtools.jumptotop = function () { $('html, body').animate({ scrollTop: (0) }); @@ -725,4 +792,53 @@ webtools.clearresult = function () { $('#webtoolssearchKeyword').val(''); $('#webtoolssearchbuttonnext').prop('disabled',true); $('#webtoolssearchbuttonprevious').prop('disabled',true); +} + +webtools.change_theme_display = function () { + + var theme_dropdown = [''); + $('#myModalLabel').html(' Change Theme'); + $('#myModalBody').html('Select one of the available themes to use it:
    ' + theme_dropdown.join('\n') + '
    Note: Changing theme in the selectbox will automatically enable it as a preview. Upon closing this modal, it will revert back to your settings.'); + $('#myModalFoot').html(' '); + $('#custom_theme_selectbox').val(webtools.active_stylesheet); + $('#myModal').modal('show'); +} + +webtools.change_theme_work = function() { + $.ajax({ + url: '/webtools2?module=settings&function=putSetting&name=wt_csstheme&value=' + $('#custom_theme_selectbox').val(), + type:'PUT', + dataType:'text', + success: function (data) { + webtools.active_stylesheet = $('#custom_theme_selectbox').val(); + $('#custom_theme_stylesheet').remove(); + if (webtools.active_stylesheet !== 'default.css') { + $('head').append(''); + } + $('#myModalLabel').html(' Change Theme'); + $('#myModalBody').html('Active theme has been changed.'); + $('#myModalFoot').html(''); + // Call webtools_wait_for_reload(); + // That function is an ajax call for /, if 404, wait for a few seconds, then try again. Otherwise, notify user of updated completed. + }, + error: function (data) { + $('#myModalLabel').html(' Change Theme'); + $('#myModalBody').html('An error occured, check the logs for more information.'); + $('#myModalFoot').html(''); + } + }) +} + +webtools.change_theme_reset = function() { + $('#custom_theme_stylesheet').remove(); + + if (webtools.active_stylesheet !== 'default.css') { + $('head').append(''); + } + //$('#custom_theme_stylesheet').prop('href',webtools.active_stylesheet); } \ No newline at end of file diff --git a/http/login.html b/http/login.html index 144e1e2..d73eb28 100644 --- a/http/login.html +++ b/http/login.html @@ -6,7 +6,7 @@ - +