Commits

Anonymous committed 5893800

Initial import of supervisor.

Comments (0)

Files changed (10)

+syntax: glob
+*.pyc
+*.pyo
+*.egg-info
+*.egg-link

extensions/move.py

+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import logging
+from extensions import Extension
+
+class MoveExtension(Extension):
+    def __init__(self, manager, conf):
+        self._conf = {
+            'seed-ratio': 1.0,
+            'finished-dir': '~/download',
+        }
+        self._conf.update(conf)
+        session = manager.client.get_session()
+        self.download_dir = manager.download_dir
+    
+    def get_configuration(self):
+        return self._conf
+    
+    def event_time(self, manager, time):
+        for h, torrent in manager.torrents.iteritems():
+            if torrent.status == 'seeding' and torrent.ratio >= self._conf['seed-ratio']:
+                logging.info('Move files for "%s".' % (torrent.name))
+                files = torrent.files()
+                for torrent_file in files.itervalues():
+                    path_from = os.path.join(self.download_dir, torrent_file['name'])
+                    path_to = os.path.join(self._conf['finished-dir'], torrent_file['name'])
+                    logging.debug('Move file "%s" -> "%s".' % (path_from, path_to))
+                    os.renames(path_from, path_to)
+                manager.client.remove(h)

extensions/tvrss/manage.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import sys, os, os.path, datetime, cmd, shlex
+from optparse import OptionParser
+
+import sqlalchemy as sa
+import sqlalchemy.orm as orm
+
+from tvrss.model import *
+
+__author__    = u'Erik Svensson <erik.public@gmail.com>'
+__version__   = u'0.1'
+__copyright__ = u'Copyright (c) 2008 Erik Svensson'
+__license__   = u'MIT'
+
+class Manage(cmd.Cmd):
+    def __init__(self):
+        cmd.Cmd.__init__(self)
+        self.intro = u'Manage %s' % (__version__)
+        self.verbose = False
+        self.session = None
+        self.doc_leader = u'''TVRSS extension manager'''
+    
+    def set_database(self, db_path):
+        if os.path.exists(db_path):
+            db_path = os.path.expanduser(db_path)
+            engine = sa.create_engine('sqlite:///%s' % (db_path), echo=False)
+            self.session = orm.sessionmaker(bind=engine)()
+        else:
+            raise ValueError('Database file does not exists')
+    
+    def arg_tokenize(self, argstr):
+        return [unicode(token, 'utf-8') for token in shlex.split(argstr.encode('utf-8'))] or []
+    
+    def help_quit(self):
+        print(u'Exit to shell.\n')
+    
+    def do_quit(self, line):
+        sys.exit(0)
+    #Alias
+    do_exit = do_quit
+    help_exit = help_quit
+    do_EOF = do_quit
+    
+    def do_quality(self, line):
+        args = self.arg_tokenize(line)
+        if len(args) != 2:
+            print(u'To add/edit an quality you must provide a name and a score')
+            return
+        try:
+            score = int(args[1])
+        except ValueError:
+            print(u'Score must be a number')
+            return
+        quality = self.session.query(Quality).get(args[0].upper())
+        if quality:
+            quality.score = score
+        else:
+            quality = Quality(args[0].upper(), score)
+            self.session.add(quality)
+        self.session.commit()
+    
+    def do_download(self, line):
+        args = self.arg_tokenize(line)
+        if len(args) != 1:
+            print(u'To download an show you must provide a show name and nothing more')
+            return
+        show = self.session.query(Show).get(args[0].lower())
+        if show:
+            show.status = 'download'
+            self.session.commit()
+            print('Download "%s"' % (show.nice_name))
+        else:
+            print('Show not found')
+    
+    def do_ignore(self, line):
+        args = self.arg_tokenize(line)
+        if len(args) != 1:
+            print(u'To download an show you must provide a show name and nothing more')
+            return
+        show = self.session.query(Show).get(args[0].lower())
+        if show:
+            show.status = 'ignore'
+            self.session.commit()
+            print('Ignore "%s"' % (show.nice_name))
+        else:
+            print('Show not found')
+    
+    def do_list(self, line):
+        args = self.arg_tokenize(line)
+        q_filter = ['ignore', 'download', 'none', 'all']
+        what = 'all'
+        if len(args) == 1:
+            if args[0] in q_filter:
+                what = args[0]
+            else:
+                print(u'filter must be one of %s' % (', '.join(q_filter)))
+                return
+        if what == 'all':
+            shows = self.session.query(Show)
+        else:
+            shows = self.session.query(Show).filter(Show.status==what)
+        for show in shows:
+            print('%s, % 8s, "%s"' % (show.last_entry.isoformat(' ')[:19], show.status, show.nice_name, ))
+    
+
+def main():
+    if len(sys.argv) == 1:
+        return
+    
+    manage = Manage()
+    manage.set_database(sys.argv[1])
+    
+    args = sys.argv[2:]
+    
+    if len(args) > 0:
+        command = args[0]
+        if len(args) > 0:
+            command += u' '.join([u'"%s"' % arg for arg in args[1:]])
+        manage.onecmd(command)
+    else:
+        try:
+            manage.cmdloop()
+        except KeyboardInterrupt:
+            manage.do_quit(0)
+
+if __name__ == '__main__':
+    sys.exit(main())

extensions/tvrss/setup.py

+# -*- coding: utf-8 -*-
+
+from setuptools import setup
+
+__author__ = 'Erik Svensson'
+
+setup(
+    name='TvRSS',
+    version='1.0',
+    description='transmission-supervisor extension for automatically download tv episodes through tvrss.net rss feeds.',
+    author=__author__,
+    packages=['tvrss'],
+    entry_points='''
+    [transmission_supervisor.extension]
+    TVRSSExtension = tvrss:TVRSSExtension
+    '''
+)

extensions/tvrss/tvrss/__init__.py

+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import os.path, time, datetime, logging, re
+
+import sqlalchemy as sa
+import sqlalchemy.orm as orm
+import feedparser
+
+from model import metadata, Show, Episode, Quality
+
+QUALITIES = {
+    'DSR'    : 1, # Digital Stream/Satellite Rip
+    'DSRIP'  : 1, # same?
+    'PDTV'   : 1, # Pure Digital Television
+    'WS'     : 1, # Widescreen
+    'PROPER' : 1, # Proper, something was probably fixed
+    'REPACK' : 1, # Repack, something was probably fixed
+    'HDTV'   : 2, # High Definition Television
+    '720P'   : 3, # HDTV, 720 Progressive Lines
+}
+
+tvrss_field_parse = re.compile('\s*([^:]+)\s*:\s*(.+)\s*')
+
+class TVRSSExtension(object):
+    
+    def __init__(self, manager, configuration):
+        self._conf = {
+            'database': '~/.config/supervisor/tvrss.db',
+            'rss-url': 'http://tvrss.net/feed/unique/',
+            'rss-interval': 20.0
+        }
+        for k, v in configuration.iteritems():
+            self._conf[k] = v
+        db_path = os.path.expanduser(self._conf['database'])
+        engine = sa.create_engine('sqlite:///%s' % (db_path), echo=False)
+        self.session = orm.sessionmaker(bind=engine)()
+        if not os.path.exists(db_path):
+            self._initialize_database()
+        self.interval = datetime.timedelta(0, self._conf['rss-interval'])
+        self.run_time = datetime.datetime.now()
+    
+    def event_time(self, manager, time):
+        if time > self.run_time:
+            self.run_time = time + self.interval
+            logging.debug('checking rss')
+            self._process_feed_entries(self._parse_feed(self._conf['rss-url']), manager)
+    
+    def get_configuration(self):
+        return self._conf
+    
+    def _initialize_database(self):
+        metadata.create_all(self.session.get_bind(None))
+        # fill quality table
+        for name, score in QUALITIES.iteritems():
+            self.session.add(Quality(unicode(name), score))
+        self.session.commit()
+
+    def _parse_feed(self, url):
+        entries = feedparser.parse(url).entries
+        items = []
+        for entry in entries:
+            entry_date = datetime.datetime.fromtimestamp(time.mktime(entry.updated_parsed))
+            item = {'Entry Date': entry_date}
+            if 'title' in entry:
+                score = 0
+                start = entry.title.rfind('[')
+                end = entry.title.rfind(']')
+                if start and end >= 0:
+                    fields = entry.title[start+1:end].split('-')
+                    if len(fields) > 0:
+                        item['Group'] = fields[-1].strip()
+                        item['Quality'] = [field.strip() for field in fields[:-1]]
+                        for field in item['Quality']:
+                            if field not in QUALITIES:
+                                logging.warning('Unknown quality: %s in %s' % (field, entry.title))
+                            else:
+                                score += QUALITIES[field]
+                item['Quality Score'] = score
+                #item['Title'] = entry.title
+            if 'summary' and 'link' in entry:
+                fields = entry.summary.split(';')
+                item['Link'] = entry.link
+                for field in fields:
+                    match = tvrss_field_parse.match(field)
+                    if match:
+                        (key, value) = match.groups()
+                        if key == 'Episode Date':
+                            item[key] = datetime.datetime.strptime(value, '%Y-%m-%d')
+                        elif key in ['Episode', 'Season']:
+                            item[key] = int(value)
+                        elif value != 'n/a':
+                            item[key] = value
+                    else:
+                        logging.warning('no match: %s' % (field))
+                items.append(item)
+        return items
+
+    def _process_feed_entries(self, entries, manager):
+        for entry in entries:
+            if 'Show Name' in entry:
+                show = self.session.query(Show).get(entry['Show Name'].lower())
+                if not show:
+                    show = Show(entry['Show Name'])
+                    self.session.add(show)
+                    logging.info('Add show "%s".' % (show.nice_name))
+                show.last_entry = entry['Entry Date']
+                if show.status != 'download':
+                    if show.status != 'ignore':
+                        logging.info('Found show "%s", but did not download.' % (show.nice_name))
+                    continue
+                quality = []
+                group = u''
+                season = None
+                episode = None
+                date = None
+                if 'Season' and 'Episode' in entry:
+                    season = entry['Season']
+                    episode = entry['Episode']
+                date = entry['Episode Date'] if 'Episode Date' in entry else None
+                group = unicode(entry['Group']) if 'Group' in entry else None
+                if 'Quality' in entry:
+                    quality = [self.session.query(Quality).get(q) for q in entry['Quality']]
+                score = Quality.total_score(quality)
+                if date:
+                    episodes = self.session.query(Episode).filter(Episode.show==show).filter(Episode.date==date)
+                else:
+                    episodes = self.session.query(Episode).filter(Episode.show==show).filter(Episode.season==season).filter(Episode.episode==episode)
+                do_add = False
+                for item in episodes:
+                    if item.torrent_hash and score <= item.total_score():
+                        break
+                else:
+                    torrent = manager.client.add_url(entry['Link']).itervalues().next()
+                    item = Episode(show, season=season, episode=episode, date=date, quality=quality, group=group, torrent_hash=torrent.hashString)
+                    item.published = entry['Entry Date']
+                    self.session.add(item)
+                    logging.info('Add: %s' % (item))
+        self.session.commit()

extensions/tvrss/tvrss/model.py

+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import sqlalchemy as sa
+import sqlalchemy.orm as orm
+
+metadata = sa.MetaData()
+
+shows_table = sa.Table('shows', metadata,
+    sa.Column('name', sa.Unicode, primary_key=True),
+    sa.Column('nice_name', sa.Unicode),
+    sa.Column('status', sa.String, default='new'),
+    sa.Column('last_entry', sa.DateTime()),
+)
+
+qualities_table = sa.Table('qualities', metadata,
+    sa.Column('name', sa.Unicode, primary_key=True),
+    sa.Column('score', sa.Integer, default=0),
+)
+
+episodes_table = sa.Table('episodes', metadata,
+    sa.Column('ix', sa.Integer, primary_key=True),
+    sa.Column('show_name', sa.Integer, sa.ForeignKey('shows.name')),
+    sa.Column('name', sa.Unicode),
+    sa.Column('season', sa.Integer),
+    sa.Column('episode', sa.Integer),
+    sa.Column('date', sa.DateTime()),
+    sa.Column('group', sa.Unicode),
+    sa.Column('torrent_hash', sa.String),
+    sa.Column('published', sa.DateTime()),
+)
+
+episode_qualities = sa.Table('episode_qualities', metadata,
+    sa.Column('episode', sa.Integer, sa.ForeignKey('episodes.ix'), nullable=False),
+    sa.Column('quality', sa.Integer, sa.ForeignKey('qualities.name'), nullable=False),
+)
+
+class Show(object):
+    def __init__(self, name):
+        self.name = name.lower()
+        self.nice_name = name
+    
+    def __repr__(self):
+        return '<Show "%s">' % (self.nice_name)
+
+class Episode(object):
+    def __init__(self, show, **kwargs):
+        self.show = show
+        if 'season' and 'episode' in kwargs:
+            self.season = kwargs['season']
+            self.episode = kwargs['episode']
+        if 'date'  in kwargs:
+            self.date = kwargs['date']
+        if 'name'  in kwargs:
+            self.name = kwargs['name']
+        if 'group'  in kwargs:
+            self.group = kwargs['group']
+        if 'quality'  in kwargs:
+            quality = kwargs['quality']
+            qualities = []
+            if isinstance(quality, Quality):
+                qualities.append(quality)
+            if isinstance(quality, list):
+                qualities = quality
+            self.quality = qualities
+        if 'torrent_hash'  in kwargs:
+            self.torrent_hash = kwargs['torrent_hash']
+    
+    def __repr__(self):
+        out = '<Episode "%s"' % (self.show.nice_name)
+        if self.name:
+            out += ' "%s"' % (self.name)
+        if self.season and self.episode:
+            out += ' %dx%d' % (self.season, self.episode)
+        elif self.date:
+            out += ' %s' % (self.date)
+        out += ' %d' % (self.total_score())
+        if self.group:
+            out += ' %s' % (self.group)
+        out += '>'
+        return out
+    
+    def total_score(self):
+        return Quality.total_score(self.quality)
+
+class Quality(object):
+    def __init__(self, name, score=0):
+        self.name = name
+        self.score = score
+    
+    def __repr__(self):
+        return '<Quality "%s" %d>' % (self.name, self.score)
+    
+    @classmethod
+    def total_score(self, qualities):
+        score = 0
+        for q in qualities:
+            if isinstance(q, Quality):
+                score += q.score
+        return score
+
+orm.mapper(Show, shows_table)
+orm.mapper(Episode, episodes_table, properties={
+    'quality': orm.relation(Quality, secondary=episode_qualities),
+    'show': orm.relation(Show, backref=orm.backref('episodes')),
+})
+orm.mapper(Quality, qualities_table)
+
+#!wing
+#!version=3.0
+##################################################################
+# Wing IDE project file                                          #
+##################################################################
+[project attributes]
+proj.directory-list = [{'dirloc': loc('.'),
+                        'excludes': [u'.hg',
+                                     u'extensions/tvrss/tvrss/old_model.py'],
+                        'filter': 'All Source Files',
+                        'include_hidden': False,
+                        'recursive': True,
+                        'watch_for_changes': True}]
+proj.file-type = 'normal'
+[user attributes]
+debug.breakpoints = {loc('extensions/tvrss/tvrss/__init__.py'): {134: (0,
+        None,
+        1,
+        0)}}
+debug.err-values = {loc('extensions/tvrss/manage.py'): {},
+                    loc('supervisor/supervisor.py'): {}}
+debug.exceptions-ignored = {loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/feedparser-4.1-py2.5.egg//feedparser.py'): {233: True}}
+debug.run-args = {loc('extensions/tvrss/manage.py'): '~/.config/supervisor/t'\
+        'vrss.db list'}
+debug.var-col-widths = [0.42725880551301687,
+                        0.57274119448698313]
+guimgr.overall-gui-state = {'windowing-policy': 'combined-window',
+                            'windows': [{'name': 'RKSZtE7JW9J6rH7Qe39s50StxS'\
+        'Gpo8oU',
+        'size-state': 'maximized',
+        'type': 'dock',
+        'view': {'area': 'tall',
+                 'current_pages': [2],
+                 'notebook_display': 'normal',
+                 'notebook_percent': 0.19247648902821313,
+                 'override_title': None,
+                 'pagelist': [('debug-stack',
+                               'tall',
+                               1,
+                               {'codeline-mode': 'below'}),
+                              ('indent',
+                               'tall',
+                               2,
+                               {}),
+                              ('project',
+                               'tall',
+                               0,
+                               {'tree-state': {'file-sort-method': 'by name',
+        'list-files-first': 0,
+        'tree-states': {'deep': {'column-widths': [1.0],
+                                 'expanded-nodes': [(0,),
+        (0,
+         0),
+        (1,)],
+                                 'selected-nodes': [],
+                                 'top-node': (0,)}},
+        'tree-style': 'deep'}})],
+                 'primary_view_state': {'area': 'wide',
+        'current_pages': [4,
+                          0],
+        'notebook_display': 'normal',
+        'notebook_percent': 0.32111111111111112,
+        'override_title': None,
+        'pagelist': [('debug-stack',
+                      'wide',
+                      0,
+                      {'codeline-mode': 'below'}),
+                     ('debug-io',
+                      'wide',
+                      1,
+                      {'attrib-starts': [],
+                       'first-line': 73,
+                       'folded-linenos': [],
+                       'history': {},
+                       'sel-line': 135,
+                       'sel-line-start': 6902,
+                       'selection_end': 6902,
+                       'selection_start': 6902}),
+                     ('debug-exceptions',
+                      'wide',
+                      0,
+                      {}),
+                     ('python-shell',
+                      'wide',
+                      2,
+                      {'attrib-starts': [],
+                       'first-line': 0,
+                       'folded-linenos': [],
+                       'history': {None: ['import re\n',
+        "list_re = re.compile(r'\\((\\S+)\\) (\\w) (\\w+)')\n",
+        'li\n',
+        "list_re.match('(\\Unmarked) \"/\" \"Arkiv\"')\n",
+        "list_re = re.compile(r'\\S+')\n",
+        "list_re = re.compile(r'(\\S+) (\\S+) (\\S+)')\n",
+        'print(repr(match))\n',
+        'print(repr(match.group(2)))\n',
+        "list_re = re.compile(r'(\\S+) \\\"(\\w+)\\\" \\\"(\\w+)\\\"')\n",
+        "list_re = re.compile(r'(\\S+) \\\"(\\S+)\\\" \\\"(\\S+)\\\"')\n",
+        'match\n',
+        "list_re = re.compile(r'(\\S+) \\\"(\\S+)\\\" \\\"(\\w+)\\\"')\n",
+        'print(repr(match.group(3)))\n',
+        "list_re = re.compile(r'\\((\\S+)\\) \\\"(\\S+)\\\" \\\"(\\w+)\\\"')"\
+        "\n",
+        "list_re = re.compile(r'\\((\\S+)\\) \\\"(\\S+)\\\" \\\"(\\[^\\\"]+)"\
+        "\\\"')\n",
+        "list_re = re.compile(r'\\((\\S+)\\) \\\"(\\S+)\\\" \\\"(\\[^\"]+)\\"\
+        "\"')\n",
+        "match = list_re.match('(\\Unmarked) \"/\" \"Arkiv\"')\n",
+        'print(match.group(1), match.group(2), match.group(3))\n',
+        'import pysyck\n',
+        'import sys\n',
+        'file = sys.stdout\n',
+        'file.is_open()\n',
+        "print(file, 'hi')\n",
+        "print('hi', file)\n",
+        "file = codecs.open('temp', 'w', 'utf-8')\n",
+        "file = codecs.open('temp.txt', 'w', 'utf-8')\n",
+        "file.write(u'\xc3\x85\xc3\x84\xc3\x96\xc3\xa5\xc3\xa4\xc3\xb6')\n",
+        "file.write(u'\\x2191')\n",
+        'pwd\n',
+        "file = open('temp.txt', 'w')\n",
+        "file.write(u'\xc3\x85\xc3\x84\xc3\x96\xc3\xa5\xc3\xa4\xc3\xb6'.enco"\
+        "de('utf-8'))\n",
+        "file.write(u'\\x2191'.encode('utf-8'))\n",
+        'file.close()\n',
+        "u'\xc3\x85\xc3\x84\xc3\x96'.encode('utf-8')\n",
+        "u'bat'.encode('utf-8')\n",
+        "u'batau'.encode('utf-8')\n",
+        "u'bataux'.encode('utf-8')\n",
+        "u'bataux en \xc3\xa5'.decode('utf-8')\n",
+        "u'bataux en \xc3\xa5'.encode('ascii')\n",
+        "u'bataux en \xc3\xa5'.encode('utf-16')\n",
+        "u'bataux en \xc3\xa5'.encode('utf-8')\n",
+        'import codecs\n',
+        "''.decode('cp1251')\n",
+        "info = ('1', '2', '3')\n",
+        'info*\n',
+        '*info\n',
+        '**info\n',
+        'info\n',
+        'import os\n',
+        "filestat = os.stat('C:\\temp.txt')\n",
+        "filestat = os.stat('C:\\\\temp.txt')\n",
+        'import datetime\n',
+        'datetime.datetime.fromtimestamp(filestat.st_mtime)\n',
+        'datetime.datetime.fromtimestamp(filestat.st_mtime).isotime()\n',
+        "datetime.datetime.fromtimestamp(filestat.st_mtime).isoformat(' ')\n",
+        "file = open('C:\\\\temp.txt')\n",
+        'i = iter(file)\n',
+        'i\n',
+        'dt.second\n',
+        'dt.ctime()\n',
+        'dt = datetime.datetime.fromtimestamp(filestat.st_mtime)\n',
+        'filestat.st_mtime\n']},
+                       'sel-line': 2,
+                       'sel-line-start': 148,
+                       'selection_end': 152,
+                       'selection_start': 152}),
+                     ('interactive-search',
+                      'wide',
+                      0,
+                      {'fScope': {'fFileSetName': u'All Source Files',
+                                  'fLocation': None,
+                                  'fRecursive': True,
+                                  'fType': 'project-files'},
+                       'fSearchSpec': {'fEndPos': None,
+                                       'fIncludeLinenos': True,
+                                       'fInterpretBackslashes': False,
+                                       'fMatchCase': False,
+                                       'fOmitBinary': True,
+                                       'fRegexFlags': 46,
+                                       'fReplaceText': u'self._conf',
+                                       'fReverse': False,
+                                       'fSearchText': u'system',
+                                       'fStartPos': 0,
+                                       'fStyle': 'text',
+                                       'fWholeWords': False,
+                                       'fWrap': True},
+                       'fUIOptions': {'fAutoBackground': True,
+                                      'fFilePrefix': 'short-file',
+                                      'fFindAfterReplace': True,
+                                      'fInSelection': False,
+                                      'fIncremental': True,
+                                      'fReplaceOnDisk': False,
+                                      'fShowFirstMatch': False,
+                                      'fShowLineno': True,
+                                      'fShowReplaceWidgets': False},
+                       'replace-entry-expanded': False,
+                       'search-entry-expanded': False}),
+                     ('batch-search',
+                      'wide',
+                      0,
+                      {'fScope': {'fFileSetName': u'All Source Files',
+                                  'fLocation': u'../../../../..\\..\\ensure_pathProject Files',
+                                  'fRecursive': True,
+                                  'fType': 'project-files'},
+                       'fSearchSpec': {'fEndPos': None,
+                                       'fIncludeLinenos': True,
+                                       'fInterpretBackslashes': False,
+                                       'fMatchCase': False,
+                                       'fOmitBinary': True,
+                                       'fRegexFlags': 46,
+                                       'fReplaceText': u'',
+                                       'fReverse': False,
+                                       'fSearchText': u'system',
+                                       'fStartPos': 0,
+                                       'fStyle': 'text',
+                                       'fWholeWords': False,
+                                       'fWrap': True},
+                       'fUIOptions': {'fAutoBackground': True,
+                                      'fFilePrefix': 'short-file',
+                                      'fFindAfterReplace': True,
+                                      'fInSelection': False,
+                                      'fIncremental': True,
+                                      'fReplaceOnDisk': False,
+                                      'fShowFirstMatch': False,
+                                      'fShowLineno': True,
+                                      'fShowReplaceWidgets': False},
+                       'replace-entry-expanded': False,
+                       'search-entry-expanded': False}),
+                     ('debug-data',
+                      'wide',
+                      0,
+                      {})],
+        'primary_view_state': {'editor_states': {'bookmarks': ([(loc('supervisor/supervisor.py'),
+        {'attrib-starts': [('Supervisor',
+                            18),
+                           ('Supervisor.run',
+                            39)],
+         'first-line': 0,
+         'folded-linenos': [],
+         'sel-line': 51,
+         'sel-line-start': 2089,
+         'selection_end': 2089,
+         'selection_start': 2089},
+        1224714283.075099),
+        (loc('extensions/move.py'),
+         {'attrib-starts': [('MoveExtension',
+                             6),
+                            ('MoveExtension.get_configuration',
+                             16)],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 16,
+          'sel-line-start': 433,
+          'selection_end': 450,
+          'selection_start': 450},
+         1224714284.354717),
+        (loc('supervisor/supervisor.py'),
+         {'attrib-starts': [('Supervisor',
+                             18),
+                            ('Supervisor.run',
+                             39)],
+          'first-line': 136,
+          'folded-linenos': [],
+          'sel-line': 51,
+          'sel-line-start': 2089,
+          'selection_end': 2089,
+          'selection_start': 2089},
+         1224714314.404634),
+        (loc('supervisor/extensions.py'),
+         {'attrib-starts': [('ExtensionManager',
+                             23),
+                            ('ExtensionManager.get_configuration',
+                             29)],
+          'first-line': 12,
+          'folded-linenos': [],
+          'sel-line': 32,
+          'sel-line-start': 940,
+          'selection_end': 999,
+          'selection_start': 999},
+         1224714325.185689),
+        (loc('extensions/move.py'),
+         {'attrib-starts': [('MoveExtension',
+                             6),
+                            ('MoveExtension.__init__',
+                             7)],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 16,
+          'sel-line-start': 433,
+          'selection_end': 466,
+          'selection_start': 466},
+         1224714353.365792),
+        (loc('supervisor/supervisor.py'),
+         {'attrib-starts': [('Supervisor',
+                             18),
+                            ('Supervisor.__init__',
+                             19)],
+          'first-line': 16,
+          'folded-linenos': [],
+          'sel-line': 39,
+          'sel-line-start': 1577,
+          'selection_end': 1581,
+          'selection_start': 1581},
+         1224714371.904603),
+        (loc('extensions/move.py'),
+         {'attrib-starts': [('MoveExtension',
+                             6)],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 6,
+          'sel-line-start': 123,
+          'selection_end': 142,
+          'selection_start': 129},
+         1224714482.5042641),
+        (loc('extensions/tvrss/tvrss/model.py'),
+         {'attrib-starts': [],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 5,
+          'sel-line-start': 127,
+          'selection_end': 127,
+          'selection_start': 127},
+         1224714492.0916259),
+        (loc('extensions/tvrss/tvrss/__init__.py'),
+         {'attrib-starts': [('TVRSSExtension',
+                             24),
+                            ('TVRSSExtension.event_time',
+                             42)],
+          'first-line': 24,
+          'folded-linenos': [],
+          'sel-line': 47,
+          'sel-line-start': 1669,
+          'selection_end': 1673,
+          'selection_start': 1673},
+         1224714493.0793369),
+        (loc('supervisor/supervisor.py'),
+         {'attrib-starts': [('Supervisor',
+                             18),
+                            ('Supervisor.__init__',
+                             19)],
+          'first-line': 129,
+          'folded-linenos': [],
+          'sel-line': 39,
+          'sel-line-start': 1577,
+          'selection_end': 1581,
+          'selection_start': 1581},
+         1224714587.8533239),
+        (loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/simplejson-1.7.3-py2.5-linux-i686.egg//simplejson/__init__.py'),
+         {'attrib-starts': [('dump',
+                             107)],
+          'first-line': 145,
+          'folded-linenos': [],
+          'sel-line': 110,
+          'sel-line-start': 3105,
+          'selection_end': 3112,
+          'selection_start': 3112},
+         1224714601.3036799),
+        (loc('supervisor/supervisor.py'),
+         {'attrib-starts': [('Supervisor',
+                             18),
+                            ('Supervisor.__init__',
+                             19)],
+          'first-line': 129,
+          'folded-linenos': [],
+          'sel-line': 39,
+          'sel-line-start': 1577,
+          'selection_end': 1581,
+          'selection_start': 1581},
+         1224714722.507863),
+        (loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/simplejson-1.7.3-py2.5-linux-i686.egg//simplejson/__init__.py'),
+         {'attrib-starts': [('dump',
+                             107)],
+          'first-line': 145,
+          'folded-linenos': [],
+          'sel-line': 110,
+          'sel-line-start': 3105,
+          'selection_end': 3112,
+          'selection_start': 3112},
+         1224714724.0762551),
+        (loc('extensions/tvrss/tvrss/__init__.py'),
+         {'attrib-starts': [('TVRSSExtension',
+                             24),
+                            ('TVRSSExtension.__init__',
+                             26)],
+          'first-line': 24,
+          'folded-linenos': [],
+          'sel-line': 33,
+          'sel-line-start': 988,
+          'selection_end': 1017,
+          'selection_start': 1017},
+         1224714725.3237619),
+        (loc('extensions/move.py'),
+         {'attrib-starts': [('MoveExtension',
+                             6),
+                            ('MoveExtension.get_configuration',
+                             16)],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 18,
+          'sel-line-start': 492,
+          'selection_end': 496,
+          'selection_start': 496},
+         1224714731.973536),
+        (loc('extensions/tvrss/tvrss/model.py'),
+         {'attrib-starts': [],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 5,
+          'sel-line-start': 127,
+          'selection_end': 127,
+          'selection_start': 127},
+         1224714803.9533701),
+        (loc('supervisor/supervisor.py'),
+         {'attrib-starts': [('Supervisor',
+                             18),
+                            ('Supervisor.__init__',
+                             19)],
+          'first-line': 129,
+          'folded-linenos': [],
+          'sel-line': 39,
+          'sel-line-start': 1577,
+          'selection_end': 1581,
+          'selection_start': 1581},
+         1224714805.0978911),
+        [loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/simplejson-1.7.3-py2.5-linux-i686.egg//simplejson/__init__.py'),
+         {'attrib-starts': [('dump',
+                             107)],
+          'first-line': 145,
+          'folded-linenos': [],
+          'sel-line': 110,
+          'sel-line-start': 3105,
+          'selection_end': 3112,
+          'selection_start': 3112},
+         1224714805.7850111],
+        (loc('supervisor/supervisor.py'),
+         {'attrib-starts': [('Supervisor',
+                             18),
+                            ('Supervisor.__init__',
+                             19)],
+          'first-line': 129,
+          'folded-linenos': [],
+          'sel-line': 39,
+          'sel-line-start': 1577,
+          'selection_end': 1581,
+          'selection_start': 1581},
+         1224714807.5749669),
+        [loc('extensions/move.py'),
+         {'attrib-starts': [('MoveExtension',
+                             6),
+                            ('MoveExtension.get_configuration',
+                             16)],
+          'first-line': 0,
+          'folded-linenos': [],
+          'sel-line': 18,
+          'sel-line-start': 492,
+          'selection_end': 496,
+          'selection_start': 496},
+         1224714987.843693]],
+        19),
+        'current-loc': loc('extensions/move.py'),
+        'editor-states': {loc('extensions/move.py'): {'attrib-starts': [('Mo'\
+        'veExtension',
+        6),
+        ('MoveExtension.event_time',
+         19)],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 29,
+        'sel-line-start': 1164,
+        'selection_end': 1204,
+        'selection_start': 1204},
+                          loc('extensions/tvrss/manage.py'): {'attrib-starts': [(''\
+        'Manage',
+        17),
+        ('Manage.do_download',
+         64)],
+        'first-line': 63,
+        'folded-linenos': [],
+        'sel-line': 70,
+        'sel-line-start': 2202,
+        'selection_end': 2218,
+        'selection_start': 2218},
+                          loc('extensions/tvrss/tvrss/__init__.py'): {'attri'\
+        'b-starts': [('TVRSSExtension',
+                      24),
+                     ('TVRSSExtension.__init__',
+                      26)],
+        'first-line': 24,
+        'folded-linenos': [],
+        'sel-line': 33,
+        'sel-line-start': 988,
+        'selection_end': 1017,
+        'selection_start': 1017},
+                          loc('extensions/tvrss/tvrss/model.py'): {'attrib-s'\
+        'tarts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 5,
+        'sel-line-start': 127,
+        'selection_end': 127,
+        'selection_start': 127},
+                          loc('supervisor/extensions.py'): {'attrib-starts': [(''\
+        'ExtensionManager',
+        23),
+        ('ExtensionManager.get_configuration',
+         29)],
+        'first-line': 12,
+        'folded-linenos': [],
+        'sel-line': 32,
+        'sel-line-start': 940,
+        'selection_end': 999,
+        'selection_start': 999},
+                          loc('supervisor/supervisor.py'): {'attrib-starts': [(''\
+        'Supervisor',
+        18),
+        ('Supervisor.__init__',
+         19)],
+        'first-line': 129,
+        'folded-linenos': [],
+        'sel-line': 39,
+        'sel-line-start': 1577,
+        'selection_end': 1581,
+        'selection_start': 1581}},
+        'has-focus': True},
+                               'open_files': [u'extensions/tvrss/manage.py',
+        u'supervisor/extensions.py',
+        u'extensions/tvrss/tvrss/__init__.py',
+        u'extensions/tvrss/tvrss/model.py',
+        u'supervisor/supervisor.py',
+        u'extensions/move.py']},
+        'split_percents': {0: 0.50195160031225605},
+        'splits': 2,
+        'tab_location': 'top',
+        'user_data': {}},
+                 'split_percents': {},
+                 'splits': 1,
+                 'tab_location': 'left',
+                 'user_data': {}},
+        'window-alloc': (0,
+                         25,
+                         1553,
+                         961)}]}
+guimgr.recent-documents = [loc('extensions/move.py'),
+                           loc('supervisor/supervisor.py'),
+                           loc('extensions/tvrss/tvrss/model.py'),
+                           loc('extensions/tvrss/tvrss/__init__.py'),
+                           loc('supervisor/extensions.py'),
+                           loc('extensions/tvrss/manage.py')]
+guimgr.visual-state = {loc('../../../Python25/Lib/site-packages/sqlalchemy-0.3.10-py2.5.egg/sqlalchemy/schema.py'): {''\
+        'attrib-starts': [('MetaData',
+                           1102),
+                          ('MetaData.create_all',
+                           1216)],
+        'first-line': 1160,
+        'folded-linenos': [],
+        'sel-line': 1221,
+        'sel-line-start': 46458,
+        'selection_end': 46458,
+        'selection_start': 46458},
+                       loc('extensions/tvrss/manage.py'): {'attrib-starts': [(''\
+        'main',
+        12)],
+        'first-line': 6,
+        'folded-linenos': [],
+        'sel-line': 13,
+        'sel-line-start': 254,
+        'selection_end': 280,
+        'selection_start': 280},
+                       loc('extensions/tvrss/model.py'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 11,
+        'sel-line-start': 303,
+        'selection_end': 333,
+        'selection_start': 333},
+                       loc('extensions/tvrss/setup.py'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 13,
+        'sel-line-start': 318,
+        'selection_end': 357,
+        'selection_start': 357},
+                       loc('extensions/tvrss/tvrss.py'): {'attrib-starts': [(''\
+        'TvRSSExtension',
+        21),
+        ('TvRSSExtension.__init__',
+         24)],
+        'first-line': 8,
+        'folded-linenos': [],
+        'sel-line': 35,
+        'sel-line-start': 1245,
+        'selection_end': 1249,
+        'selection_start': 1249},
+                       loc('extensions/tvrss/tvrss/model.py'): {'attrib-star'\
+        'ts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 21,
+        'sel-line-start': 600,
+        'selection_end': 668,
+        'selection_start': 668},
+                       loc('extensions/tvrss/tvrss/old_model.py'): {'attrib-'\
+        'starts': [],
+        'first-line': 9,
+        'folded-linenos': [],
+        'sel-line': 20,
+        'sel-line-start': 558,
+        'selection_end': 582,
+        'selection_start': 573},
+                       loc('supervisor/extensions.py'): {'attrib-starts': [(''\
+        'ExtensionManager',
+        23),
+        ('ExtensionManager._find_extensions_files',
+         54)],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 55,
+        'sel-line-start': 2057,
+        'selection_end': 2098,
+        'selection_start': 2098},
+                       loc('supervisor/supervisor.py'): {'attrib-starts': [],
+        'first-line': 14,
+        'folded-linenos': [],
+        'sel-line': 13,
+        'sel-line-start': 349,
+        'selection_end': 400,
+        'selection_start': 400},
+                       loc('supervisor/system.py'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 0,
+        'sel-line-start': 0,
+        'selection_end': 0,
+        'selection_start': 0},
+                       loc('supervisor/utils.py'): {'attrib-starts': [('exec'\
+        'ute',
+        6)],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 13,
+        'sel-line-start': 366,
+        'selection_end': 380,
+        'selection_start': 380},
+                       loc('x-wingide-zip:/C:/Python25/Lib/site-packages/PyYAML-3.05-py2.5-win32.egg/yaml/__init__.py'): {''\
+        'attrib-starts': [('dump',
+                           171)],
+        'first-line': 158,
+        'folded-linenos': [],
+        'sel-line': 172,
+        'sel-line-start': 5169,
+        'selection_end': 5176,
+        'selection_start': 5176},
+                       loc('x-wingide-zip:/C:/Python25/Lib/site-packages/simplejson-1.9.1-py2.5-win32.egg/simplejson/__init__.py'): {''\
+        'attrib-starts': [('load',
+                           246)],
+        'first-line': 223,
+        'folded-linenos': [],
+        'sel-line': 246,
+        'sel-line-start': 9029,
+        'selection_end': 9037,
+        'selection_start': 9033},
+                       loc('x-wingide-zip:/C:/Python25/Lib/site-packages/simplejson-1.9.1-py2.5-win32.egg/simplejson/decoder.py'): {''\
+        'attrib-starts': [('JSONDecoder',
+                           245),
+                          ('JSONDecoder.__init__',
+                           278)],
+        'first-line': 305,
+        'folded-linenos': [],
+        'sel-line': 314,
+        'sel-line-start': 10182,
+        'selection_end': 10182,
+        'selection_start': 10182},
+                       loc('x-wingide-zip:/C:/Python25/Lib/site-packages/transmission-0.1-py2.5.egg/transmission/transmission.py'): {''\
+        'attrib-starts': [('Client',
+                           207),
+                          ('Client._request',
+                           225)],
+        'first-line': 217,
+        'folded-linenos': [],
+        'sel-line': 249,
+        'sel-line-start': 8501,
+        'selection_end': 8501,
+        'selection_start': 8501},
+                       loc('../transmission/model.py'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 8,
+        'sel-line-start': 154,
+        'selection_end': 158,
+        'selection_start': 158},
+                       loc('../transmission/tvrss.py'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'sel-line': 0,
+        'sel-line-start': 0,
+        'selection_end': 0,
+        'selection_start': 0},
+                       loc('../../../../usr/local/lib/python2.5/site-packages/SQLAlchemy-0.5.0rc1-py2.5.egg/sqlalchemy/databases/sqlite.py'): {''\
+        'attrib-starts': [('SQLiteDialect',
+                           206),
+                          ('SQLiteDialect.create_connect_args',
+                           242)],
+        'first-line': 221,
+        'folded-linenos': [],
+        'sel-line': 249,
+        'sel-line-start': 7963,
+        'selection_end': 7963,
+        'selection_start': 7963},
+                       loc('../../../../usr/local/lib/python2.5/site-packages/SQLAlchemy-0.5.0rc1-py2.5.egg/sqlalchemy/engine/base.py'): {''\
+        'attrib-starts': [('Connection',
+                           521),
+                          ('Connection._handle_dbapi_exception',
+                           928)],
+        'first-line': 919,
+        'folded-linenos': [],
+        'sel-line': 939,
+        'sel-line-start': 33320,
+        'selection_end': 33337,
+        'selection_start': 33337},
+                       loc('../../../../usr/local/lib/python2.5/site-packages/SQLAlchemy-0.5.0rc1-py2.5.egg/sqlalchemy/orm/session.py'): {''\
+        'attrib-starts': [('Session',
+                           449),
+                          ('Session._attach',
+                           1297)],
+        'first-line': 1282,
+        'folded-linenos': [],
+        'sel-line': 1310,
+        'sel-line-start': 49367,
+        'selection_end': 49367,
+        'selection_start': 49367},
+                       loc('../../../../usr/local/lib/python2.5/site-packages/SQLAlchemy-0.5.0rc1-py2.5.egg/sqlalchemy/schema.py'): {''\
+        'attrib-starts': [('_TableSingleton',
+                           89),
+                          ('_TableSingleton.__call__',
+                           92)],
+        'first-line': 79,
+        'folded-linenos': [],
+        'sel-line': 107,
+        'sel-line-start': 3820,
+        'selection_end': 3820,
+        'selection_start': 3820},
+                       loc('unknown:<untitled> #1'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'history': {},
+        'sel-line': 0,
+        'sel-line-start': 0,
+        'selection_end': 0,
+        'selection_start': 0},
+                       loc('unknown:<untitled> #2'): {'attrib-starts': [],
+        'first-line': 0,
+        'folded-linenos': [],
+        'history': {None: ['import bencode\n',
+                           'import SQLAlchemy\n',
+                           'import sqlalchemy as sa\n',
+                           'import sqlalchemy.orm as sa.orm\n',
+                           'import sqlalchemy.orm as sa_orm\n',
+                           'pwd\n',
+                           'import sys\n',
+                           'sys.getcwd()\n',
+                           'import os\n',
+                           'os.getcwdu()\n',
+                           'import sqlite3\n',
+                           'import sqlite3 as q\n',
+                           "db = q.connect('proj.db')\n",
+                           'cur = db.cursor()\n',
+                           "cur.execute('select * from files')\n",
+                           'print(row)\n',
+                           'row = cur.next()\n',
+                           "a = ('a', 'bcd', 'e', 'f')\n",
+                           'a[:-1]\n',
+                           'a[:-2]\n',
+                           "a = ('bad', 'a', 'bcd', 'e', 'f')\n",
+                           'a[:-4]\n',
+                           'a[:-3]\n',
+                           "'\\x1e'.join(a)\n",
+                           "line = '\\x1e'.join(a)\n",
+                           "line.split('\\x1e')\n",
+                           'import projo\n',
+                           'import sys, os, os.path\n',
+                           "os.chdir('dev')\n",
+                           'import projo, sqlalchemy, sqlalchemy.orm\n',
+                           'import TransmissionClient as tc\n',
+                           'c = tc.TransmissionClient()\n',
+                           'c = tc.TransmissionClient.TransmissionClient()\n',
+                           'c = tc()\n',
+                           'import TransmissionClient\n',
+                           'TransmissionClient.__name__\n',
+                           "help('TransmissionClient')\n",
+                           "help('TransmissionClient.TransmissionClient')\n",
+                           'TransmissionClient.TransmissionClient.Transmissi'\
+                           'onClient\n',
+                           'TransmissionClient.TransmissionClient\n',
+                           "help('TransmissionClient.TransmissionClient.Tran"\
+                           "smissionClient')\n",
+                           'from bencode.bencode import bdencode\n',
+                           "help('bencode')\n",
+                           'from TransmissionClient import TransmissionClien'\
+                           't\n',
+                           'TransmissionClient.TransmissionClient()\n',
+                           'from TransmissionClient.TransmissionClient impor'\
+                           't TransmissionClient\n']},
+        'sel-line': 0,
+        'sel-line-start': 0,
+        'selection_end': 0,
+        'selection_start': 0},
+                       loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/feedparser-4.1-py2.5.egg//feedparser.py'): {''\
+        'attrib-starts': [('FeedParserDict',
+                           171),
+                          ('FeedParserDict.__getattr__',
+                           226)],
+        'first-line': 204,
+        'folded-linenos': [],
+        'sel-line': 232,
+        'sel-line-start': 8854,
+        'selection_end': 8854,
+        'selection_start': 8854},
+                       loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/simplejson-1.7.3-py2.5-linux-i686.egg//simplejson/__init__.py'): {''\
+        'attrib-starts': [('dump',
+                           107)],
+        'first-line': 145,
+        'folded-linenos': [],
+        'sel-line': 110,
+        'sel-line-start': 3105,
+        'selection_end': 3112,
+        'selection_start': 3112},
+                       loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/simplejson-1.7.3-py2.5-linux-i686.egg//simplejson/decoder.py'): {''\
+        'attrib-starts': [('JSONDecoder',
+                           195),
+                          ('JSONDecoder.raw_decode',
+                           256)],
+        'first-line': 240,
+        'folded-linenos': [],
+        'sel-line': 269,
+        'sel-line-start': 8725,
+        'selection_end': 8725,
+        'selection_start': 8725},
+                       loc('x-wingide-zip:///usr/local/lib/python2.5/site-packages/transmission-0.1-py2.5.egg//transmission/transmission.py'): {''\
+        'attrib-starts': [('Session',
+                           165)],
+        'first-line': 162,
+        'folded-linenos': [],
+        'sel-line': 172,
+        'sel-line-start': 5588,
+        'selection_end': 5626,
+        'selection_start': 5614}}
+proj.default-encoding = 'utf_8'
+proj.env-vars = {None: ('default',
+                        ['']),
+                 loc('extensions/tvrss/manage.py'): ('project',
+        ['']),
+                 loc('extensions/tvrss/tvrss/__init__.py'): ('project',
+        ['']),
+                 loc('supervisor/extensions.py'): ('project',
+        ['']),
+                 loc('supervisor/supervisor.py'): ('project',
+        ['']),
+                 loc('x-wingide-zip:/C:/Python25/Lib/site-packages/simplejson-1.9.1-py2.5-win32.egg/simplejson/decoder.py'): (''\
+        'project',
+        ['']),
+                 loc('../../../../usr/local/lib/python2.5/site-packages/SQLAlchemy-0.5.0rc1-py2.5.egg/sqlalchemy/engine/base.py'): (''\
+        'project',
+        [''])}
+search.replace-history = [u'self._conf',
+                          u'._conf',
+                          u'self.session.']
+search.search-history = [u'system',
+                         u'yaml',
+                         u'torrent_events',
+                         u'prin',
+                         u'print',
+                         u'self.config',
+                         u'self.extensions',
+                         u'self.conf',
+                         u'event_names',
+                         u'.conf',
+                         u'session.',
+                         u'ensure_path',
+                         u'bibliographic_fields',
+                         u'utils']
+search.search-scope-history = [loc('../../ensure_pathProject Files')]

supervisor/extensions.py

+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import os, os.path, logging, imp
+import pkg_resources
+
+class Extension(object):
+    """
+    Extension, extend transmission-supervisor functionality.
+    
+    An extension must have a __init__ that takes self, manager, configuration.
+    
+    An extension gets event depending on which event functions that are
+    implemented. an event function is a function that has the name
+    event_<event name>.
+    
+    well, basic event functions are
+    
+     * event_time(self, manager, time)
+     * event_torrent(self, manager, torrent_event, torrent_hash)
+    """
+    pass
+
+class ExtensionManager(object):
+    def __init__(self, manager, configuration):
+        self._conf = configuration
+        self.extensions = {}
+        self._load_extensions(manager)
+    
+    def get_configuration(self):
+        configuration = self._conf
+        configuration['extensions'] = {}
+        for name, extension in self.extensions.iteritems():
+            if hasattr(extension, 'get_configuration'):
+                configuration['extensions'][name] = extension.get_configuration()
+            else:
+                configuration['extensions'][name] = {}
+        return configuration
+    
+    def _load_extensions(self, manager):
+        extension_dir = self._conf['dir']
+        extension_dir = os.path.expanduser(extension_dir)
+        classes = self._find_extensions_files(extension_dir)
+        classes.extend(self._find_extension_eggs(extension_dir))
+        cls_dict = dict([(cls.__name__, cls) for cls in classes])
+        logging.debug('Found extensions %r' % (cls_dict.keys()))
+        for name, ext_conf in self._conf['extensions'].iteritems():
+            if name in cls_dict:
+                try:
+                    obj = cls_dict[name](manager, ext_conf)
+                except TypeError:
+                    logging.warning('Failed to create extension "%s".' % (name))
+                    continue
+                logging.debug('Add extension %s' % (name))
+                self.extensions[name] = obj
+    
+    def _find_extensions_files(self, extension_dir):
+        files = os.listdir(extension_dir)
+        for filename in files:
+            if filename[-3:] == '.py':
+                name = filename[:-3]
+                module = imp.load_source(name, os.path.join(extension_dir, filename))
+        extensions = Extension.__subclasses__()
+        return extensions
+    
+    def _find_extension_eggs(self, extension_dir):
+        pkg_resources.working_set.add_entry(extension_dir)
+        pkg_env = pkg_resources.Environment([extension_dir])
+        extensions = []
+        for name in pkg_env:
+            egg = pkg_env[name][0]
+            egg.activate()
+            modules = []
+            entry_point = 'transmission_supervisor.extension'
+            for name in egg.get_entry_map(entry_point):
+                entry_point = egg.get_entry_info(entry_point, name)
+                extensions.append(entry_point.load())
+        return extensions

supervisor/supervisor.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import sys, os, os.path, time, datetime, logging
+from optparse import OptionParser
+import simplejson as json
+import transmission
+from utils import *
+from extensions import ExtensionManager
+
+__author__    = u'Erik Svensson <erik.public@gmail.com>'
+__version__   = u'0.1'
+__copyright__ = u'Copyright (c) 2008 Erik Svensson'
+__license__   = u'MIT'
+
+_CONFIGURATION_DIR = '~/.config/supervisor/supervisor.conf'
+
+class Supervisor(object):
+    def __init__(self, config_path, username=None, password=None, verbose=False):
+        self.extmgr = None
+        self.torrents = {}
+        self.torrent_events = []
+        self._username = username
+        self._password = password
+        
+        self._load_config(config_path)
+        self._configure_logging(verbose)
+        if self._conf['transmission']['manage-process'] and len(self.find_daemons()) == 0:
+            self.start_daemon(username, password)
+        self.client = transmission.Client(port=self._conf['transmission']['port'], user=username, password=password, verbose=verbose)
+        try:
+            session = self.client.get_session()
+        except transmission.TransmissionError, exception:
+            logging.error('Session query failed: %s port: %r.' % (exception, self._conf['transmission']['port']))
+            raise
+        self.download_dir = os.path.abspath(os.path.expanduser(session.download_dir))
+        self.extmgr = ExtensionManager(self, self._conf['extensions'])
+        self._save_config(config_path)
+    
+    def run(self):
+        try:
+            while(True):
+                timer = time.clock()
+                try:
+                    torrents = self.client.info()
+                except transmission.TransmissionError, exception:
+                    logging.error('Info query failed: %s port %r' % (exception, self._conf['transmission']['port']))
+                    raise
+                for tid, torrent in torrents.iteritems():
+                    self.torrents[torrent.hashString] = torrent
+                thetime = datetime.datetime.now()
+                for name, extension in self.extmgr.extensions.iteritems():
+                    if hasattr(extension, 'event_time'):
+                        logging.debug('Run %s.event_time' % (name))
+                        extension.event_time(self, thetime)
+                # do the stuff
+                for event in self.torrent_events:
+                    for name, extension in self.extmgr.extensions.iteritems():
+                        if hasattr(extension, 'event_torrent'):
+                            logging.debug('Run %s.event_torrent' % (name))
+                            extension.event_torrent(self, event[0], event[1])
+                for torrent_hash, torrent in self.torrents.iteritems():
+                    if torrent.id not in torrents:
+                        del self.torrents[torrent_hash]
+                # sleep some
+                time.sleep(self._conf['supervisor']['poll-interval'] - (time.clock() - timer))
+        except KeyboardInterrupt:
+            pass
+        try:
+            self._save_config()
+        except IOError, exception:
+            logging.warning('Failed to save configuration: %s' % exception)
+    
+    def add_torrent_event(self, torrent_event, torrent_hash):
+        if torrent_event not in ['added', 'downloaded', 'stopped', 'done', 'removed']:
+            logging.warning('trying to add unknown torrent event "%s"' % (torrent_event))
+            return
+        if torrent_hash not in self.torrents:
+            logging.warning('trying to add unknown torrent hash "%s"' % (torrent_hash))
+            return
+        self.torrent_events.append((torrent_event, torrent_hash))
+    
+    def find_daemons(self):
+        daemons = []
+        for process in process_list():
+            if process[1][-19:] == 'transmission-daemon':
+                daemons.append(process[0])
+        return daemons
+    
+    def start_daemon(self, username=None, password=None):
+        command = 'transmission-daemon'
+        if username and password:
+            command += ' --auth --username ' + username + ' --password ' + password
+        else:
+            command += ' --no-auth'
+        if execute(command) == None:
+            logging.error('Failed to start daemon.')
+            sys.exit(0)
+        time.sleep(0.5) # wait for daemon to get ready
+    
+    def _load_config(self, path):
+        config = {
+            'supervisor': {
+                'poll-interval': 10.0,
+                'log-dir': '/tmp',
+                'log-level': 'warning',
+                },
+            'extensions': {
+                'dir': os.path.expanduser(os.path.join(os.path.dirname(__file__), 'extensions')),
+                'extensions': {},
+                },
+            'transmission': {
+                'port': transmission.DEFAULT_PORT,
+                'manage-process': True,
+                },
+        }
+        if not path:
+            path = _CONFIGURATION_DIR
+        path = os.path.expanduser(path)
+        try:
+            config_file = open(path, 'r')
+        except IOError:
+            pass
+        else:
+            try:
+                file_config = json.load(config_file)
+            except ValueError:
+                pass
+            else:
+                config = self._recursive_config_update(config, file_config)
+            config_file.close()
+        self._conf = config
+    
+    def _recursive_config_update(self, conf_to, conf_from):
+        for k, v in conf_from.iteritems():
+            if k in conf_to and isinstance(v, dict):
+                self._recursive_config_update(conf_to[k], conf_from[k])
+            else:
+                conf_to[k] = v
+        return conf_to
+    
+    def _save_config(self, path = None):
+        if not path:
+            path = _CONFIGURATION_DIR
+        path = os.path.expanduser(path)
+        ensure_path('/', path)
+        config_file = open(path, 'w')
+        if self.extmgr:
+            self._conf['extensions'] = self.extmgr.get_configuration()
+        json.dump(self._conf, config_file, indent=2)
+        config_file.close()
+    
+    def _configure_logging(self, verbose):
+        format = '%(asctime)s %(levelname)s %(message)s'
+        log_path = os.path.join(os.path.expanduser(self._conf['supervisor']['log-dir']), 'supervisor.log')
+        levels = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR}
+        log_level = logging.WARNING
+        if verbose:
+            log_level = logging.DEBUG
+        elif self._conf['supervisor']['log-level'] in levels:
+            log_level = levels[self._conf['supervisor']['log-level']]
+        logging.basicConfig(level=log_level, filename=log_path, format=format)
+        console = logging.StreamHandler()
+        console.setLevel(log_level)
+        console.setFormatter(logging.Formatter(format))
+        logging.getLogger('').addHandler(console)
+
+def main():
+    # parse args
+    parser = OptionParser(version='Supervisor %s' % (__version__), usage='%prog [options]')
+    parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='Enable verbose output.')
+    parser.add_option('-c', '--config', type='string', dest='config', help='Configuration file.', metavar='<file>')
+    parser.add_option('-u', '--username', type='string', dest='username', help='Transmission daemon username.', metavar='<username>')
+    parser.add_option('-p', '--password', type='string', dest='password', help='Transmission daemon password.', metavar='<password>')
+    opts = parser.parse_args()[0]
+    verbose = False
+    if opts.verbose:
+        verbose = opts.verbose
+    supervisor = Supervisor(config_path=opts.config, username=opts.username, password=opts.password, verbose=verbose)
+    supervisor.run()
+
+if __name__ == '__main__':
+    main()

supervisor/utils.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 2008-10, Erik Svensson <erik.public@gmail.com>
+
+import os, os.path, subprocess, re, signal
+
+def execute(command):
+    """Execute a shell command"""
+    try:
+        p = subprocess.Popen(command, shell=True, bufsize=-1, stdout=subprocess.PIPE)
+        r = p.wait()
+    except (OSError, ValueError):
+        return None
+    if r == 0:
+        return p.stdout.read()
+    else:
+        return None
+
+def process_list():
+    """
+    List active processes the UNIX way. Returns a list of tuples with:
+    (<process id>, <command>, <arguments>). <command> may include the path to the command.
+    """
+    procs = []
+    re_procs = re.compile('^\s*(\d+)\s+(\S+)\s*(.*)')
+    out = execute('ps -A -o pid= -o command=')
+    for line in out.splitlines():
+        match = re_procs.match(line)
+        if match:
+            fields = match.groups()
+            # add process as a tuple of pid, command, arguments
+            procs.append((int(fields[0]), fields[1], fields[2]))
+        else:
+            raise ValueError('BAD: \"' + line + '\"')
+    return procs
+
+def ensure_dir(base, path):
+    """Tries to create the missing directories on the joined path base + path where base must exists."""
+    if not os.path.exists(base):
+        raise ValueError()
+    if os.path.exists(os.path.join(base, path)):
+        return
+    dirs = []
+    while not os.path.exists(os.path.join(base, path)):
+        (path, name) = os.path.split(path)
+        dirs.append(name)
+    dirs.reverse()
+    for name in dirs:
+        path = os.path.join(path, name)
+        os.mkdir(path)
+
+def ensure_path(base, path):
+    """Tries to create the missing directories on the joined path base + path where base must exists."""
+    ensure_dir(base, os.path.dirname(path))