Advertisement
Guest User

mtproxy

a guest
Aug 27th, 2011
655
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.06 KB | None | 0 0
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (C) 2009 James Bellenger <james@forwardcamegrendel.org>
  4. #
  5. # Licensed under GPL version 2. For details of what this means, write to
  6. #   Free Software Foundation, Inc.
  7. #   51 Franklin St, Fifth Floor,
  8. #   Boston, MA 02110-1301, USA
  9. #
  10. #  CHANGELOG
  11. #  0.2.2
  12. #  - Changed API params to make it compatible with 0.12.1
  13. #  - Added --rar option to resolve rar files to its content using unrar binary
  14. #
  15. #  0.2.1 by James Bellenger
  16.  
  17. VERSION = '0.2.2'
  18.  
  19. USAGE = """
  20. This program turns interesting inotify events into HTTP requests that can trigger a
  21. mediatomb server to add or remove media.
  22.  
  23. Required Parameters:
  24.    -h, --host          Hostname of mediatomb daemon. Mediatomb should be running with webui enabled and
  25.                        authorization disabled (the default configuration). This parameter is required.
  26.    -w, --watch         A directory to watch. Try to avoid specifying overlapping directories
  27.                        This parameter is required and can be specified multiple times.
  28. Optional Parameters:
  29.    -p, --port          Port to look for mediatomb web service on. Defaults to 49152.
  30.    -t, --translate     A simple regex for translating paths on the file server to a path on the mediatomb machine.
  31.                        If this machine sees a file at /export/video/movie.mp4, and mediatomb sees the same file at
  32.                        /mnt/video/movie.mp4, you'll want to specify a value of 'export,mnt'. Can be specified
  33.                        multiple times.
  34.    -r, --rar           Resolve rar filenames to content filenames (Deleting won't work)
  35.    -l, --log           Log output to a given file.
  36.    -v, --verbose       Print extra debugging information.
  37.    -V, --version       Print version number and exit.
  38.    --help              Print this message and exit.
  39. """.strip()
  40.  
  41. import getopt
  42. import os
  43. import pyinotify
  44. import re
  45. import sys
  46. import urllib
  47. import logging
  48. import subprocess as sub
  49. from xml.dom import minidom
  50.  
  51. class Logger:
  52.     log = None
  53.     formatter = logging.Formatter('[%(asctime)s %(levelname)s] %(message)s')
  54.     handler = None
  55.     _instance = None
  56.  
  57.     def __init__(self):
  58.         self.log = logging.getLogger(sys.argv[0])
  59.         self.handler = logging.StreamHandler()
  60.         self.handler.setFormatter(self.formatter)
  61.         self.log.addHandler(self.handler)
  62.         self.log.setLevel(logging.INFO)
  63.  
  64.         for level in 'debug', 'info', 'warn', 'error', 'exception', 'fatal', 'setLevel':
  65.             setattr(self, level, getattr(self.log, level))
  66.  
  67.     def logtofile(self, fname):
  68.         self.log.removeHandler(self.handler)
  69.         self.handler = logging.FileHandler(fname)
  70.         self.handler.setFormatter(self.formatter)
  71.         self.log.addHandler(self.handler)
  72.  
  73.     def _getinstance(cls):
  74.         if not cls._instance:
  75.             cls._instance = Logger()
  76.         return cls._instance
  77.  
  78.     getinstance = classmethod(_getinstance)
  79.  
  80. log = Logger.getinstance()
  81.  
  82. class Mediatomb:
  83.     SID_PATTERN = re.compile(r'sid="(.*?)"', re.MULTILINE)
  84.     REQUEST_FORMAT = "http://%s:%d/content/interface?req_type=%s&return_type=xml"
  85.  
  86.     host = None
  87.     port = None
  88.     _pcdir = None
  89.     _sid = None
  90.  
  91.     def __init__(self, host, port):
  92.         self.host = host
  93.         self.port = port
  94.         self._initsession()
  95.  
  96.     def _initsession(self):
  97.         opener = urllib.URLopener()
  98.         response = self._request('auth', sid='null', action='get_sid')
  99.         if response:
  100.             match = re.search(Mediatomb.SID_PATTERN, response)
  101.             if match:
  102.                 self._sid = match.group(1)
  103.                 log.info('Created session ' + self._sid)
  104.                 return self._sid
  105.         else:
  106.             raise Exception('Could not initialize mediatomb session')
  107.  
  108.     def _tohex(self, string):
  109.         return ''.join(["%02X" % ord(x) for x in string]).strip().lower()
  110.  
  111.     def _request(self, req_type, retries=1, **kwargs):
  112.         if 'sid' not in kwargs:
  113.             if not self._sid:
  114.                 self._initsession()
  115.             kwargs['sid'] = self._sid
  116.  
  117.         opener = urllib.URLopener()
  118.         url = Mediatomb.REQUEST_FORMAT % (self.host, self.port, req_type)
  119.         extras = '&'.join([ '%s=%s' % i for i in kwargs.items() ])
  120.         if extras:
  121.             url = '&'.join((url, extras))
  122.  
  123.         log.debug('opening url: ' + url)
  124.         try:
  125.             response = opener.open(url)
  126.             if response:
  127.                 body = response.read()
  128.                 log.debug('Response: ' + body)
  129.                 if '<redirect>/</redirect>' in body and retries > 0:
  130.                     log.info('Expiring stale sid')
  131.                     kwargs.pop('sid')
  132.                     self._sid = None
  133.                     return self._request(req_type, retries-1, **kwargs)
  134.                 else:
  135.                     return body
  136.             else:
  137.                 log.error('No response received')
  138.         except Exception, e:
  139.             log.exception('Could not connect to mediatomb at %s:%d. Are you sure it\'s running?' % (self.host, self.port), e)
  140.  
  141.  
  142.     def _initpcdir(self):
  143.         containers = self.containers(0)
  144.         if containers:
  145.             for id, name in containers.items():
  146.                 if name == 'PC Directory':
  147.                     log.debug('loaded PC Directory id %s' % id)
  148.                     self._pcdir = id
  149.                     return
  150.    
  151.     def containers(self, parent):
  152.         response = self._request('containers', parent_id=parent)
  153.         if response:
  154.             dom = minidom.parseString(response)
  155.  
  156.             containers = dict()
  157.             for container in dom.getElementsByTagName('container'):
  158.                 name = container.firstChild.data
  159.                 id = container.getAttribute('id')
  160.                 if name and id:
  161.                     containers[id] = name
  162.  
  163.             return containers
  164.  
  165.     def items(self, parent):
  166.         response = self._request('items', parent_id=parent, start=0, count=0)
  167.         if response:
  168.             dom = minidom.parseString(response)
  169.  
  170.             items = dict()
  171.             for item in dom.getElementsByTagName('item'):
  172.                 id = item.getAttribute('id')
  173.                 titles = item.getElementsByTagName('title')
  174.                 if titles:
  175.                     items[id] = titles[-1].firstChild.data
  176.             return items
  177.  
  178.     def getmtid(self, path):
  179.         if self._pcdir is None:
  180.             self._initpcdir()
  181.         if self._pcdir is None:
  182.             log.error('Is PC Directory disabled in mediatomb?')
  183.             return
  184.  
  185.         path = path.split(os.path.sep)
  186.         if path[0] == '':
  187.             path.pop(0)
  188.         fname = path.pop()
  189.  
  190.         parent = self._pcdir
  191.         for p in path:
  192.             for id, name in self.containers(parent).items():
  193.                 if name == p:
  194.                     parent = id
  195.                     break
  196.  
  197.         if parent != self._pcdir:
  198.             for id, name in self.items(parent).items():
  199.                 if name == fname:
  200.                     return id
  201.  
  202.     def removefile(self, path):
  203.         mtid = self.getmtid(path)
  204.         if mtid:
  205.             response = self._request('remove', object_id = mtid)
  206.             if response:
  207.                 if '<root success="1"/>' in response:
  208.                     log.info('removed ' + path)
  209.                 else:
  210.                     log.error('Could not understand server response when removing %s. Response:\n%s' % (path, response))
  211.  
  212.     def addfile(self, path):
  213.         id = self._tohex(path)
  214.         response = self._request('add', object_id = self._tohex(path))
  215.  
  216.         if response:
  217.             if '<root success="1"/>' in response or 'Adding:' in response:
  218.                 log.info('Added file %s' % path)
  219.             else:
  220.                 log.error('Could not understand server response when adding %s. Response:\n%s' % (path, response))
  221.  
  222. class Monitor:
  223.     EVENT_MASK = pyinotify.IN_CREATE |\
  224.                  pyinotify.IN_MOVED_TO |\
  225.                  pyinotify.IN_MOVED_FROM |\
  226.                  pyinotify.IN_DELETE
  227.  
  228.     _notify = None
  229.     _mediatomb = None
  230.     _watchmgr = None
  231.     _notifycheck = None
  232.     _notifystop = None
  233.     _eventcheck = None
  234.     translations = None
  235.    
  236.    
  237.     def __init__(self, mediatomb, translations):
  238.         # there are different apis between pyinotify 2.5 and 2.4
  239.         if 'WatchManager' in dir(pyinotify):
  240.             # 2.5
  241.             self._watchmgr = pyinotify.WatchManager()
  242.             self._notify = pyinotify.Notifier(self._watchmgr)
  243.             self._notifycheck = self._notify.check_events
  244.             self._notifystop = self._notify.stop
  245.         else:
  246.             # 2.4
  247.             self._notify = pyinotify.SimpleINotify()
  248.             self._watchmgr = self._notify
  249.             self._notifycheck = self._notify.event_check
  250.             self._notifystop = self._notify.close
  251.  
  252.         self._mediatomb = mediatomb
  253.         self.translations = translations
  254.         self.resolverar = resolverar
  255.  
  256.     def add_watch(self, path):
  257.         log.info('Adding watch for %s (this can take a while)... ' % path)
  258.         self._watchmgr.add_watch(path, Monitor.EVENT_MASK, self._onevent, True)
  259.         log.info('Watch for %s added.' % path)
  260.  
  261.     def process(self):
  262.         self._notify.process_events()
  263.         if self._notifycheck(1):
  264.             self._notify.read_events()
  265.  
  266.     def close(self):
  267.         self._notifystop()
  268.  
  269.     def _onevent(self, event):
  270.         # IN_IGNORED generated when removing a directory
  271.         if not event.mask & pyinotify.IN_IGNORED:
  272.             if event.path:
  273.                 fname = event.path;
  274.  
  275.                 if event.name:
  276.                     if self.resolverar:
  277.                         fname = os.path.join(fname, event.name)
  278.                         p = sub.Popen(['unrar', 'lb', fname], stdout=sub.PIPE,stderr=sub.PIPE)
  279.                         fname = p.stdout.readline().rstrip()
  280.                         if fname:
  281.                             fname = os.path.join(event.path, fname)
  282.                         else:
  283.                             fname = os.path.join(event.path, event.name)
  284.                     else:
  285.                         fname = os.path.join(fname, event.name)
  286.                     if event.name.startswith('.'):
  287.                         log.debug('Ignoring hidden file ' + fname)
  288.                         return
  289.  
  290.                 for translation in self.translations:
  291.                     fname = re.sub(translation[0], translation[1], fname)
  292.  
  293.                 # if we're processing a directory, just add it it to our watch list
  294.                 if event.mask & pyinotify.IN_ISDIR:
  295.                     if event.mask & pyinotify.IN_CREATE:
  296.                         self._watchmgr.add_watch(fname, Monitor.EVENT_MASK, self._onevent, True)
  297.                         log.info('Added new watch directory ' + fname)
  298.                 elif event.mask & (pyinotify.IN_DELETE | pyinotify.IN_MOVED_FROM):
  299.                     self._mediatomb.removefile(fname)
  300.                 else:
  301.                     self._mediatomb.addfile(fname)
  302.  
  303. if __name__ == '__main__':
  304.     watches = list()
  305.     host = None
  306.     port = 49152
  307.     translations = list()
  308.     resolverar = 0
  309.  
  310.     if len(sys.argv) > 1:
  311.         opts, args = getopt.gnu_getopt(sys.argv[1:], 'l:t:r:h:p:e:w:vV', ('log=', 'translate=', 'rar', 'host=', 'port=', 'help', 'watch=', 'verbose', 'version'))
  312.  
  313.         for opt, arg in opts:
  314.             if opt in ('-l', '--log'):
  315.                 log.logtofile(arg)
  316.             if opt == '-t' or opt == '--translate':
  317.                 targs = arg.split(',')
  318.                 if len(targs) != 2:
  319.                     print 'Cannot understand translation argument %s' % arg
  320.                     sys.exit(1)
  321.                 else:
  322.                     translations.append(targs)
  323.             if opt in ('-r', '--rar'):
  324.                 resolverar = 1
  325.             if opt in ('-h', '--host'):
  326.                 host = arg
  327.             if opt in ('-p', '--port'):
  328.                 port = int(arg)
  329.             if opt in ('-w', '--watch'):
  330.                 watches.append(arg)
  331.             if opt in ('-v', '--verbose'):
  332.                 log.setLevel(logging.DEBUG)
  333.             if opt in ('-V', '--version'):
  334.                 print 'mtproxy version %s' % VERSION
  335.                 sys.exit(0)
  336.             if opt == '--help':
  337.                 print USAGE
  338.                 sys.exit(0)
  339.     else:
  340.         print USAGE
  341.         sys.exit(1)
  342.  
  343.     if not host:
  344.         print 'Mediatomb host not specified. Exiting'
  345.         sys.exit(1)
  346.     if not port:
  347.         print 'Mediatomb port not specified. Exiting'
  348.         sys.exit(1)
  349.     if not watches:
  350.         print 'No watches specified. Exiting'
  351.         sys.exit(1)
  352.  
  353.     log.info('Starting mtproxy')
  354.     mediatomb = Mediatomb(host, port)
  355.     monitor = Monitor(mediatomb, translations)
  356.  
  357.     for watch in watches:
  358.         monitor.add_watch(watch)
  359.  
  360.     while True:
  361.         try:
  362.             monitor.process()
  363.         except KeyboardInterrupt:
  364.             log.info('Closing')
  365.             monitor.close()
  366.             break
  367.         except Exception, err:
  368.             print err
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement