commit 2dd34be9501df101b4fe517187a176bd7550dbce Author: Arne Bultman Date: Mon Dec 26 12:45:54 2011 +0100 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34fcef1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyo +*.pyc diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..722523e --- /dev/null +++ b/addon.xml @@ -0,0 +1,18 @@ + + + + + + + video + + + + + Play BluRays from XBMC with MakeMKV + Awesomeness + all + + diff --git a/default.py b/default.py new file mode 100644 index 0000000..b49fcba --- /dev/null +++ b/default.py @@ -0,0 +1,200 @@ +import xbmc, xbmcgui, subprocess, os, time, sys, urllib, re +import xbmcplugin, xbmcaddon + +# Shared resources +BASE_RESOURCE_PATH = os.path.join( xbmcaddon.Addon().getAddonInfo('path'), "resources" ) +sys.path.append( os.path.join( BASE_RESOURCE_PATH, "lib" ) ) + + +__scriptname__ = "MakeMKV BluRay Watch Plugin" +__scriptID__ = "plugin.makemkvbluray" +__author__ = "Magnetism" +__url__ = "http://bultsblog.com/arne" +__credits__ = "" +__version__ = "0.1" +__addon__ = xbmcaddon.Addon(__scriptID__) + +__language__ = __addon__.getLocalizedString +_ = sys.modules[ "__main__" ].__language__ + +import settings, file, mkvparser, brlog, makemkv + +_log = brlog.BrLog() + +_log.info('Starting the BluRay script') #@UndefinedVariable + +class BluRayStarter: + def __init__(self): + _log.info('Staring') #@UndefinedVariable + self.settings = settings.BluRaySettings() + self.makemkv = makemkv.MakeMkvInteraction() + + def killAndStart(self, mkvStart): + if self.settings.local: + _log.info('Running makemkvcon locally') #@UndefinedVariable + self.killMkv() + # Determine if we're doing the disc or if we're browsing.. + _log.info(mkvStart) #@UndefinedVariable + return subprocess.Popen(mkvStart, shell=True) + else: + _log.info('connecting to remote stream, returning fake file browse class..') #@UndefinedVariable + return file.FakeFile() + + def killMkv(self): + # Linux + try : + _log.info('attempting linux kill of makemkvcon') #@UndefinedVariable + subprocess.call('killall -9 makemkvcon', shell=True) + _log.info('Linux call successful') #@UndefinedVariable + except: + pass + + #Windows. + try : + _log.info('attempting windows kill of makemkvcon') #@UndefinedVariable + subprocess.call('taskkill /F /IM makemkvcon.exe', shell=True) + _log.info('Windows call successful') #@UndefinedVariable + except: + pass + + def browse(self, url) : + _log.info('starting browser handler') #@UndefinedVariable + h = mkvparser.BrowseHandler() + h.start(url) + for k,v in h.titleMap.iteritems() : #@UnusedVariable + self.addLink("%s %s, %s %s" %(_(50005), v['duration'], _(50006), v['chaptercount']),v['file']) + + + def getMainFeatureTitle(self, url): + h = mkvparser.BrowseHandler() + h.start(url) + # Play the longest feature on the disc: + largest = 0 + largestTitle = '' + for k,v in h.titleMap.iteritems() : #@UnusedVariable + m = re.search('(\d+):(\d+):(\d+)', v['duration']) + length = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3)) + if length > largest : + largest = length + largestTitle = v['file'] + _log.info('largest: %d, %s' %(largest,largestTitle)) + return largestTitle + + def handleListing(self): + mode = self.settings.paramMode + _log.info( 'mode: ' + str(mode)) + if mode ==None: + _log.info('Showing categories') + self.CATEGORIES() + _log.info('Showing categories done') +# _log.info(__addon__.) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + + if mode == 1 : + _log.info( 'Entering Disc mode') + self.process(self.makemkv.startStream(self.settings.disc)) + elif mode == 3 : + _log.info( 'Entering Remote mode') + mystarter = BluRayStarter() + mystarter.process('') + elif mode == 2: + _log.info( 'Entering Browse mode') + d = xbmcgui.Dialog() #@UndefinedVariable + choice = d.browse(1, 'Select folder', 'video', 'index.bdmv|.iso|.isoRenamedMeansSkip!|.MDS|.CUE|.CDI|.CCD', False, False, '') + if choice <> '': + self.process(self.makemkv.startFileStream(choice)) + + if mode == 20: + self.settings.showSettings() + + def process(self, ready): + try : + if ready: + _log.info( 'Stream ready. ') + # the Stream has started, start auto playback? + if self.settings.autoPlay: + _log.info( 'Autoplay selected') + title = self.getMainFeatureTitle(self.settings.rootURL) + _log.info( 'Main feature determined to be : ' + title) + opener = urllib.URLopener() + testfile = '' + try: + testfile = title + opener.open(testfile) + except IOError: + testfile = '' + + del opener + + if testfile<>'': + _log.info( 'Playing file ' + testfile) + li = xbmcgui.ListItem(path = testfile) + li.setProperty('IsPlayable', 'true') + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, li) #@UndefinedVariable + else: + self.message(_(50071)) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) #@UndefinedVariable + + else: + # Add the selections as selectable files. + self.browse(self.settings.rootURL) + xbmcplugin.endOfDirectory(int(sys.argv[1])) #@UndefinedVariable + + + except : + self.message(_(50072)) + self.pDialog.close() + xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) #@UndefinedVariable + raise + + def CATEGORIES(self): + # Disc + if self.settings.enableDisc: + disclist = self.makemkv.discList() + for disc in disclist: + self.addDir(_(50061) %(disc[0], disc[1]),1, True, disc[0]) + for disc in disclist: + self.addDir(_(50062) %(disc[0], disc[1]),1, False, disc[0]) + # Filelocation + if self.settings.enableFile: + self.addDir(_(50063),2, True) + self.addDir(_(50064),2, False) + # Remote + # if self.settings.enableRemote: + # self.addDir(_(50065),3, True) + # self.addDir(_(50066),3, False) + self.addDir(_(50060),20, True, '0', False) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + + + def addDir(self, name,mode, autoplay, disc = '0', isPlayable = True): + u=sys.argv[0]+"?mode="+str(mode)+"&autoplay="+urllib.quote_plus(str(autoplay)) + "&disc=" + disc + _log.info(u) + icon = "DefaultVideoPlaylists.png" + if autoplay: + icon= "DefaultVideo.png" + liz=xbmcgui.ListItem(name, iconImage=icon, thumbnailImage='') + if autoplay and isPlayable: + liz.setProperty("IsPlayable", "true") + liz.setInfo( type="Video", infoLabels={ "Title": name } ) + _log.info(name) + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=liz, isFolder= not autoplay) + + + def addLink(self, name,url): + liz=xbmcgui.ListItem(name, iconImage="DefaultVideo.png", thumbnailImage='') #@UndefinedVariable + liz.setInfo( type="Video", infoLabels={ "Title": name } ) + liz.setProperty("IsPlayable" , "true") + xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=url,listitem=liz) #@UndefinedVariable + + def message(self, messageText): + dialog = xbmcgui.Dialog() #@UndefinedVariable + dialog.ok("Info", messageText) + +_log.info("args") +for arg in sys.argv: + _log.info(arg) +_log.info("done args") + +mydisplay = BluRayStarter() +mydisplay.handleListing() diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..d2ebaf0 Binary files /dev/null and b/icon.png differ diff --git a/resources/language/english/strings.xml b/resources/language/english/strings.xml new file mode 100644 index 0000000..954386a --- /dev/null +++ b/resources/language/english/strings.xml @@ -0,0 +1,41 @@ + + MakeMKV location + Autoplay main feature + Remote Setup + Remote IP address + Local Port number + Seconds to wait for stream + General Settings + Browseable Options + Enable Disc support + Enable File location support + Enable Remote location support + Remote Port number + + duration + chapters + + Starting BluRay script + Initializing + Waiting for BluRay to be prepared + Starting Disc + Starting Image + Starting Directory + Waiting for stream + + + Settings + + Play Disc %s: %s + Browse Disc %s: %s + Play Filelocation + Browse Filelocation + Play Remote location + Browse Remote location + + + Running MakeMKV ended abnormally. Is it installed? + unable to find autoplay stream + Error trying to open makemkv stream + The file you've selected cannot be accessed by the filesystem + diff --git a/resources/lib/brlog.py b/resources/lib/brlog.py new file mode 100644 index 0000000..e4ba1b1 --- /dev/null +++ b/resources/lib/brlog.py @@ -0,0 +1,32 @@ + +class BrLog: + __DEBUG = 0 + __INFO = 1 + __WARN = 2 + __ERROR = 3 + + def __init__(self, prefix = ''): + self.logLevel = self.__INFO + if prefix <> '': + prefix = '-' + prefix + self.prefix = prefix + + def setLevel(self, level): + if level >= 0 and level <= 3: + self.logLevel = level + + def info(self, message): + self.log(message, self.__INFO) + + def debug(self, message): + self.log(message, self.__DEBUG) + + def error(self, message): + self.log(message, self.__ERROR) + + def warn(self, message): + self.log(message, self.__WARN) + + def log(self, message, level): + if self.logLevel <= level: + print '[BR%s %d] %s' %(self.prefix, level, message) diff --git a/resources/lib/elementtree/ElementPath.py b/resources/lib/elementtree/ElementPath.py new file mode 100644 index 0000000..e8093c6 --- /dev/null +++ b/resources/lib/elementtree/ElementPath.py @@ -0,0 +1,196 @@ +# +# ElementTree +# $Id: ElementPath.py 1858 2004-06-17 21:31:41Z Fredrik $ +# +# limited xpath support for element trees +# +# history: +# 2003-05-23 fl created +# 2003-05-28 fl added support for // etc +# 2003-08-27 fl fixed parsing of periods in element names +# +# Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved. +# +# fredrik@pythonware.com +# http://www.pythonware.com +# +# -------------------------------------------------------------------- +# The ElementTree toolkit is +# +# Copyright (c) 1999-2004 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + +## +# Implementation module for XPath support. There's usually no reason +# to import this module directly; the ElementTree does this for +# you, if needed. +## + +import re + +xpath_tokenizer = re.compile( + "(::|\.\.|\(\)|[/.*:\[\]\(\)@=])|((?:\{[^}]+\})?[^/:\[\]\(\)@=\s]+)|\s+" + ).findall + +class xpath_descendant_or_self: + pass + +## +# Wrapper for a compiled XPath. + +class Path: + + ## + # Create an Path instance from an XPath expression. + + def __init__(self, path): + tokens = xpath_tokenizer(path) + # the current version supports 'path/path'-style expressions only + self.path = [] + self.tag = None + if tokens and tokens[0][0] == "/": + raise SyntaxError("cannot use absolute path on element") + while tokens: + op, tag = tokens.pop(0) + if tag or op == "*": + self.path.append(tag or op) + elif op == ".": + pass + elif op == "/": + self.path.append(xpath_descendant_or_self()) + continue + else: + raise SyntaxError("unsupported path syntax (%s)" % op) + if tokens: + op, tag = tokens.pop(0) + if op != "/": + raise SyntaxError( + "expected path separator (%s)" % (op or tag) + ) + if self.path and isinstance(self.path[-1], xpath_descendant_or_self): + raise SyntaxError("path cannot end with //") + if len(self.path) == 1 and isinstance(self.path[0], type("")): + self.tag = self.path[0] + + ## + # Find first matching object. + + def find(self, element): + tag = self.tag + if tag is None: + nodeset = self.findall(element) + if not nodeset: + return None + return nodeset[0] + for elem in element: + if elem.tag == tag: + return elem + return None + + ## + # Find text for first matching object. + + def findtext(self, element, default=None): + tag = self.tag + if tag is None: + nodeset = self.findall(element) + if not nodeset: + return default + return nodeset[0].text or "" + for elem in element: + if elem.tag == tag: + return elem.text or "" + return default + + ## + # Find all matching objects. + + def findall(self, element): + nodeset = [element] + index = 0 + while 1: + try: + path = self.path[index] + index = index + 1 + except IndexError: + return nodeset + set = [] + if isinstance(path, xpath_descendant_or_self): + try: + tag = self.path[index] + if not isinstance(tag, type("")): + tag = None + else: + index = index + 1 + except IndexError: + tag = None # invalid path + for node in nodeset: + new = list(node.getiterator(tag)) + if new and new[0] is node: + set.extend(new[1:]) + else: + set.extend(new) + else: + for node in nodeset: + for node in node: + if path == "*" or node.tag == path: + set.append(node) + if not set: + return [] + nodeset = set + +_cache = {} + +## +# (Internal) Compile path. + +def _compile(path): + p = _cache.get(path) + if p is not None: + return p + p = Path(path) + if len(_cache) >= 100: + _cache.clear() + _cache[path] = p + return p + +## +# Find first matching object. + +def find(element, path): + return _compile(path).find(element) + +## +# Find text for first matching object. + +def findtext(element, path, default=None): + return _compile(path).findtext(element, default) + +## +# Find all matching objects. + +def findall(element, path): + return _compile(path).findall(element) + diff --git a/resources/lib/elementtree/ElementTree.py b/resources/lib/elementtree/ElementTree.py new file mode 100644 index 0000000..bd45251 --- /dev/null +++ b/resources/lib/elementtree/ElementTree.py @@ -0,0 +1,1254 @@ +# +# ElementTree +# $Id: ElementTree.py 2326 2005-03-17 07:45:21Z fredrik $ +# +# light-weight XML support for Python 1.5.2 and later. +# +# history: +# 2001-10-20 fl created (from various sources) +# 2001-11-01 fl return root from parse method +# 2002-02-16 fl sort attributes in lexical order +# 2002-04-06 fl TreeBuilder refactoring, added PythonDoc markup +# 2002-05-01 fl finished TreeBuilder refactoring +# 2002-07-14 fl added basic namespace support to ElementTree.write +# 2002-07-25 fl added QName attribute support +# 2002-10-20 fl fixed encoding in write +# 2002-11-24 fl changed default encoding to ascii; fixed attribute encoding +# 2002-11-27 fl accept file objects or file names for parse/write +# 2002-12-04 fl moved XMLTreeBuilder back to this module +# 2003-01-11 fl fixed entity encoding glitch for us-ascii +# 2003-02-13 fl added XML literal factory +# 2003-02-21 fl added ProcessingInstruction/PI factory +# 2003-05-11 fl added tostring/fromstring helpers +# 2003-05-26 fl added ElementPath support +# 2003-07-05 fl added makeelement factory method +# 2003-07-28 fl added more well-known namespace prefixes +# 2003-08-15 fl fixed typo in ElementTree.findtext (Thomas Dartsch) +# 2003-09-04 fl fall back on emulator if ElementPath is not installed +# 2003-10-31 fl markup updates +# 2003-11-15 fl fixed nested namespace bug +# 2004-03-28 fl added XMLID helper +# 2004-06-02 fl added default support to findtext +# 2004-06-08 fl fixed encoding of non-ascii element/attribute names +# 2004-08-23 fl take advantage of post-2.1 expat features +# 2005-02-01 fl added iterparse implementation +# 2005-03-02 fl fixed iterparse support for pre-2.2 versions +# +# Copyright (c) 1999-2005 by Fredrik Lundh. All rights reserved. +# +# fredrik@pythonware.com +# http://www.pythonware.com +# +# -------------------------------------------------------------------- +# The ElementTree toolkit is +# +# Copyright (c) 1999-2005 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + +__all__ = [ + # public symbols + "Comment", + "dump", + "Element", "ElementTree", + "fromstring", + "iselement", "iterparse", + "parse", + "PI", "ProcessingInstruction", + "QName", + "SubElement", + "tostring", + "TreeBuilder", + "VERSION", "XML", + "XMLTreeBuilder", + ] + +## +# The Element type is a flexible container object, designed to +# store hierarchical data structures in memory. The type can be +# described as a cross between a list and a dictionary. +#

+# Each element has a number of properties associated with it: +#

+# +# To create an element instance, use the {@link #Element} or {@link +# #SubElement} factory functions. +#

+# The {@link #ElementTree} class can be used to wrap an element +# structure, and convert it from and to XML. +## + +import string, sys, re + +class _SimpleElementPath: + # emulate pre-1.2 find/findtext/findall behaviour + def find(self, element, tag): + for elem in element: + if elem.tag == tag: + return elem + return None + def findtext(self, element, tag, default=None): + for elem in element: + if elem.tag == tag: + return elem.text or "" + return default + def findall(self, element, tag): + if tag[:3] == ".//": + return element.getiterator(tag[3:]) + result = [] + for elem in element: + if elem.tag == tag: + result.append(elem) + return result + +try: + import ElementPath +except ImportError: + # FIXME: issue warning in this case? + ElementPath = _SimpleElementPath() + +# TODO: add support for custom namespace resolvers/default namespaces +# TODO: add improved support for incremental parsing + +VERSION = "1.2.6" + +## +# Internal element class. This class defines the Element interface, +# and provides a reference implementation of this interface. +#

+# You should not create instances of this class directly. Use the +# appropriate factory functions instead, such as {@link #Element} +# and {@link #SubElement}. +# +# @see Element +# @see SubElement +# @see Comment +# @see ProcessingInstruction + +class _ElementInterface: + # text...tail + + ## + # (Attribute) Element tag. + + tag = None + + ## + # (Attribute) Element attribute dictionary. Where possible, use + # {@link #_ElementInterface.get}, + # {@link #_ElementInterface.set}, + # {@link #_ElementInterface.keys}, and + # {@link #_ElementInterface.items} to access + # element attributes. + + attrib = None + + ## + # (Attribute) Text before first subelement. This is either a + # string or the value None, if there was no text. + + text = None + + ## + # (Attribute) Text after this element's end tag, but before the + # next sibling element's start tag. This is either a string or + # the value None, if there was no text. + + tail = None # text after end tag, if any + + def __init__(self, tag, attrib): + self.tag = tag + self.attrib = attrib + self._children = [] + + def __repr__(self): + return "" % (self.tag, id(self)) + + ## + # Creates a new element object of the same type as this element. + # + # @param tag Element tag. + # @param attrib Element attributes, given as a dictionary. + # @return A new element instance. + + def makeelement(self, tag, attrib): + return Element(tag, attrib) + + ## + # Returns the number of subelements. + # + # @return The number of subelements. + + def __len__(self): + return len(self._children) + + ## + # Returns the given subelement. + # + # @param index What subelement to return. + # @return The given subelement. + # @exception IndexError If the given element does not exist. + + def __getitem__(self, index): + return self._children[index] + + ## + # Replaces the given subelement. + # + # @param index What subelement to replace. + # @param element The new element value. + # @exception IndexError If the given element does not exist. + # @exception AssertionError If element is not a valid object. + + def __setitem__(self, index, element): + assert iselement(element) + self._children[index] = element + + ## + # Deletes the given subelement. + # + # @param index What subelement to delete. + # @exception IndexError If the given element does not exist. + + def __delitem__(self, index): + del self._children[index] + + ## + # Returns a list containing subelements in the given range. + # + # @param start The first subelement to return. + # @param stop The first subelement that shouldn't be returned. + # @return A sequence object containing subelements. + + def __getslice__(self, start, stop): + return self._children[start:stop] + + ## + # Replaces a number of subelements with elements from a sequence. + # + # @param start The first subelement to replace. + # @param stop The first subelement that shouldn't be replaced. + # @param elements A sequence object with zero or more elements. + # @exception AssertionError If a sequence member is not a valid object. + + def __setslice__(self, start, stop, elements): + for element in elements: + assert iselement(element) + self._children[start:stop] = list(elements) + + ## + # Deletes a number of subelements. + # + # @param start The first subelement to delete. + # @param stop The first subelement to leave in there. + + def __delslice__(self, start, stop): + del self._children[start:stop] + + ## + # Adds a subelement to the end of this element. + # + # @param element The element to add. + # @exception AssertionError If a sequence member is not a valid object. + + def append(self, element): + assert iselement(element) + self._children.append(element) + + ## + # Inserts a subelement at the given position in this element. + # + # @param index Where to insert the new subelement. + # @exception AssertionError If the element is not a valid object. + + def insert(self, index, element): + assert iselement(element) + self._children.insert(index, element) + + ## + # Removes a matching subelement. Unlike the find methods, + # this method compares elements based on identity, not on tag + # value or contents. + # + # @param element What element to remove. + # @exception ValueError If a matching element could not be found. + # @exception AssertionError If the element is not a valid object. + + def remove(self, element): + assert iselement(element) + self._children.remove(element) + + ## + # Returns all subelements. The elements are returned in document + # order. + # + # @return A list of subelements. + # @defreturn list of Element instances + + def getchildren(self): + return self._children + + ## + # Finds the first matching subelement, by tag name or path. + # + # @param path What element to look for. + # @return The first matching element, or None if no element was found. + # @defreturn Element or None + + def find(self, path): + return ElementPath.find(self, path) + + ## + # Finds text for the first matching subelement, by tag name or path. + # + # @param path What element to look for. + # @param default What to return if the element was not found. + # @return The text content of the first matching element, or the + # default value no element was found. Note that if the element + # has is found, but has no text content, this method returns an + # empty string. + # @defreturn string + + def findtext(self, path, default=None): + return ElementPath.findtext(self, path, default) + + ## + # Finds all matching subelements, by tag name or path. + # + # @param path What element to look for. + # @return A list or iterator containing all matching elements, + # in document order. + # @defreturn list of Element instances + + def findall(self, path): + return ElementPath.findall(self, path) + + ## + # Resets an element. This function removes all subelements, clears + # all attributes, and sets the text and tail attributes to None. + + def clear(self): + self.attrib.clear() + self._children = [] + self.text = self.tail = None + + ## + # Gets an element attribute. + # + # @param key What attribute to look for. + # @param default What to return if the attribute was not found. + # @return The attribute value, or the default value, if the + # attribute was not found. + # @defreturn string or None + + def get(self, key, default=None): + return self.attrib.get(key, default) + + ## + # Sets an element attribute. + # + # @param key What attribute to set. + # @param value The attribute value. + + def set(self, key, value): + self.attrib[key] = value + + ## + # Gets a list of attribute names. The names are returned in an + # arbitrary order (just like for an ordinary Python dictionary). + # + # @return A list of element attribute names. + # @defreturn list of strings + + def keys(self): + return self.attrib.keys() + + ## + # Gets element attributes, as a sequence. The attributes are + # returned in an arbitrary order. + # + # @return A list of (name, value) tuples for all attributes. + # @defreturn list of (string, string) tuples + + def items(self): + return self.attrib.items() + + ## + # Creates a tree iterator. The iterator loops over this element + # and all subelements, in document order, and returns all elements + # with a matching tag. + #

+ # If the tree structure is modified during iteration, the result + # is undefined. + # + # @param tag What tags to look for (default is to return all elements). + # @return A list or iterator containing all the matching elements. + # @defreturn list or iterator + + def getiterator(self, tag=None): + nodes = [] + if tag == "*": + tag = None + if tag is None or self.tag == tag: + nodes.append(self) + for node in self._children: + nodes.extend(node.getiterator(tag)) + return nodes + +# compatibility +_Element = _ElementInterface + +## +# Element factory. This function returns an object implementing the +# standard Element interface. The exact class or type of that object +# is implementation dependent, but it will always be compatible with +# the {@link #_ElementInterface} class in this module. +#

+# The element name, attribute names, and attribute values can be +# either 8-bit ASCII strings or Unicode strings. +# +# @param tag The element name. +# @param attrib An optional dictionary, containing element attributes. +# @param **extra Additional attributes, given as keyword arguments. +# @return An element instance. +# @defreturn Element + +def Element(tag, attrib={}, **extra): + attrib = attrib.copy() + attrib.update(extra) + return _ElementInterface(tag, attrib) + +## +# Subelement factory. This function creates an element instance, and +# appends it to an existing element. +#

+# The element name, attribute names, and attribute values can be +# either 8-bit ASCII strings or Unicode strings. +# +# @param parent The parent element. +# @param tag The subelement name. +# @param attrib An optional dictionary, containing element attributes. +# @param **extra Additional attributes, given as keyword arguments. +# @return An element instance. +# @defreturn Element + +def SubElement(parent, tag, attrib={}, **extra): + attrib = attrib.copy() + attrib.update(extra) + element = parent.makeelement(tag, attrib) + parent.append(element) + return element + +## +# Comment element factory. This factory function creates a special +# element that will be serialized as an XML comment. +#

+# The comment string can be either an 8-bit ASCII string or a Unicode +# string. +# +# @param text A string containing the comment string. +# @return An element instance, representing a comment. +# @defreturn Element + +def Comment(text=None): + element = Element(Comment) + element.text = text + return element + +## +# PI element factory. This factory function creates a special element +# that will be serialized as an XML processing instruction. +# +# @param target A string containing the PI target. +# @param text A string containing the PI contents, if any. +# @return An element instance, representing a PI. +# @defreturn Element + +def ProcessingInstruction(target, text=None): + element = Element(ProcessingInstruction) + element.text = target + if text: + element.text = element.text + " " + text + return element + +PI = ProcessingInstruction + +## +# QName wrapper. This can be used to wrap a QName attribute value, in +# order to get proper namespace handling on output. +# +# @param text A string containing the QName value, in the form {uri}local, +# or, if the tag argument is given, the URI part of a QName. +# @param tag Optional tag. If given, the first argument is interpreted as +# an URI, and this argument is interpreted as a local name. +# @return An opaque object, representing the QName. + +class QName: + def __init__(self, text_or_uri, tag=None): + if tag: + text_or_uri = "{%s}%s" % (text_or_uri, tag) + self.text = text_or_uri + def __str__(self): + return self.text + def __hash__(self): + return hash(self.text) + def __cmp__(self, other): + if isinstance(other, QName): + return cmp(self.text, other.text) + return cmp(self.text, other) + +## +# ElementTree wrapper class. This class represents an entire element +# hierarchy, and adds some extra support for serialization to and from +# standard XML. +# +# @param element Optional root element. +# @keyparam file Optional file handle or name. If given, the +# tree is initialized with the contents of this XML file. + +class ElementTree: + + def __init__(self, element=None, file=None): + assert element is None or iselement(element) + self._root = element # first node + if file: + self.parse(file) + + ## + # Gets the root element for this tree. + # + # @return An element instance. + # @defreturn Element + + def getroot(self): + return self._root + + ## + # Replaces the root element for this tree. This discards the + # current contents of the tree, and replaces it with the given + # element. Use with care. + # + # @param element An element instance. + + def _setroot(self, element): + assert iselement(element) + self._root = element + + ## + # Loads an external XML document into this element tree. + # + # @param source A file name or file object. + # @param parser An optional parser instance. If not given, the + # standard {@link XMLTreeBuilder} parser is used. + # @return The document root element. + # @defreturn Element + + def parse(self, source, parser=None): + if not hasattr(source, "read"): + source = open(source, "rb") + if not parser: + parser = XMLTreeBuilder() + while 1: + data = source.read(32768) + if not data: + break + parser.feed(data) + self._root = parser.close() + return self._root + + ## + # Creates a tree iterator for the root element. The iterator loops + # over all elements in this tree, in document order. + # + # @param tag What tags to look for (default is to return all elements) + # @return An iterator. + # @defreturn iterator + + def getiterator(self, tag=None): + assert self._root is not None + return self._root.getiterator(tag) + + ## + # Finds the first toplevel element with given tag. + # Same as getroot().find(path). + # + # @param path What element to look for. + # @return The first matching element, or None if no element was found. + # @defreturn Element or None + + def find(self, path): + assert self._root is not None + if path[:1] == "/": + path = "." + path + return self._root.find(path) + + ## + # Finds the element text for the first toplevel element with given + # tag. Same as getroot().findtext(path). + # + # @param path What toplevel element to look for. + # @param default What to return if the element was not found. + # @return The text content of the first matching element, or the + # default value no element was found. Note that if the element + # has is found, but has no text content, this method returns an + # empty string. + # @defreturn string + + def findtext(self, path, default=None): + assert self._root is not None + if path[:1] == "/": + path = "." + path + return self._root.findtext(path, default) + + ## + # Finds all toplevel elements with the given tag. + # Same as getroot().findall(path). + # + # @param path What element to look for. + # @return A list or iterator containing all matching elements, + # in document order. + # @defreturn list of Element instances + + def findall(self, path): + assert self._root is not None + if path[:1] == "/": + path = "." + path + return self._root.findall(path) + + ## + # Writes the element tree to a file, as XML. + # + # @param file A file name, or a file object opened for writing. + # @param encoding Optional output encoding (default is US-ASCII). + + def write(self, file, encoding="us-ascii"): + assert self._root is not None + if not hasattr(file, "write"): + file = open(file, "wb") + if not encoding: + encoding = "us-ascii" + elif encoding != "utf-8" and encoding != "us-ascii": + file.write("\n" % encoding) + self._write(file, self._root, encoding, {}) + + def _write(self, file, node, encoding, namespaces): + # write XML to file + tag = node.tag + if tag is Comment: + file.write("" % _escape_cdata(node.text, encoding)) + elif tag is ProcessingInstruction: + file.write("" % _escape_cdata(node.text, encoding)) + else: + items = node.items() + xmlns_items = [] # new namespaces in this scope + try: + if isinstance(tag, QName) or tag[:1] == "{": + tag, xmlns = fixtag(tag, namespaces) + if xmlns: xmlns_items.append(xmlns) + except TypeError: + _raise_serialization_error(tag) + file.write("<" + _encode(tag, encoding)) + if items or xmlns_items: + items.sort() # lexical order + for k, v in items: + try: + if isinstance(k, QName) or k[:1] == "{": + k, xmlns = fixtag(k, namespaces) + if xmlns: xmlns_items.append(xmlns) + except TypeError: + _raise_serialization_error(k) + try: + if isinstance(v, QName): + v, xmlns = fixtag(v, namespaces) + if xmlns: xmlns_items.append(xmlns) + except TypeError: + _raise_serialization_error(v) + file.write(" %s=\"%s\"" % (_encode(k, encoding), + _escape_attrib(v, encoding))) + for k, v in xmlns_items: + file.write(" %s=\"%s\"" % (_encode(k, encoding), + _escape_attrib(v, encoding))) + if node.text or len(node): + file.write(">") + if node.text: + file.write(_escape_cdata(node.text, encoding)) + for n in node: + self._write(file, n, encoding, namespaces) + file.write("") + else: + file.write(" />") + for k, v in xmlns_items: + del namespaces[v] + if node.tail: + file.write(_escape_cdata(node.tail, encoding)) + +# -------------------------------------------------------------------- +# helpers + +## +# Checks if an object appears to be a valid element object. +# +# @param An element instance. +# @return A true value if this is an element object. +# @defreturn flag + +def iselement(element): + # FIXME: not sure about this; might be a better idea to look + # for tag/attrib/text attributes + return isinstance(element, _ElementInterface) or hasattr(element, "tag") + +## +# Writes an element tree or element structure to sys.stdout. This +# function should be used for debugging only. +#

+# The exact output format is implementation dependent. In this +# version, it's written as an ordinary XML file. +# +# @param elem An element tree or an individual element. + +def dump(elem): + # debugging + if not isinstance(elem, ElementTree): + elem = ElementTree(elem) + elem.write(sys.stdout) + tail = elem.getroot().tail + if not tail or tail[-1] != "\n": + sys.stdout.write("\n") + +def _encode(s, encoding): + try: + return s.encode(encoding) + except AttributeError: + return s # 1.5.2: assume the string uses the right encoding + +if sys.version[:3] == "1.5": + _escape = re.compile(r"[&<>\"\x80-\xff]+") # 1.5.2 +else: + _escape = re.compile(eval(r'u"[&<>\"\u0080-\uffff]+"')) + +_escape_map = { + "&": "&", + "<": "<", + ">": ">", + '"': """, +} + +_namespace_map = { + # "well-known" namespace prefixes + "http://www.w3.org/XML/1998/namespace": "xml", + "http://www.w3.org/1999/xhtml": "html", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", + "http://schemas.xmlsoap.org/wsdl/": "wsdl", +} + +def _raise_serialization_error(text): + raise TypeError( + "cannot serialize %r (type %s)" % (text, type(text).__name__) + ) + +def _encode_entity(text, pattern=_escape): + # map reserved and non-ascii characters to numerical entities + def escape_entities(m, map=_escape_map): + out = [] + append = out.append + for char in m.group(): + text = map.get(char) + if text is None: + text = "&#%d;" % ord(char) + append(text) + return string.join(out, "") + try: + return _encode(pattern.sub(escape_entities, text), "ascii") + except TypeError: + _raise_serialization_error(text) + +# +# the following functions assume an ascii-compatible encoding +# (or "utf-16") + +def _escape_cdata(text, encoding=None, replace=string.replace): + # escape character data + try: + if encoding: + try: + text = _encode(text, encoding) + except UnicodeError: + return _encode_entity(text) + text = replace(text, "&", "&") + text = replace(text, "<", "<") + text = replace(text, ">", ">") + return text + except (TypeError, AttributeError): + _raise_serialization_error(text) + +def _escape_attrib(text, encoding=None, replace=string.replace): + # escape attribute value + try: + if encoding: + try: + text = _encode(text, encoding) + except UnicodeError: + return _encode_entity(text) + text = replace(text, "&", "&") + text = replace(text, "'", "'") # FIXME: overkill + text = replace(text, "\"", """) + text = replace(text, "<", "<") + text = replace(text, ">", ">") + return text + except (TypeError, AttributeError): + _raise_serialization_error(text) + +def fixtag(tag, namespaces): + # given a decorated tag (of the form {uri}tag), return prefixed + # tag and namespace declaration, if any + if isinstance(tag, QName): + tag = tag.text + namespace_uri, tag = string.split(tag[1:], "}", 1) + prefix = namespaces.get(namespace_uri) + if prefix is None: + prefix = _namespace_map.get(namespace_uri) + if prefix is None: + prefix = "ns%d" % len(namespaces) + namespaces[namespace_uri] = prefix + if prefix == "xml": + xmlns = None + else: + xmlns = ("xmlns:%s" % prefix, namespace_uri) + else: + xmlns = None + return "%s:%s" % (prefix, tag), xmlns + +## +# Parses an XML document into an element tree. +# +# @param source A filename or file object containing XML data. +# @param parser An optional parser instance. If not given, the +# standard {@link XMLTreeBuilder} parser is used. +# @return An ElementTree instance + +def parse(source, parser=None): + tree = ElementTree() + tree.parse(source, parser) + return tree + +## +# Parses an XML document into an element tree incrementally, and reports +# what's going on to the user. +# +# @param source A filename or file object containing XML data. +# @param events A list of events to report back. If omitted, only "end" +# events are reported. +# @return A (event, elem) iterator. + +class iterparse: + + def __init__(self, source, events=None): + if not hasattr(source, "read"): + source = open(source, "rb") + self._file = source + self._events = [] + self._index = 0 + self.root = self._root = None + self._parser = XMLTreeBuilder() + # wire up the parser for event reporting + parser = self._parser._parser + append = self._events.append + if events is None: + events = ["end"] + for event in events: + if event == "start": + try: + parser.ordered_attributes = 1 + parser.specified_attributes = 1 + def handler(tag, attrib_in, event=event, append=append, + start=self._parser._start_list): + append((event, start(tag, attrib_in))) + parser.StartElementHandler = handler + except AttributeError: + def handler(tag, attrib_in, event=event, append=append, + start=self._parser._start): + append((event, start(tag, attrib_in))) + parser.StartElementHandler = handler + elif event == "end": + def handler(tag, event=event, append=append, + end=self._parser._end): + append((event, end(tag))) + parser.EndElementHandler = handler + elif event == "start-ns": + def handler(prefix, uri, event=event, append=append): + try: + uri = _encode(uri, "ascii") + except UnicodeError: + pass + append((event, (prefix or "", uri))) + parser.StartNamespaceDeclHandler = handler + elif event == "end-ns": + def handler(prefix, event=event, append=append): + append((event, None)) + parser.EndNamespaceDeclHandler = handler + + def next(self): + while 1: + try: + item = self._events[self._index] + except IndexError: + if self._parser is None: + self.root = self._root + try: + raise StopIteration + except NameError: + raise IndexError + # load event buffer + del self._events[:] + self._index = 0 + data = self._file.read(16384) + if data: + self._parser.feed(data) + else: + self._root = self._parser.close() + self._parser = None + else: + self._index = self._index + 1 + return item + + try: + iter + def __iter__(self): + return self + except NameError: + def __getitem__(self, index): + return self.next() + +## +# Parses an XML document from a string constant. This function can +# be used to embed "XML literals" in Python code. +# +# @param source A string containing XML data. +# @return An Element instance. +# @defreturn Element + +def XML(text): + parser = XMLTreeBuilder() + parser.feed(text) + return parser.close() + +## +# Parses an XML document from a string constant, and also returns +# a dictionary which maps from element id:s to elements. +# +# @param source A string containing XML data. +# @return A tuple containing an Element instance and a dictionary. +# @defreturn (Element, dictionary) + +def XMLID(text): + parser = XMLTreeBuilder() + parser.feed(text) + tree = parser.close() + ids = {} + for elem in tree.getiterator(): + id = elem.get("id") + if id: + ids[id] = elem + return tree, ids + +## +# Parses an XML document from a string constant. Same as {@link #XML}. +# +# @def fromstring(text) +# @param source A string containing XML data. +# @return An Element instance. +# @defreturn Element + +fromstring = XML + +## +# Generates a string representation of an XML element, including all +# subelements. +# +# @param element An Element instance. +# @return An encoded string containing the XML data. +# @defreturn string + +def tostring(element, encoding=None): + class dummy: + pass + data = [] + file = dummy() + file.write = data.append + ElementTree(element).write(file, encoding) + return string.join(data, "") + +## +# Generic element structure builder. This builder converts a sequence +# of {@link #TreeBuilder.start}, {@link #TreeBuilder.data}, and {@link +# #TreeBuilder.end} method calls to a well-formed element structure. +#

+# You can use this class to build an element structure using a custom XML +# parser, or a parser for some other XML-like format. +# +# @param element_factory Optional element factory. This factory +# is called to create new Element instances, as necessary. + +class TreeBuilder: + + def __init__(self, element_factory=None): + self._data = [] # data collector + self._elem = [] # element stack + self._last = None # last element + self._tail = None # true if we're after an end tag + if element_factory is None: + element_factory = _ElementInterface + self._factory = element_factory + + ## + # Flushes the parser buffers, and returns the toplevel documen + # element. + # + # @return An Element instance. + # @defreturn Element + + def close(self): + assert len(self._elem) == 0, "missing end tags" + assert self._last != None, "missing toplevel element" + return self._last + + def _flush(self): + if self._data: + if self._last is not None: + text = string.join(self._data, "") + if self._tail: + assert self._last.tail is None, "internal error (tail)" + self._last.tail = text + else: + assert self._last.text is None, "internal error (text)" + self._last.text = text + self._data = [] + + ## + # Adds text to the current element. + # + # @param data A string. This should be either an 8-bit string + # containing ASCII text, or a Unicode string. + + def data(self, data): + self._data.append(data) + + ## + # Opens a new element. + # + # @param tag The element name. + # @param attrib A dictionary containing element attributes. + # @return The opened element. + # @defreturn Element + + def start(self, tag, attrs): + self._flush() + self._last = elem = self._factory(tag, attrs) + if self._elem: + self._elem[-1].append(elem) + self._elem.append(elem) + self._tail = 0 + return elem + + ## + # Closes the current element. + # + # @param tag The element name. + # @return The closed element. + # @defreturn Element + + def end(self, tag): + self._flush() + self._last = self._elem.pop() + assert self._last.tag == tag,\ + "end tag mismatch (expected %s, got %s)" % ( + self._last.tag, tag) + self._tail = 1 + return self._last + +## +# Element structure builder for XML source data, based on the +# expat parser. +# +# @keyparam target Target object. If omitted, the builder uses an +# instance of the standard {@link #TreeBuilder} class. +# @keyparam html Predefine HTML entities. This flag is not supported +# by the current implementation. +# @see #ElementTree +# @see #TreeBuilder + +class XMLTreeBuilder: + + def __init__(self, html=0, target=None): + try: + from xml.parsers import expat + except ImportError: + raise ImportError( + "No module named expat; use SimpleXMLTreeBuilder instead" + ) + self._parser = parser = expat.ParserCreate(None, "}") + if target is None: + target = TreeBuilder() + self._target = target + self._names = {} # name memo cache + # callbacks + parser.DefaultHandlerExpand = self._default + parser.StartElementHandler = self._start + parser.EndElementHandler = self._end + parser.CharacterDataHandler = self._data + # let expat do the buffering, if supported + try: + self._parser.buffer_text = 1 + except AttributeError: + pass + # use new-style attribute handling, if supported + try: + self._parser.ordered_attributes = 1 + self._parser.specified_attributes = 1 + parser.StartElementHandler = self._start_list + except AttributeError: + pass + encoding = None + if not parser.returns_unicode: + encoding = "utf-8" + # target.xml(encoding, None) + self._doctype = None + self.entity = {} + + def _fixtext(self, text): + # convert text string to ascii, if possible + try: + return _encode(text, "ascii") + except UnicodeError: + return text + + def _fixname(self, key): + # expand qname, and convert name string to ascii, if possible + try: + name = self._names[key] + except KeyError: + name = key + if "}" in name: + name = "{" + name + self._names[key] = name = self._fixtext(name) + return name + + def _start(self, tag, attrib_in): + fixname = self._fixname + tag = fixname(tag) + attrib = {} + for key, value in attrib_in.items(): + attrib[fixname(key)] = self._fixtext(value) + return self._target.start(tag, attrib) + + def _start_list(self, tag, attrib_in): + fixname = self._fixname + tag = fixname(tag) + attrib = {} + if attrib_in: + for i in range(0, len(attrib_in), 2): + attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1]) + return self._target.start(tag, attrib) + + def _data(self, text): + return self._target.data(self._fixtext(text)) + + def _end(self, tag): + return self._target.end(self._fixname(tag)) + + def _default(self, text): + prefix = text[:1] + if prefix == "&": + # deal with undefined entities + try: + self._target.data(self.entity[text[1:-1]]) + except KeyError: + from xml.parsers import expat + raise expat.error( + "undefined entity %s: line %d, column %d" % + (text, self._parser.ErrorLineNumber, + self._parser.ErrorColumnNumber) + ) + elif prefix == "<" and text[:9] == "": + self._doctype = None + return + text = string.strip(text) + if not text: + return + self._doctype.append(text) + n = len(self._doctype) + if n > 2: + type = self._doctype[1] + if type == "PUBLIC" and n == 4: + name, type, pubid, system = self._doctype + elif type == "SYSTEM" and n == 3: + name, type, system = self._doctype + pubid = None + else: + return + if pubid: + pubid = pubid[1:-1] + self.doctype(name, pubid, system[1:-1]) + self._doctype = None + + ## + # Handles a doctype declaration. + # + # @param name Doctype name. + # @param pubid Public identifier. + # @param system System identifier. + + def doctype(self, name, pubid, system): + pass + + ## + # Feeds data to the parser. + # + # @param data Encoded data. + + def feed(self, data): + self._parser.Parse(data, 0) + + ## + # Finishes feeding data to the parser. + # + # @return An element structure. + # @defreturn Element + + def close(self): + self._parser.Parse("", 1) # end of data + tree = self._target.close() + del self._target, self._parser # get rid of circular references + return tree diff --git a/resources/lib/elementtree/__init__.py b/resources/lib/elementtree/__init__.py new file mode 100644 index 0000000..ef8d14c --- /dev/null +++ b/resources/lib/elementtree/__init__.py @@ -0,0 +1,30 @@ +# $Id: __init__.py 1821 2004-06-03 16:57:49Z fredrik $ +# elementtree package + +# -------------------------------------------------------------------- +# The ElementTree toolkit is +# +# Copyright (c) 1999-2004 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- diff --git a/resources/lib/file.py b/resources/lib/file.py new file mode 100644 index 0000000..a169e28 --- /dev/null +++ b/resources/lib/file.py @@ -0,0 +1,6 @@ +class FakeFile: + def poll(self): + return False + + def communicate(self): + return '', '' \ No newline at end of file diff --git a/resources/lib/makemkv.py b/resources/lib/makemkv.py new file mode 100644 index 0000000..3217121 --- /dev/null +++ b/resources/lib/makemkv.py @@ -0,0 +1,148 @@ +import subprocess, settings, brlog, tempfile, os, threading, xbmcgui, urllib, re, xbmc +from functools import partial + +def killMkvOnPlaybackEnd(pid): + dialog = xbmcgui.Dialog() #@UndefinedVariable + dialog.ok("playback ended, about to kill %d"%(pid)) + + +class MakeMkvInteraction: + def __init__(self): + self.settings = settings.BluRaySettings() + self.progress = ProgressInfo() + self.progress.start() + self.log = brlog.BrLog('makemkvinteraction') + + def discList(self): + # First stop any old one + self.killMkv() + tmpf = tempfile.NamedTemporaryFile(delete=True) + progressFile = self.progress.startProgress() + sp = os.system(r'%s -r --cache=1 --progress=%s --messages=%s info disc:10' %(self.settings.mkvLocation, progressFile, tmpf.name)) + + tmpf = open(tmpf.name) + content = tmpf.read() + tmpf.close() + disclist = [x for x in content.splitlines() if x.startswith('DRV:')] + readableDiscs = list() + for item in disclist: + info = [x.replace('"','') for x in item[4:].split(',')] + if len(info[5]) > 0: + readableDiscs.append( (info[0],info[5])) + return readableDiscs + + def startStream(self, disc): + self.log.info("Starting stream on disc %s with local url %s" %(disc, self.settings.rootURL)) + # Then fork the makemkv process + # progressFile = self.progress.startProgress(times = 2) + return self.__runandregistershutdown('%s -r --cache=128 stream disc:%s' %(self.settings.mkvLocation, disc)) + + def startFileStream(self, choice): + type = '' + if re.search("BDMV.index.bdmv", choice) : + # Treat as file + type = 'file' + choice = choice[:-15] + elif re.search("BDMV.MovieObject.bdmv", choice) : + # Treat as file + type = 'file' + choice = choice[:-21] + else: + # Treat as iso + type = 'iso' + + # Check if the file is reachable through the filesystem, to prevent errors with smb:// shares etc. + if not os.path.exists(choice) : + dialog = xbmcgui.Dialog() #@UndefinedVariable + dialog.ok("Info", _(50073)) + return False + + return self.__runandregistershutdown('"%s" -r --cache=128 stream \'%s:%s\'' %(self.settings.mkvLocation, type, choice)) + + + def __runandregistershutdown(self, mkvStart): + result = self.__runmkvstream(mkvStart) + if result >= 0: + xbmc.Player().onPlayBackStopped(partial(killMkvOnPlaybackEnd, result)) + return True + else: + return False + + + def __runmkvstream(self, mkvStart): + self.log.info('Starting %s' %(mkvStart)) + # First stop any old one + self.killMkv() + timeSlept = 0 + proc = subprocess.Popen(mkvStart, shell=True) + # Then wait for the stream to come up + while True: + try: + urllib.urlretrieve(self.settings.rootURL) + return proc.pid + except IOError: + pass + if proc.poll() : + if proc.proc != 0 : + self.message(_(50070)) + return -1 + xbmc.sleep(1000) + timeSlept = timeSlept + 1 + if timeSlept > self.settings.waitTimeOut : + return -1 + + + def killMkv(self): + # Linux + try : + self.log.info('attempting linux kill of makemkvcon') #@UndefinedVariable + subprocess.call('killall -9 makemkvcon', shell=True) + self.log.info('Linux call successful') #@UndefinedVariable + except: + pass + + #Windows. + try : + self.log.info('attempting windows kill of makemkvcon') #@UndefinedVariable + subprocess.call('taskkill /F /IM makemkvcon.exe', shell=True) + self.log.info('Windows call successful') #@UndefinedVariable + except: + pass + + +class ProgressInfo(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.progressFile = None + self.showing = False + self.dialog = None + + def run(self): + while True: + if self.showing: + busy = True + maxprg = 1000 + current = 0 + + while self.showing: + line = self.progressFile.readline() + if line.startswith('PRGV:'): + progress = line[4:].split(',') + maxprg = int(progress[2]) + current= int(progress[1]) + self.dialog.update(int(float(current) / float(maxprg) * 1000.0) ) + if current >= maxprg: + self.times = self.times - 1 + + if self.times == 0: + self.dialog.close() + self.showing = False + + def startProgress(self, times = 1): + self.dialog = xbmcgui.DialogProgress() + self.dialog.create('XBMC', '', '') + self.progressFile = tempfile.NamedTemporaryFile() + self.times = times + self.showing = True + return self.progressFile.name + diff --git a/resources/lib/mkvparser.py b/resources/lib/mkvparser.py new file mode 100644 index 0000000..deeb001 --- /dev/null +++ b/resources/lib/mkvparser.py @@ -0,0 +1,56 @@ +import urllib, re, elementtree.ElementTree as ET + +class BrowseHandler: + def __init__(self) : + self.catchCharData = False + self.keyColumn = False + self.map = {} + self.lastKey = '' + self.lastVal = '' + self.titleMap = {} + + def start(self, url, title = 'none'): + # Initialize all locals + self.catchCharData = False + self.keyColumn = False + self.map[url] = {} + self.currMap = self.map[url] + self.lastKey = '' + self.lastVal = '' + filename, headers = urllib.urlretrieve(url) + del headers + + XML = ET.parse(filename) + elems = XML.getiterator(tag='{http://www.w3.org/1999/xhtml}td') + for each in elems: + self.keyColumn = not self.keyColumn + if self.keyColumn: + self.lastKey = each.text + else: + txt = '' + # Check for href: + refs = each.getiterator(tag='{http://www.w3.org/1999/xhtml}a') + for ref in refs: + txt = ref.get('href') + + if txt == '' : + txt = each.text + + self.currMap[self.lastKey] = txt + + # Now do some processing: + for k, v in self.map[url].iteritems() : + if k == 'titles': + # go straight ahead and parse some more: + self.start(v) + if re.search('title\d+', k) : + self.titleMap[k] = {} + self.start(v, k) + if title != 'none': + if k == 'duration': + self.titleMap[title]['duration'] = v + elif k == 'file0': + self.titleMap[title]['file'] = v + elif k == 'chaptercount': + self.titleMap[title]['chaptercount'] = v + diff --git a/resources/lib/settings.py b/resources/lib/settings.py new file mode 100644 index 0000000..db1eb41 --- /dev/null +++ b/resources/lib/settings.py @@ -0,0 +1,86 @@ +import xbmcplugin, xbmcaddon +import sys +import urllib +import brlog + +__scriptID__ = sys.modules[ "__main__" ].__scriptID__ + + +class BluRaySettings: + def __init__(self): + addon = xbmcaddon.Addon(__scriptID__) + self.log = brlog.BrLog('settings') + self.log.info('reading settings') #@UndefinedVariable + + params = self.getParams() + self.paramUrl = self.getParam(params, 'url') + self.paramName = self.getParam(params, "name") + self.paramMode = self.getIntParam(params, "mode") + self.autoPlay = self.getBoolParam(params, "autoplay") + self.disc = self.getParam(params, "disc") + self.local = self.paramMode <> 3 + self.ipAddress = addon.getSetting('ip_address') #@UndefinedVariable + self.portNumber = addon.getSetting('port_number') #@UndefinedVariable + if (self.local): + #Make sure local means 127.0.0.1 ... + self.ipAddress = '127.0.0.1' + else : + #Remote so use that portnumber + self.portNumber = addon.getSetting('remote_port_number') #@UndefinedVariable + + self.mkvLocation = addon.getSetting('mkvlocation') #@UndefinedVariable + self.rootURL = 'http://%s:%s/' % (self.ipAddress, self.portNumber) + self.waitTimeOut = int(addon.getSetting('wait_timeout')) #@UndefinedVariable + + # Sections: + self.enableDisc = addon.getSetting('support_disc') == "true" #@UndefinedVariable + self.enableFile = addon.getSetting('support_fileselect') == "true" #@UndefinedVariable + self.enableRemote = addon.getSetting('support_remote') == "true" #@UndefinedVariable + + def getParam(self, params, name): + try: + result = params[name] + result = urllib.unquote_plus(result) + return result + except: + return None + + def getIntParam (self, params, name): + try: + param = self.getParam(params,name) + self.log.debug(name + ' = ' + param) + return int(param) + except: + return None + + def getBoolParam (self, params, name): + try: + param = self.getParam(params,name) + self.log.debug(name + ' = ' + param) + return 'True' == param + except: + return None + + def getParams(self): + try: + param=[] + paramstring=sys.argv[2] + self.log.info('raw param string: ' + paramstring) + if len(paramstring)>=2: + params=sys.argv[2] + cleanedparams=params.replace('?','') + if (params[len(params)-1]=='/'): + params=params[0:len(params)-2] + pairsofparams=cleanedparams.split('&') + param={} + for i in range(len(pairsofparams)): + splitparams={} + splitparams=pairsofparams[i].split('=') + if (len(splitparams))==2: + param[splitparams[0]]=splitparams[1] + return param + except: + return [] + + def showSettings(self): + xbmcaddon.Addon(__scriptID__).openSettings(sys.argv[ 0 ]) #@UndefinedVariable diff --git a/resources/settings.xml b/resources/settings.xml new file mode 100644 index 0000000..4596004 --- /dev/null +++ b/resources/settings.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/service.py b/service.py new file mode 100644 index 0000000..7348819 --- /dev/null +++ b/service.py @@ -0,0 +1,41 @@ +import xbmc, xbmcgui, subprocess, os, sys, urllib, re +import xbmcplugin, xbmcaddon + + +__scriptID__ = "plugin.makemkvbluray" +__addon__ = xbmcaddon.Addon(__scriptID__) + +# Shared resources +BASE_RESOURCE_PATH = os.path.join( __addon__.getAddonInfo('path'), "resources" ) +sys.path.append( os.path.join( BASE_RESOURCE_PATH, "lib" ) ) + +import makemkv + +__language__ = __addon__.getLocalizedString +_ = sys.modules[ "__main__" ].__language__ + +import settings, file, mkvparser, brlog, makemkv + +_log = brlog.BrLog('tracker service') + +_log.info('Starting the BluRay tracker service') #@UndefinedVariable + +class MyPlayer(xbmc.Player): + def __init__(self): + xbmc.Player.__init__(self) + self.makemkv = makemkv.MakeMkvInteraction() + + def onPlayBackStopped(self): + _log.info('Playback stopped, trying to kill makemkv') + xbmc.executebuiltin('Notification("MakeMkv", "Playback ended, stopping makemkv")') + self.makemkv.killMkv() + + def onPlayBackStarted(self): + _log.info('Playback started') + + + +myPlayer = MyPlayer() + +while (not xbmc.abortRequested): + xbmc.sleep(4)