def AddViewGroup(name, viewMode="list", contentType="items"): global ViewGroups if viewMode in MediaXML.ViewModes.keys(): Log.Add("(Framework) Adding view group '%s'" % name) ViewGroups[name] = {"ViewMode": str(MediaXML.ViewModes[viewMode]), "ContentType": contentType} else: Log.Add("(Framework) Couldn't create view group '%s' - invalid view mode." % name)
def Open(url, values=None, headers={}): """ Returns the response of a HTTP request using the given URL and values @param url: The URL to request data from @type url: string @param values: The values to post as part of the request @type values: dictionary @param headers: Extra headers to send when making the request (optional) @type headers: dictionary @return: string """ # TODO: Add gzip support try: data = None if values is not None: data = urllib.urlencode(values) h = __headers.copy() for header in headers: h[header] = headers[header] request = urllib2.Request(url, data, __headers) response = urllib2.urlopen(request) if __cookieJar is not None: __cookieJar.save(Plugin.DataPath + __cookieFile) return response except urllib2.HTTPError: Log.Add("(Framework) HTTPError when requesting '%s'" % url) return None except urllib2.URLError: Log.Add("(Framework) URLError when requesting '%s'" % url) return None
def GetCached(url, cacheTime=1800, headers={}, forceUpdate=False): if not Plugin.Dict.has_key("_PMS.HTTP.Cache"): Plugin.Dict["_PMS.HTTP.Cache"] = {} cache = Plugin.Dict["_PMS.HTTP.Cache"] now = datetime.datetime.now() if cache.has_key(url): cachedAt = cache[url]["CheckTime"] expiresAt = cachedAt + datetime.timedelta(seconds=cacheTime) if datetime.datetime.now() < expiresAt and forceUpdate == False: Log.Add("(Framework) Loaded '%s' from the cache" % url) return cache[url]["Content"] content = Get(url, headers) if content is not None: cache[url] = {} cache[url]["Content"] = content cache[url]["CheckTime"] = now Plugin.Dict["_PMS.HTTP.Cache"] = cache Plugin.Dict["_PMS.HTTP.CacheTime"] = datetime.datetime.now() Log.Add("(Framework) Saved '%s' to the cache" % url) return content else: Log.Add("(Framework) Couldn't cache '%s'" % url) return None
def __loadDB(): import sqlite3 global __db shouldCreateTables = not os.path.exists(Plugin.__databasePath) __db = sqlite3.connect(Plugin.__databasePath) if shouldCreateTables: try: Plugin.__call(Plugin.__pluginModule.CreateTables) Commit() Log.Add("(Framework) Created database tables") except AttributeError: Log.Add("(Framework) Error creating database tables", False) pass
def SaveDict(): """ Save the plugin's dictionary to the data directory if it has been modified. """ global Dict global __savedDict if Dict != __savedDict: Log.Add("(Framework) Dictionary has been changed") __savedDict = Dict.copy() pickle_file = DataFilePath("DictPickle") f = open(pickle_file, "w") pickle.dump(__savedDict, f, 2) Log.Add("(Framework) Plugin dictionary pickled")
def AddRequestHandler(prefix, handler, name="", thumb="", art=""): """ Add a request handler for a given path prefix. This must be a function within the plugin which accepts pathNouns and count as arguments. The prefix should not include a trailing '/' character. @param prefix: The path prefix to be handled @type prefix: string @param handler: The function that will handle the request @type handler: function @param name: The display name for the prefix @type prefix: string @param thumb: The name of an exposed resource file to use as a thumbnail @type thumb: string @param art: The name of an exposed resource file to use as artwork @type art: string """ global __requestHandlers if not __requestHandlers.has_key(prefix): handler_info = { "handler": handler, "name": name, "thumb": thumb, "art": art } __requestHandlers[prefix] = handler_info Log.Add("(Framework) Adding request handler for prefix '%s'" % prefix)
def SetViewGroup(self, viewGroup): if Plugin.ViewGroups.has_key(viewGroup): self.SetAttr("viewmode", Plugin.ViewGroups[viewGroup]["ViewMode"]) self.SetAttr("content", Plugin.ViewGroups[viewGroup]["ContentType"]) else: Log.Add("(Framework) Error: Invalid view group.")
def LoadDict(): """ Load the plugin's dictionary from the data directory. """ global Dict global __savedDict pickle_file = DataFilePath("DictPickle") try: if os.path.exists(pickle_file): f = open(pickle_file, "r") __savedDict = pickle.load(f) f.close() Log.Add("(Framework) Plugin dictionary unpickled") else: __savedDict = {} except: Log.Add("(Framework) Unable to load plugin dictionary - file appears to be corrupt") __savedDict = {} Dict = __savedDict.copy()
def ResetDict(): """ Reset the plugin's dictionary. This will erase all previously stored values. """ pickle_file = DataFilePath("DictPickle") if os.path.exists(pickle_file): os.remove(pickle_file) Dict = {} __savedDict = {} Log.Add("(Framework) Plugin dictionary reset")
def __loadDefaults(): global __defaultLangDict global __defaultCountryDict global __defaultLocale pos = __defaultLocale.find("-") if pos > -1: lang = __defaultLocale[:pos] langPath = os.path.join(Plugin.BundlePath, "Contents/Strings/%s.json" % lang) if os.path.exists(langPath): __defaultLangDict = JSON.DictFromFile(langPath) Log.Add("(Framework) Loaded %s strings" % lang) else: Log.Add("(Framework) Couldn't find %s strings" % lang) locPath = os.path.join(Plugin.BundlePath, "Contents/Strings/%s.json" % __defaultLocale) if os.path.exists(locPath): __defaultCountryDict = JSON.DictFromFile(locPath) Log.Add("(Framework) Loaded %s strings" % __defaultLocale) else: Log.Add("(Framework) Couldn't find %s strings" % __defaultLocale) else: langPath = os.path.join(Plugin.BundlePath, "Contents/Strings/%s.json" % __defaultLocale) if os.path.exists(langPath): __defaultLangDict = JSON.DictFromFile(langPath) Log.Add("(Framework) Loaded %s strings" % __defaultLocale) else: Log.Add("(Framework) Couldn't find %s strings" % __defaultLocale)
def __loadLocale(loc): global CurrentLocale global __langDict global __countryDict pos = loc.find("-") if pos > -1: lang = loc[:pos] langPath = os.path.join(Plugin.BundlePath, "Contents/Strings/%s.json" % lang) if os.path.exists(langPath): __langDict = JSON.DictFromFile(langPath) Log.Add("(Framework) Loaded %s strings" % lang) else: Log.Add("(Framework) Couldn't find %s strings" % lang) locPath = os.path.join(Plugin.BundlePath, "Contents/Strings/%s.json" % loc) if os.path.exists(locPath): __countryDict = JSON.DictFromFile(locPath) Log.Add("(Framework) Loaded %s strings" % loc) else: Log.Add("(Framework) Couldn't find %s strings" % loc) else: langPath = os.path.join(Plugin.BundlePath, "Contents/Strings/%s.json" % loc) if os.path.exists(langPath): __langDict = JSON.DictFromFile(langPath) Log.Add("(Framework) Loaded %s strings" % loc) else: Log.Add("(Framework) Couldn't find %s strings" % loc) CurrentLocale = loc
def Expose(key, description): """ Make the given preference available for modification via the plugin's request handlers. @param key: The key of the preference @type key: string @param description: The description of the preference @type description: string """ global __publicPrefs if not __publicPrefs.has_key(key): __publicPrefs[key] = description Log.Add("(Framework) Exposed preference '%s'" % key)
def SaveDataToFile(file_name, data): """ Saves data to a given file in the plugin's data directory. @param file_name: The name of the file @type file_name: string @param data: The data to save @type data: any """ f = open(DataFilePath(file_name), 'w') f.write(data) f.close() Log.Add("(Framework) Saved data file named '%s'" % file_name)
def DataFile(file_name): """ Returns the data contained in the given file in the plugin's data directory. @param file_name: The name of the file @type file_name: string @return: data """ f = open(DataFilePath(file_name), 'r') d = f.read() f.close() Log.Add("(Framework) Loaded data file named '%s'" % file_name) return d
def Resource(file_name): """ Returns the data contained in the given file in the Resources directory inside the plugin bundle. @param file_name: The name of the file @type file_name: string @return: data """ f = open(ResourceFilePath(file_name), 'r') d = f.read() f.close() Log.Add("(Framework) Loaded resource named '%s'" % file_name) return d
def __handleInternalRequest(pathNouns, count): # # Handle a request internally # global __publicResources if count > 1: if pathNouns[1] == "resources": if count == 3: if __publicResources.has_key(pathNouns[2]): Log.Add("Getting resource") resource = Resource(pathNouns[2]) Response["Content-Type"] = __publicResources[pathNouns[2]] return resource elif pathNouns[1] == "prefs": return Prefs.__handleRequest(pathNouns,count)
def ExposeResource(resource_name, content_type): """ Make the given file in the Resources directory inside the plugin bundle available publically via request handlers. Returns True if the file was found or False otherwise. @param resource_name: The name of the file @type resource_name: string @param content_type: The content type to use when returning the data in the file @type content_type: string @return: boolean """ global __publicResources if os.path.exists(ResourceFilePath(resource_name)) and not __publicResources.has_key(resource_name): __publicResources[resource_name] = content_type Log.Add("(Framework) Exposed resource named '%s' as '%s'" % (resource_name, content_type)) return True else: return False
def Set(key, value): """ Sets the preference for the given key to the given value. @param key: The key of the preference @type key: string @param value: The value to set @type value: string """ el = Plugin.__prefs.find(key) if el is None: el = XML.Element(key) el.text = value Plugin.__prefs.append(el) else: el.text = value XML.ElementToFile(Plugin.__prefs, Plugin.__prefsPath) Log.Add("(Framework) Set preference '%s' to '%s'", (key, value))
def ElementFromURL(url, use_html_parser=False): """ Creates a new Element object with the response from the given URL. @param url: The URL used in the request @type url: string @param use_html_parser: Specifies whether to parse the response as HTML instead of XML @type use_html_parser: boolean @return: Element """ text = HTTP.Get(url) if text is not None: Log.Add('(Framework) Request to %s return %d bytes' % (url, len(text))) if use_html_parser: try: root = html.fromstring(text) test = html.tostring(root, encoding=unicode) return root except: return fromstring(text) else: return etree.fromstring(text) else: return None
def SetTimeout(timeout): socket.setdefaulttimeout(timeout) Log.Add("(Framework) Set the default socket timeout to %.1f seconds" % timeout)
def __run(_bundlePath): # # Initializes the plugin framework, verifies the plugin & extracts information, then enters a # run loop for handling requests. # global BundlePath global ResourcesPath global Identifier global DataPath global Debug global __pluginModule global __prefs global __prefsPath global __databasePath global __logFilePath global __requestHandlers if sys.platform == "win32": if 'PLEXLOCALAPPDATA' in os.environ: key = 'PLEXLOCALAPPDATA' else: key = 'LOCALAPPDATA' supportFilesPath = os.path.join(os.environ[key], "Plex Media Server", "Plug-in Support") logFilesPath = os.path.join(os.environ[key], "Plex Media Server", "Logs", "PMS Plugin Logs") else: supportFilesPath = os.environ["HOME"] + "/Library/Application Support/Plex Media Server/Plug-in Support" logFilesPath = os.environ["HOME"] + "/Library/Logs/Plex Media Server/PMS Plugin Logs/" def checkpath(path): try: if not os.path.exists(path): os.makedirs(path) except: pass checkpath(supportFilesPath + "/Preferences") checkpath(supportFilesPath + "/Databases") checkpath(logFilesPath) # Set the bundle path variable BundlePath = _bundlePath.rstrip('/') ResourcesPath = BundlePath + "/Contents/Resources" # Add the bundle path to the system path if os.path.isdir(BundlePath + "/Contents"): sys.path.append(BundlePath + "/Contents") if os.path.isdir(BundlePath + "/Contents/Libraries"): sys.path.append(BundlePath + "/Contents/Libraries") else: print "Couldn't find bundle directory" return None # Open the Info.plist file infoplist = XML.ElementFromFile((BundlePath + "/Contents/Info.plist")) if infoplist is None: print "Couldn't load Info.plist file from plugin" return # Get the plugin identifier Identifier = infoplist.xpath('//key[text()="CFBundleIdentifier"]//following-sibling::string/text()')[0] if Identifier is None: print "Invalid Info.plist file in plugin" return None # Set up the log file __logFilePath = logFilesPath + Identifier + ".log" if os.path.exists(__logFilePath): if os.path.exists(__logFilePath + ".old"): os.remove(__logFilePath + ".old") os.rename(__logFilePath, __logFilePath + ".old") # Show a big warning message - Framework v0 is deprecated!! Log.Add("(Framework) Deprecated version\n\nNOTICE: This version of the Plex Media Framework is deprecated and is no longer supported.\nPlease migrate your code to a newer version. More information can be found at http://dev.plexapp.com/\n") Log.Add("(Framework) Plugin initialized", False) # Check whether debugging is enabled try: _debug = infoplist.xpath('//key[text()="PlexPluginDebug"]//following-sibling::string/text()')[0] if _debug == "1": Debug = True Log.Add("(Framework) Debugging is enabled") except: pass # Create the data path if it doesn't already exist DataPath = supportFilesPath + "/Data/" + Identifier if not os.path.isdir(DataPath): os.makedirs(DataPath) # Change directory to the data path os.chdir(DataPath) # If a preference file exists, load it __prefsPath = supportFilesPath + "/Preferences/" + Identifier + ".xml" defaultsPath = BundlePath + "/Contents/Defaults.xml" if os.path.exists(__prefsPath): __prefs = XML.ElementFromFile(__prefsPath) Log.Add("(Framework) Loaded user preferences") # Otherwise, try to apply the defaults file elif os.path.exists(defaultsPath): __prefs = XML.ElementFromFile(defaultsPath) Log.Add("(Framework) Loaded default preferences") # If no preferences were loaded, create an empty preferences element else: __prefs = XML.Element("PluginPreferences") # Load the plugin's dictionary file LoadDict() # Load the plugin's localization strings Locale.__loadDefaults() # TODO: Retrieve locale info from PMS for overriding default dict strings # Locale.__loadLocale(loc) # Set the database file path __databasePath = supportFilesPath + "/Databases/" + Identifier + ".db" # Initialize the plugin's CookieJar HTTP.__loadCookieJar() Log.Add("(Framework) Loaded cookie jar") if HTTP_TIMEOUT_VAR_NAME in os.environ: HTTP.SetTimeout(float(os.environ[HTTP_TIMEOUT_VAR_NAME])) else: HTTP.SetTimeout(HTTP_DEFAULT_TIMEOUT) # Attempt to import the plugin module - if debugging is enabled, don't catch exceptions if Debug: import Code as _plugin Log.Add("(Framework) Imported plugin module") else: try: import Code as _plugin Log.Add("(Framework) Imported plugin module") except ImportError: Log.Add("(Framework) Couldn't import plugin from bundle") return __pluginModule = _plugin # Call the plugin's Start method Log.Add("(Framework) Attempting to start the plugin...") __call(_plugin.Start) Log.Add("(Framework) Plugin started", False) Log.Add("(Framework) Entering run loop") # Enter a run loop to handle requests while True: try: # Read the input path = raw_input() # Strip GET from the start of the path path = path.lstrip("GET ").strip() # Split the path into components and decode. pathNouns = path.replace('?query=', '/').split('/') pathNouns = [urllib.unquote(p) for p in pathNouns] # If no input was given, return an error if len(pathNouns) <= 1: sys.stdout.write("%s\r\n\r\n" % PMS.Error['BadRequest']) sys.stdout.flush() # Otherwise, attempt to handle the request else: Response['Content-Type'] = 'application/xml' Response['Status'] = '200 OK' Response["Headers"] = "" result = None pathNouns.pop(0) count = len(pathNouns) if pathNouns[count-1] == "": count = count - 1 pathNouns.pop(len(pathNouns)-1) Log.Add("(Framework) Handling request : " + path, False) # Check for a management request if pathNouns[0] == ":": result = __handlePMSRequest(pathNouns, count) else: # Check each request handler to see if it handles the current prefix handler = None for key in __requestHandlers: if handler is None: if path.count(key, 0, len(key)) == 1: # Remove the prefix from the path keyNounCount = len(key.split('/')) - 1 for i in range(keyNounCount): pathNouns.pop(0) count = count - keyNounCount # Find the request handler handler = __requestHandlers[key]["handler"] # Check whether we should handle the request internally handled = False if count > 0: if pathNouns[0] == ":": handled = True result = __handleInternalRequest(pathNouns, count) # If the request hasn't been handled, and we have a valid request handler, call it if not handled and handler is not None: result = handler(pathNouns, count) # If the request wasn't handled, return an error if result == None: Log.Add("(Framework) Request not handled by plugin", False) response = "%s\r\n\r\n" % PMS.Error['NotFound'] # If the plugin returned an error, return it to PMS elif result in PMS.Error.values(): Log.Add("(Framework) Plug-in returned an error : %s" % result, False) response = result + "\r\n" # Otherwise, return the result else: Log.Add("(Framework) Response OK") response = "%s\r\nContent-Type: %s\r\nContent-Length: %i\r\n%s\r\n%s\r\n" % \ (Response["Status"], str(Response['Content-Type']), len(result), Response["Headers"], result) sys.stdout.write(response) sys.stdout.flush() # If a KeyboardInterrupt (SIGINT) is raised, stop the plugin except KeyboardInterrupt: # Commit any changes to the database and close it if DB.__db is not None: DB.Commit() DB.__db.close() # Save the dictionary SaveDict() # Exit sys.exit() except EOFError: # Commit any changes to the database and close it if DB.__db is not None: DB.Commit() DB.__db.close() Log.Add("(Framework) Plugin stopped") sys.exit() # If another exception is raised, deal with the problem except: # If in debug mode, print the traceback, otherwise report an internal error if Debug: Log.Add("(Framework) An exception happened:\n%s" % traceback.format_exc()) else: Log.Add("(Framework) An internal error occurred", False) sys.stdout.write("%s\r\n\r\n" % PMS.Error['InternalError']) sys.stdout.flush() # Make sure the plugin's dictionary is saved finally: SaveDict()