#!/usr/bin/env python # # Copyright (C) 2009 James Bellenger # # Licensed under GPL version 2. For details of what this means, write to # Free Software Foundation, Inc. # 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301, USA VERSION = '0.2.1' USAGE = """ This program turns interesting inotify events into HTTP requests that can trigger a mediatomb server to add or remove media. Required Parameters: -h, --host Hostname of mediatomb daemon. Mediatomb should be running with webui enabled and authorization disabled (the default configuration). This parameter is required. -w, --watch A directory to watch. Try to avoid specifying overlapping directories This parameter is required and can be specified multiple times. Optional Parameters: -p, --port Port to look for mediatomb web service on. Defaults to 49152. -t, --translate A simple regex for translating paths on the file server to a path on the mediatomb machine. If this machine sees a file at /export/video/movie.mp4, and mediatomb sees the same file at /mnt/video/movie.mp4, you'll want to specify a value of 'export,mnt'. Can be specified multiple times. -l, --log Log output to a given file. -v, --verbose Print extra debugging information. -V, --version Print version number and exit. --help Print this message and exit. """.strip() import getopt import os import pyinotify import re import sys import urllib import logging from xml.dom import minidom class Logger: log = None formatter = logging.Formatter('[%(asctime)s %(levelname)s] %(message)s') handler = None _instance = None def __init__(self): self.log = logging.getLogger(sys.argv[0]) self.handler = logging.StreamHandler() self.handler.setFormatter(self.formatter) self.log.addHandler(self.handler) self.log.setLevel(logging.INFO) for level in 'debug', 'info', 'warn', 'error', 'exception', 'fatal', 'setLevel': setattr(self, level, getattr(self.log, level)) def logtofile(self, fname): self.log.removeHandler(self.handler) self.handler = logging.FileHandler(fname) self.handler.setFormatter(self.formatter) self.log.addHandler(self.handler) def _getinstance(cls): if not cls._instance: cls._instance = Logger() return cls._instance getinstance = classmethod(_getinstance) log = Logger.getinstance() class Mediatomb: SID_PATTERN = re.compile(r'sid="(.*?)"', re.MULTILINE) REQUEST_FORMAT = "http://%s:%d/content/interface?req_type=%s" host = None port = None _pcdir = None _sid = None def __init__(self, host, port): self.host = host self.port = port self._initsession() def _initsession(self): opener = urllib.URLopener() response = self._request('auth', 0, sid='null', checkSID=1) if response: match = re.search(Mediatomb.SID_PATTERN, response) if match: self._sid = match.group(1) log.info('Created session ' + self._sid) return self._sid else: raise Exception('Could not initialize mediatomb session') def _tohex(self, string): return ''.join(["%02X" % ord(x) for x in string]).strip().lower() def _request(self, req_type, retries=1, **kwargs): if 'sid' not in kwargs: if not self._sid: self._initsession() kwargs['sid'] = self._sid opener = urllib.URLopener() url = Mediatomb.REQUEST_FORMAT % (self.host, self.port, req_type) extras = '&'.join([ '%s=%s' % i for i in kwargs.items() ]) if extras: url = '&'.join((url, extras)) log.debug('opening url: ' + url) try: response = opener.open(url) if response: body = response.read() log.debug('Response: ' + body) if '/' in body and retries > 0: log.info('Expiring stale sid') kwargs.pop('sid') self._sid = None return self._request(req_type, retries-1, **kwargs) else: return body else: log.error('No response received') except Exception, e: log.exception('Could not connect to mediatomb at %s:%d. Are you sure it\'s running?' % (self.host, self.port), e) def _initpcdir(self): containers = self.containers(0) if containers: for id, name in containers.items(): if name == 'PC Directory': log.debug('loaded PC Directory id %s' % id) self._pcdir = id return def containers(self, parent): response = self._request('containers', parent_id=parent) if response: dom = minidom.parseString(response) containers = dict() for container in dom.getElementsByTagName('container'): name = container.firstChild.data id = container.getAttribute('id') if name and id: containers[id] = name return containers def items(self, parent): response = self._request('items', parent_id=parent, start=0, count=0) if response: dom = minidom.parseString(response) items = dict() for item in dom.getElementsByTagName('item'): id = item.getAttribute('id') titles = item.getElementsByTagName('title') if titles: items[id] = titles[-1].firstChild.data return items def getmtid(self, path): if self._pcdir is None: self._initpcdir() if self._pcdir is None: log.error('Is PC Directory disabled in mediatomb?') return path = path.split(os.path.sep) if path[0] == '': path.pop(0) fname = path.pop() parent = self._pcdir for p in path: for id, name in self.containers(parent).items(): if name == p: parent = id break if parent != self._pcdir: for id, name in self.items(parent).items(): if name == fname: return id def removefile(self, path): mtid = self.getmtid(path) if mtid: response = self._request('remove', object_id = mtid) if response: if '' in response: log.info('removed ' + path) else: log.error('Could not understand server response when removing %s. Response:\n%s' % (path, response)) def addfile(self, path): id = self._tohex(path) response = self._request('add', object_id = self._tohex(path)) if response: if '' in response or '