Browse Source

black reformatting

boyska 4 years ago
parent
commit
b956076fc5
10 changed files with 475 additions and 375 deletions
  1. 73 41
      server/cli.py
  2. 19 16
      server/config_manager.py
  3. 55 45
      server/db.py
  4. 23 20
      server/default_config.py
  5. 44 46
      server/forge.py
  6. 5 3
      server/maint.py
  7. 13 7
      server/processqueue.py
  8. 191 157
      server/server.py
  9. 47 38
      server/test_forge.py
  10. 5 2
      setup.py

+ 73 - 41
server/cli.py

@@ -3,8 +3,9 @@ import sys
 from argparse import ArgumentParser, Action
 from datetime import datetime
 import logging
+
 logging.basicConfig(stream=sys.stdout)
-logger = logging.getLogger('cli')
+logger = logging.getLogger("cli")
 
 
 CWD = os.getcwd()
@@ -16,19 +17,18 @@ from . import server
 
 
 def pre_check_permissions():
-
     def is_writable(d):
         return os.access(d, os.W_OK)
 
-    if is_writable(get_config()['AUDIO_INPUT']):
-        yield "Audio input '%s' writable" % get_config()['AUDIO_INPUT']
-    if not os.access(get_config()['AUDIO_INPUT'], os.R_OK):
-        yield "Audio input '%s' unreadable" % get_config()['AUDIO_INPUT']
+    if is_writable(get_config()["AUDIO_INPUT"]):
+        yield "Audio input '%s' writable" % get_config()["AUDIO_INPUT"]
+    if not os.access(get_config()["AUDIO_INPUT"], os.R_OK):
+        yield "Audio input '%s' unreadable" % get_config()["AUDIO_INPUT"]
         sys.exit(10)
     if is_writable(os.getcwd()):
         yield "Code writable"
-    if not is_writable(get_config()['AUDIO_OUTPUT']):
-        yield "Audio output '%s' not writable" % get_config()['AUDIO_OUTPUT']
+    if not is_writable(get_config()["AUDIO_OUTPUT"]):
+        yield "Audio output '%s' not writable" % get_config()["AUDIO_OUTPUT"]
         logger.critical("Aborting")
         sys.exit(10)
 
@@ -37,13 +37,15 @@ def pre_check_user():
     if os.geteuid() == 0:
         yield "You're running as root; this is dangerous"
 
+
 def pre_check_ffmpeg():
-    path = get_config()['FFMPEG_PATH']
-    if not path.startswith('/'):
+    path = get_config()["FFMPEG_PATH"]
+    if not path.startswith("/"):
         yield "FFMPEG_PATH is not absolute: %s" % path
         from subprocess import check_output
+
         try:
-            check_output([path, '-version'])
+            check_output([path, "-version"])
         except OSError:
             yield "FFMPEG not found as " + path
     else:
@@ -54,7 +56,7 @@ def pre_check_ffmpeg():
 class DateTimeAction(Action):
     def __call__(self, parser, namespace, values, option_string=None):
         if len(values) == 15 or len(values) == 13:
-            parsed_val = datetime.strptime(values, '%Y%m%d-%H%M%S')
+            parsed_val = datetime.strptime(values, "%Y%m%d-%H%M%S")
         else:
             raise ValueError("'%s' is not a valid datetime" % values)
         setattr(namespace, self.dest, parsed_val)
@@ -62,15 +64,14 @@ class DateTimeAction(Action):
 
 def common_pre():
     prechecks = [pre_check_user, pre_check_permissions, pre_check_ffmpeg]
-    configs = ['default_config.py']
-    if 'TECHREC_CONFIG' in os.environ:
-        for conf in os.environ['TECHREC_CONFIG'].split(':'):
+    configs = ["default_config.py"]
+    if "TECHREC_CONFIG" in os.environ:
+        for conf in os.environ["TECHREC_CONFIG"].split(":"):
             if not conf:
                 continue
             path = os.path.realpath(conf)
             if not os.path.exists(path):
-                logger.warn("Configuration file '%s' does not exist; skipping"
-                            % path)
+                logger.warn("Configuration file '%s' does not exist; skipping" % path)
                 continue
             configs.append(path)
     os.chdir(os.path.dirname(os.path.realpath(__file__)))
@@ -81,36 +82,65 @@ def common_pre():
         for warn in check():
             logger.warn(warn)
 
+
 def main():
-    parser = ArgumentParser(description='creates mp3 from live recordings')
-    parser.add_argument('--verbose', '-v', action='count',
-                        default=0,
-                        help='Increase verbosity; can be used multiple times')
-    parser.add_argument('--pretend', '-p', action='store_true', default=False,
-                        help='Only pretend; no real action will be done')
-    sub = parser.add_subparsers(title='main subcommands',
-                                description='valid subcommands')
-    serve_p = sub.add_parser('serve', help="Start an HTTP server")
+    parser = ArgumentParser(description="creates mp3 from live recordings")
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="count",
+        default=0,
+        help="Increase verbosity; can be used multiple times",
+    )
+    parser.add_argument(
+        "--pretend",
+        "-p",
+        action="store_true",
+        default=False,
+        help="Only pretend; no real action will be done",
+    )
+    sub = parser.add_subparsers(
+        title="main subcommands", description="valid subcommands"
+    )
+    serve_p = sub.add_parser("serve", help="Start an HTTP server")
     serve_p.set_defaults(func=server.main_cmd)
-    forge_p = sub.add_parser('forge', help="Create an audio file")
-    forge_p.add_argument('starttime', metavar='START',
-            help='Start time, espressed as 19450425_1200 (%%Y%%m%%d-%%H%%M%%S)',
-            action=DateTimeAction)
-    forge_p.add_argument('endtime', metavar='END',
-            help='End time, espressed as 19450425_1200 (%%Y%%m%%d-%%H%%M%%S)',
-            action=DateTimeAction)
-    forge_p.add_argument('-o', metavar='OUTFILE', dest='outfile',
-                         default='out.mp3', help='Path of the output mp3')
+    forge_p = sub.add_parser("forge", help="Create an audio file")
+    forge_p.add_argument(
+        "starttime",
+        metavar="START",
+        help="Start time, espressed as 19450425_1200 (%%Y%%m%%d-%%H%%M%%S)",
+        action=DateTimeAction,
+    )
+    forge_p.add_argument(
+        "endtime",
+        metavar="END",
+        help="End time, espressed as 19450425_1200 (%%Y%%m%%d-%%H%%M%%S)",
+        action=DateTimeAction,
+    )
+    forge_p.add_argument(
+        "-o",
+        metavar="OUTFILE",
+        dest="outfile",
+        default="out.mp3",
+        help="Path of the output mp3",
+    )
     forge_p.set_defaults(func=forge.main_cmd)
 
-    cleanold_p = sub.add_parser('cleanold', help="Remove old files from DB",
-            description="Will remove oldfiles with no filename from DB")
-    cleanold_p.add_argument('-t', metavar='MINAGE', dest='minage',
-                         default='14', type=int,
-                         help='Minimum age (in days) for removal')
+    cleanold_p = sub.add_parser(
+        "cleanold",
+        help="Remove old files from DB",
+        description="Will remove oldfiles with no filename from DB",
+    )
+    cleanold_p.add_argument(
+        "-t",
+        metavar="MINAGE",
+        dest="minage",
+        default="14",
+        type=int,
+        help="Minimum age (in days) for removal",
+    )
     cleanold_p.set_defaults(func=maint.cleanold_cmd)
 
-
     options = parser.parse_args()
     options.cwd = CWD
     if options.verbose < 1:
@@ -123,5 +153,7 @@ def main():
             logging.info("giving verbose flag >2 times is useless")
     common_pre()
     options.func(options)
+
+
 if __name__ == "__main__":
     main()

+ 19 - 16
server/config_manager.py

@@ -10,6 +10,8 @@ def get_config():
     if get_config.instance is None:
         get_config.instance = Config(os.getcwd())
     return get_config.instance
+
+
 get_config.instance = None
 
 
@@ -78,11 +80,12 @@ class Config(dict):
         if not rv:
             if silent:
                 return False
-            raise RuntimeError('The environment variable %r is not set '
-                               'and as such configuration could not be '
-                               'loaded.  Set this variable and make it '
-                               'point to a configuration file' %
-                               variable_name)
+            raise RuntimeError(
+                "The environment variable %r is not set "
+                "and as such configuration could not be "
+                "loaded.  Set this variable and make it "
+                "point to a configuration file" % variable_name
+            )
         return self.from_pyfile(rv, silent=silent)
 
     def from_pyfile(self, filename, silent=False):
@@ -100,15 +103,15 @@ class Config(dict):
            `silent` parameter.
         """
         filename = os.path.join(self.root_path, filename)
-        d = imp.new_module('config')
+        d = imp.new_module("config")
         d.__file__ = filename
         try:
             with open(filename) as config_file:
-                exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
+                exec(compile(config_file.read(), filename, "exec"), d.__dict__)
         except IOError as e:
             if silent and e.errno in (errno.ENOENT, errno.EISDIR):
                 return False
-            e.strerror = 'Unable to load configuration file (%s)' % e.strerror
+            e.strerror = "Unable to load configuration file (%s)" % e.strerror
             raise
         self.from_object(d)
         return True
@@ -143,7 +146,7 @@ class Config(dict):
                 self[key] = getattr(obj, key)
 
     def __repr__(self):
-        return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
+        return "<%s %s>" % (self.__class__.__name__, dict.__repr__(self))
 
 
 def import_string(import_name, silent=False):
@@ -159,27 +162,27 @@ def import_string(import_name, silent=False):
                    `None` is returned instead.
     :return: imported object
     """
-    #XXX: py3 review needed
+    # XXX: py3 review needed
     assert isinstance(import_name, string_types)
     # force the import name to automatically convert to strings
     import_name = str(import_name)
     try:
-        if ':' in import_name:
-            module, obj = import_name.split(':', 1)
-        elif '.' in import_name:
-            module, obj = import_name.rsplit('.', 1)
+        if ":" in import_name:
+            module, obj = import_name.split(":", 1)
+        elif "." in import_name:
+            module, obj = import_name.rsplit(".", 1)
         else:
             return __import__(import_name)
         # __import__ is not able to handle unicode strings in the fromlist
         # if the module is a package
         if sys.version_info[0] == 2 and isinstance(obj, unicode):
-            obj = obj.encode('utf-8')
+            obj = obj.encode("utf-8")
         try:
             return getattr(__import__(module, None, None, [obj]), obj)
         except (ImportError, AttributeError):
             # support importing modules not yet set up by the parent module
             # (or package for that matter)
-            modname = module + '.' + obj
+            modname = module + "." + obj
             __import__(modname)
             return sys.modules[modname]
     except ImportError as e:

+ 55 - 45
server/db.py

@@ -1,6 +1,6 @@
-'''
+"""
 This module contains DB logic
-'''
+"""
 from __future__ import print_function
 import logging
 from datetime import datetime, timedelta
@@ -20,33 +20,38 @@ Base = declarative_base()
 
 
 class Rec(Base):
-    '''Entry on the DB'''
-    __tablename__ = 'rec'
+    """Entry on the DB"""
+
+    __tablename__ = "rec"
     id = Column(Integer, primary_key=True)
     name = Column(String, nullable=True)
     starttime = Column(DateTime, nullable=True)
     endtime = Column(DateTime, nullable=True)
     filename = Column(String, nullable=True)
 
-    def __init__(self, name="", starttime=None, endtime=None,
-                 filename=None):
+    def __init__(self, name="", starttime=None, endtime=None, filename=None):
         self.name = name
         self.starttime = starttime
         self.endtime = endtime
         self.filename = filename
 
     def serialize(self):
-        '''json-friendly encoding'''
-        return {'id': self.id,
-                'name': self.name,
-                'starttime': self.starttime,
-                'endtime': self.endtime,
-                'filename': self.filename
-                }
+        """json-friendly encoding"""
+        return {
+            "id": self.id,
+            "name": self.name,
+            "starttime": self.starttime,
+            "endtime": self.endtime,
+            "filename": self.filename,
+        }
 
     def __repr__(self):
-        contents = "id:'%s',name:'%s',Start: '%s',End: '%s'" % \
-            (self.id, self.name, self.starttime, self.endtime)
+        contents = "id:'%s',name:'%s',Start: '%s',End: '%s'" % (
+            self.id,
+            self.name,
+            self.starttime,
+            self.endtime,
+        )
         if self.filename is not None:
             contents += ",Filename: '%s'" % self.filename
         return "<Rec(%s)>" % contents
@@ -58,12 +63,11 @@ class RecDB:
         self.conn = self.engine.connect()
         self.log = logging.getLogger(name=self.__class__.__name__)
 
-        logging.getLogger('sqlalchemy.engine').setLevel(logging.FATAL)
-        logging.getLogger('sqlalchemy.engine.base.Engine')\
-            .setLevel(logging.FATAL)
-        logging.getLogger('sqlalchemy.dialects').setLevel(logging.FATAL)
-        logging.getLogger('sqlalchemy.pool').setLevel(logging.FATAL)
-        logging.getLogger('sqlalchemy.orm').setLevel(logging.FATAL)
+        logging.getLogger("sqlalchemy.engine").setLevel(logging.FATAL)
+        logging.getLogger("sqlalchemy.engine.base.Engine").setLevel(logging.FATAL)
+        logging.getLogger("sqlalchemy.dialects").setLevel(logging.FATAL)
+        logging.getLogger("sqlalchemy.pool").setLevel(logging.FATAL)
+        logging.getLogger("sqlalchemy.orm").setLevel(logging.FATAL)
 
         Base.metadata.create_all(self.engine)  # create Database
 
@@ -74,17 +78,17 @@ class RecDB:
 
     def add(self, simplerecord):
         s = self.get_session()
-        s.add( simplerecord )
+        s.add(simplerecord)
         s.commit()
         self.log.info("New Record: %s" % simplerecord)
-        return ( simplerecord )
+        return simplerecord
 
     def update(self, id, rec):
 
         # TODO: rlist = results list
         _rlist = self._search(_id=id)
         if not len(_rlist) == 1:
-            raise ValueError('Too many recs with id=%s' % id)
+            raise ValueError("Too many recs with id=%s" % id)
 
         self.log.debug("DB:: Update request %s:%s " % (id, rec))
         self.log.debug("DB:: Update: data before %s" % _rlist[0])
@@ -92,7 +96,7 @@ class RecDB:
         # 2013-11-24 22:22:42
         _rlist[0].starttime = rec["starttime"]
         _rlist[0].endtime = rec["endtime"]
-        if 'name' in rec:
+        if "name" in rec:
             _rlist[0].name = rec["name"]
         self.log.debug("DB:: Update: data AFTER %s" % _rlist[0])
 
@@ -150,34 +154,34 @@ class RecDB:
         return query.all()
 
     def _query_ongoing(self, query=None):
-        '''
+        """
         Not terminated AND recent.
 
         The meaning is "a query that makes sense to stop"
-        '''
-        delta = timedelta(seconds=get_config()['FORGE_MAX_DURATION'])
+        """
+        delta = timedelta(seconds=get_config()["FORGE_MAX_DURATION"])
         return self._query_newer(delta, self._query_not_saved(query))
 
     def _query_not_saved(self, query=None):
-        '''Still not saved'''
+        """Still not saved"""
         if query is None:
             query = self.get_session().query(Rec)
         return query.filter(Rec.filename == None)
 
     def _query_saved(self, query=None):
-        '''Still not saved'''
+        """Still not saved"""
         if query is None:
             query = self.get_session().query(Rec)
         return query.filter(Rec.filename != None)
 
     def _query_newer(self, delta, query=None):
-        '''Get Rec older than delta seconds'''
+        """Get Rec older than delta seconds"""
         if query is None:
             query = self.get_session().query(Rec)
         return query.filter(Rec.starttime > datetime.now() - delta)
 
     def _query_older(self, delta, query=None):
-        '''Get Rec older than delta seconds'''
+        """Get Rec older than delta seconds"""
         if query is None:
             query = self.get_session().query(Rec)
         return query.filter(Rec.starttime < datetime.now() - delta)
@@ -187,15 +191,14 @@ class RecDB:
             page_size = int(page_size)
             query = query.limit(page_size)
         if page:
-            query = query.offset(page*page_size)
+            query = query.offset(page * page_size)
         return query
 
-    def _query_generic(self, query, _id=None, name=None, starttime=None,
-                       endtime=None):
+    def _query_generic(self, query, _id=None, name=None, starttime=None, endtime=None):
         if _id is not None:
             query = query.filter_by(id=_id)
         if name is not None:
-            query = query.filter(Rec.name.like("%"+name+"%"))
+            query = query.filter(Rec.name.like("%" + name + "%"))
         if starttime is not None:
             _st = starttime
             query = query.filter(Rec.starttime > _st)
@@ -204,15 +207,22 @@ class RecDB:
             query = query.filter(Rec.endtime < _et)
         return query
 
-    def _search(self, _id=None, name=None, starttime=None,
-                endtime=None, page=0, page_size=PAGESIZE):
+    def _search(
+        self,
+        _id=None,
+        name=None,
+        starttime=None,
+        endtime=None,
+        page=0,
+        page_size=PAGESIZE,
+    ):
         self.log.debug(
-            "DB: Search => id:%s name:%s starttime:%s endtime=%s" %
-            (_id, name, starttime, endtime))
+            "DB: Search => id:%s name:%s starttime:%s endtime=%s"
+            % (_id, name, starttime, endtime)
+        )
 
         query = self.get_session().query(Rec)
-        query = self._query_generic(query, _id, name, starttime,
-                                    endtime)
+        query = self._query_generic(query, _id, name, starttime, endtime)
         query = self._query_page(query, page, page_size)
         self.log.debug("Searching: %s" % str(query))
         ret = query.all()
@@ -226,6 +236,7 @@ class RecDB:
 
 
 if __name__ == "__main__":
+
     def printall(queryres):
         for record in queryres:
             print("Record: %s" % record)
@@ -245,12 +256,11 @@ if __name__ == "__main__":
     print("Mimmo ")
     printall(db._search(name="Mimmo1"))
     print("Search")
-    printall(db._search(name="Mimmo1",
-                        starttime=datetime(2014, 5, 24, 15, 16, 1) ))
+    printall(db._search(name="Mimmo1", starttime=datetime(2014, 5, 24, 15, 16, 1)))
     a = db.get_by_id(5)
     a.start()
     db.delete(1)
     db.delete(2)
     db.delete(4)
     db.delete(1)
-    printall( db._search() )
+    printall(db._search())

+ 23 - 20
server/default_config.py

@@ -1,37 +1,40 @@
 import logging
-HOST = 'localhost'
-PORT = '8000'
+
+HOST = "localhost"
+PORT = "8000"
 # pastelog is just "paste", but customized to accept logging options
-WSGI_SERVER = 'pastelog'
+WSGI_SERVER = "pastelog"
 # these are pastelog-specific options for logging engine
 TRANSLOGGER_OPTS = {
-        'logger_name': 'accesslog',
-        'set_logger_level': logging.WARNING,
-        'setup_console_handler': False }
+    "logger_name": "accesslog",
+    "set_logger_level": logging.WARNING,
+    "setup_console_handler": False,
+}
 WSGI_SERVER_OPTIONS = {}
 
 DEBUG = True
-DB_URI = 'sqlite:///techrec.db'
-AUDIO_OUTPUT = 'output/'
-AUDIO_INPUT = 'rec/'
-AUDIO_INPUT_FORMAT = '%Y-%m/%d/rec-%Y-%m-%d-%H-%M-%S.mp3'
-AUDIO_OUTPUT_FORMAT = 'techrec-%(startdt)s-%(endtime)s-%(name)s.mp3'
+DB_URI = "sqlite:///techrec.db"
+AUDIO_OUTPUT = "output/"
+AUDIO_INPUT = "rec/"
+AUDIO_INPUT_FORMAT = "%Y-%m/%d/rec-%Y-%m-%d-%H-%M-%S.mp3"
+AUDIO_OUTPUT_FORMAT = "techrec-%(startdt)s-%(endtime)s-%(name)s.mp3"
 FORGE_TIMEOUT = 20
-FORGE_MAX_DURATION = 3600*5
-FFMPEG_OUT_CODEC = ['-acodec', 'copy']
-FFMPEG_OPTIONS = ['-loglevel', 'warning', '-n']
-FFMPEG_PATH = 'ffmpeg'
+FORGE_MAX_DURATION = 3600 * 5
+FFMPEG_OUT_CODEC = ["-acodec", "copy"]
+FFMPEG_OPTIONS = ["-loglevel", "warning", "-n"]
+FFMPEG_PATH = "ffmpeg"
 # tag:value pairs
 TAG_EXTRA = {}
 # LICENSE URI is special because date need to be added
 TAG_LICENSE_URI = None
 
-STATIC_FILES='static/'
-STATIC_PAGES='pages/'
+STATIC_FILES = "static/"
+STATIC_PAGES = "pages/"
 try:
     from pkg_resources import resource_filename, resource_isdir
-    if resource_isdir('techrec', 'pages'):
-        STATIC_PAGES = resource_filename('techrec', 'pages')
-        STATIC_FILES = resource_filename('techrec', 'static')
+
+    if resource_isdir("techrec", "pages"):
+        STATIC_PAGES = resource_filename("techrec", "pages")
+        STATIC_FILES = resource_filename("techrec", "static")
 except ImportError:
     logging.exception("Error loading resources from installed part")

+ 44 - 46
server/forge.py

@@ -8,20 +8,19 @@ from .config_manager import get_config
 
 
 def get_timefile_exact(time):
-    '''
+    """
     time is of type `datetime`; it is not "rounded" to match the real file;
     that work is done in get_timefile(time)
-    '''
+    """
     return os.path.join(
-        get_config()['AUDIO_INPUT'],
-        time.strftime(get_config()['AUDIO_INPUT_FORMAT'])
+        get_config()["AUDIO_INPUT"], time.strftime(get_config()["AUDIO_INPUT_FORMAT"])
     )
 
 
 def round_timefile(exact):
-    '''
+    """
     This will round the datetime, so to match the file organization structure
-    '''
+    """
     return datetime(exact.year, exact.month, exact.day, exact.hour)
 
 
@@ -30,11 +29,11 @@ def get_timefile(exact):
 
 
 def get_files_and_intervals(start, end, rounder=round_timefile):
-    '''
+    """
     both arguments are datetime objects
     returns an iterator whose elements are (filename, start_cut, end_cut)
     Cuts are expressed in seconds
-    '''
+    """
     if end <= start:
         raise ValueError("end < start!")
 
@@ -50,7 +49,7 @@ def get_files_and_intervals(start, end, rounder=round_timefile):
 
 
 def mp3_join(named_intervals):
-    '''
+    """
     Note that these are NOT the intervals returned by get_files_and_intervals,
     as they do not supply a filename, but only a datetime.
     What we want in input is basically the same thing, but with get_timefile()
@@ -58,8 +57,8 @@ def mp3_join(named_intervals):
 
     This function make the (quite usual) assumption that the only start_cut (if
     any) is at the first file, and the last one is at the last file
-    '''
-    ffmpeg = get_config()['FFMPEG_PATH']
+    """
+    ffmpeg = get_config()["FFMPEG_PATH"]
     startskip = None
     endskip = None
     files = []
@@ -72,65 +71,64 @@ def mp3_join(named_intervals):
         if end_cut:
             assert endskip is None
             endskip = end_cut
-        assert '|' not in filename
+        assert "|" not in filename
         files.append(filename)
 
-    cmdline = [ffmpeg, '-i', 'concat:%s' % '|'.join(files)]
-    cmdline += get_config()['FFMPEG_OUT_CODEC']
+    cmdline = [ffmpeg, "-i", "concat:%s" % "|".join(files)]
+    cmdline += get_config()["FFMPEG_OUT_CODEC"]
     if startskip is not None:
-        cmdline += ['-ss', str(startskip)]
+        cmdline += ["-ss", str(startskip)]
     else:
         startskip = 0
     if endskip is not None:
-        cmdline += ['-t', str(len(files)*3600 - (startskip + endskip))]
+        cmdline += ["-t", str(len(files) * 3600 - (startskip + endskip))]
     return cmdline
 
 
 def create_mp3(start, end, outfile, options={}, **kwargs):
-    intervals = [(get_timefile(begin), start_cut, end_cut)
-                 for begin, start_cut, end_cut
-                 in get_files_and_intervals(start, end)]
+    intervals = [
+        (get_timefile(begin), start_cut, end_cut)
+        for begin, start_cut, end_cut in get_files_and_intervals(start, end)
+    ]
     if os.path.exists(outfile):
         raise OSError("file '%s' already exists" % outfile)
     for path, _s, _e in intervals:
         if not os.path.exists(path):
-            raise OSError("file '%s' does not exist; recording system broken?"
-                          % path)
+            raise OSError("file '%s' does not exist; recording system broken?" % path)
 
     # metadata date/time formatted according to
     # https://wiki.xiph.org/VorbisComment#Date_and_time
     metadata = {}
-    if outfile.endswith('.mp3'):
-        metadata['TRDC'] = start.replace(microsecond=0).isoformat()
-        metadata['RECORDINGTIME'] = metadata['TRDC']
-        metadata['ENCODINGTIME'] = datetime.now().replace(
-            microsecond=0).isoformat()
+    if outfile.endswith(".mp3"):
+        metadata["TRDC"] = start.replace(microsecond=0).isoformat()
+        metadata["RECORDINGTIME"] = metadata["TRDC"]
+        metadata["ENCODINGTIME"] = datetime.now().replace(microsecond=0).isoformat()
     else:
-        metadata['DATE'] = start.replace(microsecond=0).isoformat()
-    metadata['ENCODER'] = 'https://github.com/boyska/techrec'
-    if 'title' in options:
-        metadata['TITLE'] = options['title']
-    if options.get('license_uri', None) is not None:
-        metadata['RIGHTS-DATE'] = start.strftime('%Y-%m')
-        metadata['RIGHTS-URI'] = options['license_uri']
-    if 'extra_tags' in options:
-        metadata.update(options['extra_tags'])
+        metadata["DATE"] = start.replace(microsecond=0).isoformat()
+    metadata["ENCODER"] = "https://github.com/boyska/techrec"
+    if "title" in options:
+        metadata["TITLE"] = options["title"]
+    if options.get("license_uri", None) is not None:
+        metadata["RIGHTS-DATE"] = start.strftime("%Y-%m")
+        metadata["RIGHTS-URI"] = options["license_uri"]
+    if "extra_tags" in options:
+        metadata.update(options["extra_tags"])
     metadata_list = []
     for tag, value in metadata.items():
-        if '=' in tag:
+        if "=" in tag:
             logging.error('Received a tag with "=" inside, skipping')
             continue
-        metadata_list.append('-metadata')
-        metadata_list.append('%s=%s' % (tag, value))
+        metadata_list.append("-metadata")
+        metadata_list.append("%s=%s" % (tag, value))
 
-    p = Popen(mp3_join(intervals) + metadata_list +
-              get_config()['FFMPEG_OPTIONS'] + [outfile])
-    if get_config()['FORGE_TIMEOUT'] == 0:
+    p = Popen(
+        mp3_join(intervals) + metadata_list + get_config()["FFMPEG_OPTIONS"] + [outfile]
+    )
+    if get_config()["FORGE_TIMEOUT"] == 0:
         p.wait()
     else:
         start = datetime.now()
-        while (datetime.now() - start).total_seconds() \
-                < get_config()['FORGE_TIMEOUT']:
+        while (datetime.now() - start).total_seconds() < get_config()["FORGE_TIMEOUT"]:
             p.poll()
             if p.returncode is None:
                 sleep(1)
@@ -142,14 +140,14 @@ def create_mp3(start, end, outfile, options={}, **kwargs):
             os.remove(outfile)
         except:
             pass
-        raise Exception('timeout')  # TODO: make a specific TimeoutError
+        raise Exception("timeout")  # TODO: make a specific TimeoutError
     if p.returncode != 0:
         raise OSError("return code was %d" % p.returncode)
     return True
 
 
 def main_cmd(options):
-    log = logging.getLogger('forge_main')
+    log = logging.getLogger("forge_main")
     outfile = os.path.abspath(os.path.join(options.cwd, options.outfile))
-    log.debug('will forge an mp3 into %s' % (outfile))
+    log.debug("will forge an mp3 into %s" % (outfile))
     create_mp3(options.starttime, options.endtime, outfile)

+ 5 - 3
server/maint.py

@@ -7,11 +7,12 @@ from sqlalchemy import inspect
 from .config_manager import get_config
 from .db import RecDB
 
+
 def cleanold_cmd(options):
-    log = logging.getLogger('cleanold')
+    log = logging.getLogger("cleanold")
     log.debug("starting cleanold[%d]" % options.minage)
-    db = RecDB(get_config()['DB_URI'])
-    res = db.get_not_completed(options.minage*3600*24)
+    db = RecDB(get_config()["DB_URI"])
+    res = db.get_not_completed(options.minage * 3600 * 24)
     count = len(res)
     if options.pretend:
         for rec in res:
@@ -25,4 +26,5 @@ def cleanold_cmd(options):
         logging.info("Cleanold complete: %d deleted" % count)
     sys.exit(0)
 
+
 # vim: set ai ts=4 sw=4 et:

+ 13 - 7
server/processqueue.py

@@ -12,18 +12,18 @@ class JobQueue(object):
         job_id = self.last_job_id
 
         def clean_jobs(res):
-            '''this callback will remove the job from the queue'''
+            """this callback will remove the job from the queue"""
             del self.jobs[job_id]
-        self.jobs[job_id] = self.pool.apply_async(function, args, kwargs,
-                                                  clean_jobs)
+
+        self.jobs[job_id] = self.pool.apply_async(function, args, kwargs, clean_jobs)
         return job_id
 
     def check_job(self, job_id):
-        '''
+        """
         If the job is running, return the asyncResult.
         If it has already completed, returns True.
         If no such job_id exists at all, returns False
-        '''
+        """
         if job_id <= 0:
             raise ValueError("non-valid job_id")
         if self.last_job_id < job_id:
@@ -38,13 +38,16 @@ class JobQueue(object):
         self.pool = None
 
 
-def simulate_long_job(recid=None, starttime=None, endtime=None, name='', filename=None):
+def simulate_long_job(recid=None, starttime=None, endtime=None, name="", filename=None):
     from time import sleep
+
     print("evviva " + name)
     sleep(2)
     print("lavoro su " + name)
     sleep(2)
     print("done su " + name)
+
+
 _queue = None
 
 
@@ -54,12 +57,15 @@ def get_process_queue():
         _queue = JobQueue()
     return _queue
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     from datetime import datetime
+
     n = datetime.now()
 
     def sleep(n):
         import time
+
         print("Inizio %d" % n)
         time.sleep(n)
         print("Finisco %d" % n)

+ 191 - 157
server/server.py

@@ -8,8 +8,8 @@ import unicodedata
 from bottle import Bottle, request, static_file, redirect, abort, response
 import bottle
 
-logger = logging.getLogger('server')
-botlog = logging.getLogger('bottle')
+logger = logging.getLogger("server")
+botlog = logging.getLogger("bottle")
 botlog.setLevel(logging.INFO)
 botlog.addHandler(logging.StreamHandler(sys.stdout))
 bottle._stderr = lambda x: botlog.info(x.strip())
@@ -25,47 +25,49 @@ def date_read(s):
 
 
 def date_write(dt):
-    return dt.strftime('%s')
+    return dt.strftime("%s")
 
 
 def rec_sanitize(rec):
     d = rec.serialize()
-    d['starttime'] = date_write(d['starttime'])
-    d['endtime'] = date_write(d['endtime'])
+    d["starttime"] = date_write(d["starttime"])
+    d["endtime"] = date_write(d["endtime"])
     return d
 
 
 class DateApp(Bottle):
-    '''
+    """
     This application will expose some date-related functions; it is intended to
     be used when you need to know the server's time on the browser
-    '''
+    """
+
     def __init__(self):
         Bottle.__init__(self)
-        self.route('/help', callback=self.help)
-        self.route('/date', callback=self.date)
-        self.route('/custom', callback=self.custom)
+        self.route("/help", callback=self.help)
+        self.route("/date", callback=self.date)
+        self.route("/custom", callback=self.custom)
 
     def date(self):
         n = datetime.now()
         return {
-            'unix': n.strftime('%s'),
-            'isoformat': n.isoformat(),
-            'ctime': n.ctime()
+            "unix": n.strftime("%s"),
+            "isoformat": n.isoformat(),
+            "ctime": n.ctime(),
         }
 
     def custom(self):
         n = datetime.now()
-        if 'strftime' not in request.query:
+        if "strftime" not in request.query:
             abort(400, 'Need argument "strftime"')
-        response.content_type = 'text/plain'
-        return n.strftime(request.query['strftime'])
+        response.content_type = "text/plain"
+        return n.strftime(request.query["strftime"])
 
     def help(self):
-        response.content_type = 'text/plain'
-        return \
-            '/date : get JSON dict containing multiple formats of now()\n' + \
-            '/custom?strftime=FORMAT : get now().strftime(FORMAT)'
+        response.content_type = "text/plain"
+        return (
+            "/date : get JSON dict containing multiple formats of now()\n"
+            + "/custom?strftime=FORMAT : get now().strftime(FORMAT)"
+        )
 
 
 class RecAPI(Bottle):
@@ -73,20 +75,20 @@ class RecAPI(Bottle):
         Bottle.__init__(self)
         self._route()
         self._app = app
-        self.db = RecDB(get_config()['DB_URI'])
+        self.db = RecDB(get_config()["DB_URI"])
 
     def _route(self):
-        self.post('/create', callback=self.create)
-        self.post('/delete', callback=self.delete)
-        self.post('/update/<recid:int>', callback=self.update)
-        self.post('/generate', callback=self.generate)
-        self.get('/help', callback=self.help)
-        self.get('/', callback=self.help)
-        self.get('/get/search', callback=self.search)
-        self.get('/get/ongoing', callback=self.get_ongoing)
-        self.get('/get/archive', callback=self.get_archive)
-        self.get('/jobs', callback=self.running_jobs)
-        self.get('/jobs/<job_id:int>', callback=self.check_job)
+        self.post("/create", callback=self.create)
+        self.post("/delete", callback=self.delete)
+        self.post("/update/<recid:int>", callback=self.update)
+        self.post("/generate", callback=self.generate)
+        self.get("/help", callback=self.help)
+        self.get("/", callback=self.help)
+        self.get("/get/search", callback=self.search)
+        self.get("/get/ongoing", callback=self.get_ongoing)
+        self.get("/get/archive", callback=self.get_archive)
+        self.get("/jobs", callback=self.running_jobs)
+        self.get("/jobs/<job_id:int>", callback=self.check_job)
 
     def create(self):
         req = dict(request.POST.decode().allitems())
@@ -94,22 +96,21 @@ class RecAPI(Bottle):
         logger.debug("Create request %s " % req)
 
         now = datetime.now()
-        start = date_read(req['starttime']) if 'starttime' in req else now
-        name = req['name'] if 'name' in req else u""
-        end = date_read(req['endtime']) if 'endtime' in req else now
+        start = date_read(req["starttime"]) if "starttime" in req else now
+        name = req["name"] if "name" in req else u""
+        end = date_read(req["endtime"]) if "endtime" in req else now
 
-        rec = Rec(name=name,
-                  starttime=start,
-                  endtime=end)
+        rec = Rec(name=name, starttime=start, endtime=end)
         ret = self.db.add(rec)
 
-        return self.rec_msg("Nuova registrazione creata! (id:%d)" % ret.id,
-                            rec=rec_sanitize(rec))
+        return self.rec_msg(
+            "Nuova registrazione creata! (id:%d)" % ret.id, rec=rec_sanitize(rec)
+        )
 
     def delete(self):
         req = dict(request.POST.decode().allitems())
         logging.info("Server: request delete %s " % (req))
-        if 'id' not in req:
+        if "id" not in req:
             return self.rec_err("No valid ID")
 
         if self.db.delete(req["id"]):
@@ -122,16 +123,16 @@ class RecAPI(Bottle):
 
         newrec = {}
         now = datetime.now()
-        if 'starttime' not in req:
-            newrec['starttime'] = now
+        if "starttime" not in req:
+            newrec["starttime"] = now
         else:
-            newrec['starttime'] = date_read(req['starttime'])
+            newrec["starttime"] = date_read(req["starttime"])
         if "endtime" not in req:
-            newrec['endtime'] = now
+            newrec["endtime"] = now
         else:
-            newrec['endtime'] = date_read(req['endtime'])
-        if 'name' in req:
-            newrec["name"] = req['name']
+            newrec["endtime"] = date_read(req["endtime"])
+        if "name" in req:
+            newrec["name"] = req["name"]
 
         try:
             logger.info("prima di update")
@@ -139,88 +140,101 @@ class RecAPI(Bottle):
             logger.info("dopo update")
         except Exception as exc:
             return self.rec_err("Errore Aggiornamento", exception=exc)
-        return self.rec_msg("Aggiornamento completato!",
-                            rec=rec_sanitize(result_rec))
+        return self.rec_msg("Aggiornamento completato!", rec=rec_sanitize(result_rec))
 
     def generate(self):
         # prendiamo la rec in causa
-        recid = dict(request.POST.decode().allitems())['id']
+        recid = dict(request.POST.decode().allitems())["id"]
         rec = self.db._search(_id=recid)[0]
         if rec.filename is not None and os.path.exists(rec.filename):
-            return {'status': 'ready',
-                    'message': 'The file has already been generated at %s' %
-                    rec.filename,
-                    'rec': rec
-                    }
-        if get_config()['FORGE_MAX_DURATION'] > 0 and \
-                (rec.endtime - rec.starttime).total_seconds() > \
-                get_config()['FORGE_MAX_DURATION']:
-                    response.status = 400
-                    return {'status': 'error',
-                            'message': 'The requested recording is too long' +
-                            ' (%d seconds)' %
-                            (rec.endtime - rec.starttime).total_seconds()
-                            }
-        rec.filename = get_config()['AUDIO_OUTPUT_FORMAT'] % {
-            'time': rec.starttime.strftime('%y%m%d_%H%M'),  # kept for retrocompatibility, should be dropped
-            'endtime': rec.endtime.strftime('%H%M'),
-            'startdt': rec.starttime.strftime('%y%m%d_%H%M'),
-            'enddt': rec.endtime.strftime('%y%m%d_%H%M'),
-            'name': ''.join(filter(lambda c: c.isalpha(),
-                           unicodedata.normalize('NFKD', rec.name).encode('ascii', 'ignore').decode('ascii'))),
+            return {
+                "status": "ready",
+                "message": "The file has already been generated at %s" % rec.filename,
+                "rec": rec,
+            }
+        if (
+            get_config()["FORGE_MAX_DURATION"] > 0
+            and (rec.endtime - rec.starttime).total_seconds()
+            > get_config()["FORGE_MAX_DURATION"]
+        ):
+            response.status = 400
+            return {
+                "status": "error",
+                "message": "The requested recording is too long"
+                + " (%d seconds)" % (rec.endtime - rec.starttime).total_seconds(),
+            }
+        rec.filename = get_config()["AUDIO_OUTPUT_FORMAT"] % {
+            "time": rec.starttime.strftime(
+                "%y%m%d_%H%M"
+            ),  # kept for retrocompatibility, should be dropped
+            "endtime": rec.endtime.strftime("%H%M"),
+            "startdt": rec.starttime.strftime("%y%m%d_%H%M"),
+            "enddt": rec.endtime.strftime("%y%m%d_%H%M"),
+            "name": "".join(
+                filter(
+                    lambda c: c.isalpha(),
+                    unicodedata.normalize("NFKD", rec.name)
+                    .encode("ascii", "ignore")
+                    .decode("ascii"),
+                )
+            ),
         }
         self.db.get_session(rec).commit()
         job_id = self._app.pq.submit(
             create_mp3,
             start=rec.starttime,
             end=rec.endtime,
-            outfile=os.path.join(get_config()['AUDIO_OUTPUT'], rec.filename),
+            outfile=os.path.join(get_config()["AUDIO_OUTPUT"], rec.filename),
             options={
-                'title': rec.name,
-                'license_uri': get_config()['TAG_LICENSE_URI'],
-                'extra_tags': get_config()['TAG_EXTRA']
-            }
+                "title": rec.name,
+                "license_uri": get_config()["TAG_LICENSE_URI"],
+                "extra_tags": get_config()["TAG_EXTRA"],
+            },
         )
         logger.debug("SUBMITTED: %d" % job_id)
-        return self.rec_msg("Aggiornamento completato!",
-                            job_id=job_id,
-                            result='/output/' + rec.filename,
-                            rec=rec_sanitize(rec))
+        return self.rec_msg(
+            "Aggiornamento completato!",
+            job_id=job_id,
+            result="/output/" + rec.filename,
+            rec=rec_sanitize(rec),
+        )
 
     def check_job(self, job_id):
         try:
             job = self._app.pq.check_job(job_id)
         except ValueError:
-            abort(400, 'job_id not valid')
+            abort(400, "job_id not valid")
 
         def ret(status):
-            return {'job_status': status, 'job_id': job_id}
+            return {"job_status": status, "job_id": job_id}
+
         if job is True:
-            return ret('DONE')
+            return ret("DONE")
         if job is False:
-            abort(404, 'No such job has ever been spawned')
+            abort(404, "No such job has ever been spawned")
         else:
             if job.ready():
                 try:
                     res = job.get()
                     return res
                 except Exception as exc:
-                    r = ret('FAILED')
-                    r['exception'] = str(exc)
+                    r = ret("FAILED")
+                    r["exception"] = str(exc)
                     import traceback
+
                     tb = traceback.format_exc()
                     logger.warning(tb)
-                    if get_config()['DEBUG']:
-                        r['exception'] = "%s: %s" % (str(exc), tb)
-                        r['traceback'] = tb
+                    if get_config()["DEBUG"]:
+                        r["exception"] = "%s: %s" % (str(exc), tb)
+                        r["traceback"] = tb
 
                     return r
-            return ret('WIP')
+            return ret("WIP")
 
     def running_jobs(self):
         res = {}
-        res['last_job_id'] = self._app.pq.last_job_id
-        res['running'] = self._app.pq.jobs.keys()
+        res["last_job_id"] = self._app.pq.last_job_id
+        res["running"] = self._app.pq.jobs.keys()
         return res
 
     def search(self, args=None):
@@ -230,8 +244,8 @@ class RecAPI(Bottle):
 
         values = self.db._search(**req)
         from pprint import pprint
-        logger.debug("Returned Values %s" %
-                     pprint([r.serialize() for r in values]))
+
+        logger.debug("Returned Values %s" % pprint([r.serialize() for r in values]))
 
         ret = {}
         for rec in values:
@@ -241,12 +255,10 @@ class RecAPI(Bottle):
         return ret
 
     def get_ongoing(self):
-        return {rec.id: rec_sanitize(rec)
-                for rec in self.db.get_ongoing()}
+        return {rec.id: rec_sanitize(rec) for rec in self.db.get_ongoing()}
 
     def get_archive(self):
-        return {rec.id: rec_sanitize(rec)
-                for rec in self.db.get_archive_recent()}
+        return {rec.id: rec_sanitize(rec) for rec in self.db.get_archive_recent()}
 
     # @route('/help')
     def help(self):
@@ -279,104 +291,126 @@ class RecServer:
         self._app.pq = get_process_queue()
         self._route()
 
-        self.db = RecDB(get_config()['DB_URI'])
+        self.db = RecDB(get_config()["DB_URI"])
 
     def _route(self):
         # Static part of the site
-        self._app.route('/output/<filepath:path>',
-                        callback=lambda filepath:
-                        static_file(filepath,
-                                    root=get_config()['AUDIO_OUTPUT'],
-                                    download=True))
-
-        self._app.route('/static/<filepath:path>',
-                        callback=lambda filepath: static_file(filepath,
-                                                              root=get_config()['STATIC_FILES']))
-        self._app.route('/', callback=lambda: redirect('/new.html'))
-        self._app.route('/new.html',
-                        callback=partial(static_file, 'new.html',
-                                         root=get_config()['STATIC_PAGES']))
-        self._app.route('/old.html',
-                        callback=partial(static_file, 'old.html',
-                                         root=get_config()['STATIC_PAGES']))
-        self._app.route('/archive.html',
-                        callback=partial(static_file, 'archive.html',
-                                         root=get_config()['STATIC_PAGES']))
+        self._app.route(
+            "/output/<filepath:path>",
+            callback=lambda filepath: static_file(
+                filepath, root=get_config()["AUDIO_OUTPUT"], download=True
+            ),
+        )
+
+        self._app.route(
+            "/static/<filepath:path>",
+            callback=lambda filepath: static_file(
+                filepath, root=get_config()["STATIC_FILES"]
+            ),
+        )
+        self._app.route("/", callback=lambda: redirect("/new.html"))
+        self._app.route(
+            "/new.html",
+            callback=partial(
+                static_file, "new.html", root=get_config()["STATIC_PAGES"]
+            ),
+        )
+        self._app.route(
+            "/old.html",
+            callback=partial(
+                static_file, "old.html", root=get_config()["STATIC_PAGES"]
+            ),
+        )
+        self._app.route(
+            "/archive.html",
+            callback=partial(
+                static_file, "archive.html", root=get_config()["STATIC_PAGES"]
+            ),
+        )
 
 
 class DebugAPI(Bottle):
-    '''
+    """
     This application is useful for testing the webserver itself
-    '''
+    """
+
     def __init__(self):
         Bottle.__init__(self)
-        self.route('/sleep/:milliseconds', callback=self.sleep)
-        self.route('/cpusleep/:howmuch', callback=self.cpusleep)
-        self.route('/big/:exponent', callback=self.big)
+        self.route("/sleep/:milliseconds", callback=self.sleep)
+        self.route("/cpusleep/:howmuch", callback=self.cpusleep)
+        self.route("/big/:exponent", callback=self.big)
 
     def sleep(self, milliseconds):
         import time
-        time.sleep(int(milliseconds)/1000.0)
-        return 'ok'
+
+        time.sleep(int(milliseconds) / 1000.0)
+        return "ok"
 
     def cpusleep(self, howmuch):
-        out = ''
-        for i in xrange(int(howmuch) * (10**3)):
+        out = ""
+        for i in xrange(int(howmuch) * (10 ** 3)):
             if i % 11234 == 0:
-                out += 'a'
+                out += "a"
         return out
 
     def big(self, exponent):
-        '''
+        """
         returns a 2**n -1 string
-        '''
+        """
         for i in xrange(int(exponent)):
             yield str(i) * (2 ** i)
 
     def help(self):
-        response.content_type = 'text/plain'
-        return '''
+        response.content_type = "text/plain"
+        return """
     /sleep/<int:milliseconds> : sleep, than say "ok"
     /cpusleep/<int:howmuch> : busysleep, than say "ok"
     /big/<int:exponent> : returns a 2**n -1 byte content
-    '''
+    """
 
 
 class PasteLoggingServer(bottle.PasteServer):
     def run(self, handler):  # pragma: no cover
         from paste import httpserver
         from paste.translogger import TransLogger
-        handler = TransLogger(handler, **self.options['translogger_opts'])
-        del self.options['translogger_opts']
-        httpserver.serve(handler, host=self.host, port=str(self.port),
-                         **self.options)
-bottle.server_names['pastelog'] = PasteLoggingServer
+
+        handler = TransLogger(handler, **self.options["translogger_opts"])
+        del self.options["translogger_opts"]
+        httpserver.serve(handler, host=self.host, port=str(self.port), **self.options)
+
+
+bottle.server_names["pastelog"] = PasteLoggingServer
 
 
 def main_cmd(*args):
     """meant to be called from argparse"""
     c = RecServer()
-    c._app.mount('/date', DateApp())
-    c._app.mount('/api', RecAPI(c._app))
-    if get_config()['DEBUG']:
-        c._app.mount('/debug', DebugAPI())
+    c._app.mount("/date", DateApp())
+    c._app.mount("/api", RecAPI(c._app))
+    if get_config()["DEBUG"]:
+        c._app.mount("/debug", DebugAPI())
 
-    server = get_config()['WSGI_SERVER']
-    if server == 'pastelog':
+    server = get_config()["WSGI_SERVER"]
+    if server == "pastelog":
         from paste.translogger import TransLogger
-        get_config()['WSGI_SERVER_OPTIONS']['translogger_opts'] = \
-            get_config()['TRANSLOGGER_OPTS']
-
-    c._app.run(server=server,
-               host=get_config()['HOST'],
-               port=get_config()['PORT'],
-               debug=get_config()['DEBUG'],
-               quiet=True,  # this is to hide access.log style messages
-               **get_config()['WSGI_SERVER_OPTIONS']
-               )
-
-if __name__ == '__main__':
+
+        get_config()["WSGI_SERVER_OPTIONS"]["translogger_opts"] = get_config()[
+            "TRANSLOGGER_OPTS"
+        ]
+
+    c._app.run(
+        server=server,
+        host=get_config()["HOST"],
+        port=get_config()["PORT"],
+        debug=get_config()["DEBUG"],
+        quiet=True,  # this is to hide access.log style messages
+        **get_config()["WSGI_SERVER_OPTIONS"]
+    )
+
+
+if __name__ == "__main__":
     from cli import common_pre
+
     common_pre()
     logger.warn("Usage of server.py is deprecated; use cli.py")
     main_cmd()

+ 47 - 38
server/test_forge.py

@@ -2,18 +2,23 @@ from datetime import datetime, timedelta
 
 from nose.tools import raises, eq_
 
-from .forge import get_files_and_intervals, get_timefile_exact, round_timefile,\
-    get_timefile, mp3_join
+from .forge import (
+    get_files_and_intervals,
+    get_timefile_exact,
+    round_timefile,
+    get_timefile,
+    mp3_join,
+)
 from .config_manager import get_config
 
 eight = datetime(2014, 5, 30, 20)
 nine = datetime(2014, 5, 30, 21)
 ten = datetime(2014, 5, 30, 22)
 
-get_config()['AUDIO_INPUT'] = ''
-get_config()['AUDIO_INPUT_FORMAT'] = '%Y-%m/%d/%Y-%m-%d-%H-%M-%S.mp3'
-get_config()['FFMPEG_PATH'] = 'ffmpeg'
-get_config()['FFMPEG_OUT_CODEC'] = ['-acodec', 'copy']
+get_config()["AUDIO_INPUT"] = ""
+get_config()["AUDIO_INPUT_FORMAT"] = "%Y-%m/%d/%Y-%m-%d-%H-%M-%S.mp3"
+get_config()["FFMPEG_PATH"] = "ffmpeg"
+get_config()["FFMPEG_OUT_CODEC"] = ["-acodec", "copy"]
 
 
 def minutes(n):
@@ -23,18 +28,19 @@ def minutes(n):
 def seconds(n):
     return timedelta(seconds=n)
 
+
 # timefile
 
 
 def test_timefile_exact():
-    eq_(get_timefile_exact(eight),
-        '2014-05/30/2014-05-30-20-00-00.mp3')
+    eq_(get_timefile_exact(eight), "2014-05/30/2014-05-30-20-00-00.mp3")
+
 
 # Rounding
 
 
 def test_rounding_similarity():
-    eq_(round_timefile(eight), round_timefile(eight+minutes(20)))
+    eq_(round_timefile(eight), round_timefile(eight + minutes(20)))
     assert round_timefile(eight) != round_timefile(nine)
 
 
@@ -47,13 +53,12 @@ def test_rounding_value():
 
 
 def test_timefile_alreadyround():
-    eq_(get_timefile(eight),
-        '2014-05/30/2014-05-30-20-00-00.mp3')
+    eq_(get_timefile(eight), "2014-05/30/2014-05-30-20-00-00.mp3")
 
 
 def test_timefile_toround():
-    eq_(get_timefile(eight + minutes(20)),
-        '2014-05/30/2014-05-30-20-00-00.mp3')
+    eq_(get_timefile(eight + minutes(20)), "2014-05/30/2014-05-30-20-00-00.mp3")
+
 
 # Intervals
 
@@ -69,17 +74,17 @@ def test_intervals_before():
 
 
 def test_intervals_full_1():
-    res = list(get_files_and_intervals(eight, nine-seconds(1)))
+    res = list(get_files_and_intervals(eight, nine - seconds(1)))
     eq_(len(res), 1)
     eq_(res[0][1], 0)
     eq_(res[0][2], 0)
 
 
 def test_intervals_partial_1():
-    res = list(get_files_and_intervals(eight, nine-minutes(10)))
+    res = list(get_files_and_intervals(eight, nine - minutes(10)))
     eq_(len(res), 1)
     eq_(res[0][1], 0)
-    eq_(res[0][2], 10*60 - 1)
+    eq_(res[0][2], 10 * 60 - 1)
 
 
 def test_intervals_exact_2():
@@ -101,8 +106,7 @@ def test_intervals_partial_2():
 
 
 def test_intervals_full_2():
-    res = list(get_files_and_intervals(eight,
-                                       nine + minutes(59) + seconds(59)))
+    res = list(get_files_and_intervals(eight, nine + minutes(59) + seconds(59)))
     eq_(len(res), 2)
     eq_(res[0][1], 0)
     eq_(res[0][2], 0)
@@ -122,7 +126,7 @@ def test_intervals_exact_3():
 
 
 def test_intervals_partial_3():
-    res = list(get_files_and_intervals(eight, ten+minutes(50)))
+    res = list(get_files_and_intervals(eight, ten + minutes(50)))
     eq_(len(res), 3)
     eq_(res[0][1], 0)
     eq_(res[0][2], 0)
@@ -133,7 +137,7 @@ def test_intervals_partial_3():
 
 
 def test_intervals_full_3():
-    res = list(get_files_and_intervals(eight, ten+minutes(59) + seconds(59)))
+    res = list(get_files_and_intervals(eight, ten + minutes(59) + seconds(59)))
     eq_(len(res), 3)
     eq_(res[0][1], 0)
     eq_(res[0][2], 0)
@@ -144,49 +148,54 @@ def test_intervals_full_3():
 
 
 def test_intervals_middle_1():
-    res = list(get_files_and_intervals(eight + minutes(20),
-                                       nine - minutes(20)))
+    res = list(get_files_and_intervals(eight + minutes(20), nine - minutes(20)))
     eq_(len(res), 1)
-    eq_(res[0][1], 20*60)
-    eq_(res[0][2], 20*60-1)
+    eq_(res[0][1], 20 * 60)
+    eq_(res[0][2], 20 * 60 - 1)
 
 
 def test_intervals_left_2():
-    res = list(get_files_and_intervals(eight+minutes(30), nine))
+    res = list(get_files_and_intervals(eight + minutes(30), nine))
     eq_(len(res), 2)
-    eq_(res[0][1], 30*60)
+    eq_(res[0][1], 30 * 60)
     eq_(res[0][2], 0)
     eq_(res[1][1], 0)
     eq_(res[1][2], 3599)
 
+
 # MP3 Join
 
 
 def test_mp3_1():
-    eq_(' '.join(mp3_join((('a', 0, 0),))),
-        'ffmpeg -i concat:a -acodec copy')
+    eq_(" ".join(mp3_join((("a", 0, 0),))), "ffmpeg -i concat:a -acodec copy")
 
 
 def test_mp3_1_left():
-    eq_(' '.join(mp3_join((('a', 160, 0),))),
-        'ffmpeg -i concat:a -acodec copy -ss 160')
+    eq_(" ".join(mp3_join((("a", 160, 0),))), "ffmpeg -i concat:a -acodec copy -ss 160")
 
 
 def test_mp3_1_right():
-    eq_(' '.join(mp3_join((('a', 0, 1600),))),
-        'ffmpeg -i concat:a -acodec copy -t 2000')
+    eq_(
+        " ".join(mp3_join((("a", 0, 1600),))), "ffmpeg -i concat:a -acodec copy -t 2000"
+    )
 
 
 def test_mp3_1_leftright():
-    eq_(' '.join(mp3_join((('a', 160, 1600),))),
-        'ffmpeg -i concat:a -acodec copy -ss 160 -t 1840')
+    eq_(
+        " ".join(mp3_join((("a", 160, 1600),))),
+        "ffmpeg -i concat:a -acodec copy -ss 160 -t 1840",
+    )
 
 
 def test_mp3_2():
-    eq_(' '.join(mp3_join((('a', 0, 0), ('b', 0, 0)))),
-        'ffmpeg -i concat:a|b -acodec copy')
+    eq_(
+        " ".join(mp3_join((("a", 0, 0), ("b", 0, 0)))),
+        "ffmpeg -i concat:a|b -acodec copy",
+    )
 
 
 def test_mp3_2_leftright():
-    eq_(' '.join(mp3_join((('a', 1000, 0), ('b', 0, 1600)))),
-        'ffmpeg -i concat:a|b -acodec copy -ss 1000 -t 4600')
+    eq_(
+        " ".join(mp3_join((("a", 1000, 0), ("b", 0, 1600)))),
+        "ffmpeg -i concat:a|b -acodec copy -ss 1000 -t 4600",
+    )

+ 5 - 2
setup.py

@@ -4,7 +4,7 @@ from setuptools import setup
 
 setup(
     name="techrec",
-    version="1.1.3",
+    version="1.2.0",
     description="A Python2 web application "
     "that assist radio speakers in recording their shows",
     long_description=open("README.md").read(),
@@ -14,7 +14,10 @@ setup(
     packages=["techrec"],
     package_dir={"techrec": "server"},
     install_requires=["Paste~=3.2", "SQLAlchemy==0.8.3", "bottle~=0.12"],
-    classifiers=["Programming Language :: Python :: 2.7"],
+    classifiers=[
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3.7",
+    ],
     entry_points={"console_scripts": ["techrec = techrec.cli:main"]},
     zip_safe=False,
     install_package_data=True,