]> www.vanbest.org Git - motioneye-debian/commitdiff
more config code
authorCalin Crisan <ccrisan@gmail.com>
Sun, 22 Sep 2013 09:08:06 +0000 (12:08 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Sun, 22 Sep 2013 09:44:28 +0000 (12:44 +0300)
doc/todo.txt
src/config.py
src/handlers.py
src/motionctl.py [new file with mode: 0644]
src/server.py
src/smbctl.py [new file with mode: 0644]
src/v4l2ctl.py [new file with mode: 0644]

index 9cc7c3eb9a9b15525921ee7b9c1d3458960eef9c..515ffc10f8ec2e35557081b45fa574de7696db96 100644 (file)
@@ -1,9 +1,9 @@
--> config.py functions should make use of exceptions rather than returning None
-
 -> browser compatibility test
+-> hint text next to section titles
 -> authentication
 -> proxy for slave motioneyes
 -> add a view log functionality
 -> click to zoom on cameras
 -> add a previewer for movies
 -> add a previewer for snapshots
+-> other todos
\ No newline at end of file
index a268f1cdf4a2d9041ec9b48345bcb1796b9366ba..6590e3665ef14583940f5bba466c02a39867abef 100644 (file)
@@ -3,7 +3,6 @@ import errno
 import json
 import logging
 import os.path
-import re
 
 import settings
 
@@ -17,6 +16,8 @@ _CAMERA_CONFIG_FILE_PATH = os.path.join(_CONFIG_DIR, _CAMERA_CONFIG_FILE_NAME)
 
 
 def get_general():
+    # TODO use a cache
+    
     config_file_path = os.path.join(settings.PROJECT_PATH, _GENERAL_CONFIG_FILE_PATH)
     
     logging.info('reading general config from file %(path)s...' % {'path': config_file_path})
@@ -34,7 +35,7 @@ def get_general():
             logging.error('could not open config file %(path)s: %(msg)s' % {
                     'path': config_file_path, 'msg': unicode(e)})
             
-            return None
+            raise
     
     try:
         data = json.load(file)
@@ -46,13 +47,15 @@ def get_general():
         logging.error('could not read config file %(path)s: %(msg)s' % {
                 'path': config_file_path, 'msg': unicode(e)})
         
+        raise
+        
     finally:
         file.close()
         
-    return None
-
 
 def set_general(data):
+    # TODO use a cache
+    
     _set_default_general(data)
 
     config_file_path = os.path.join(settings.PROJECT_PATH, _GENERAL_CONFIG_FILE_PATH)
@@ -66,10 +69,10 @@ def set_general(data):
         logging.error('could not open config file %(path)s for writing: %(msg)s' % {
                 'path': config_file_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     try:
-        json.dump(file, data)
+        json.dump(data, file)
     
     except Exception as e:
         logging.error('could not write config file %(path)s: %(msg)s' % {
@@ -77,70 +80,13 @@ def set_general(data):
         
     finally:
         file.close()
-        
+
     return data
 
 
-def get_cameras():
-    config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR)
-    
-    logging.info('loading cameras from directory %(path)s...' % {'path': config_path})
-    
-    try:
-        ls = os.listdir(config_path)
-        
-    except Exception as e:
-        logging.error('could not list contents of %(dir)s: %(msg)s' % {
-                'dir': config_path, 'msg': unicode(e)})
-        
-        return None
-    
-    cameras = {}
-    
-    pattern = _CAMERA_CONFIG_FILE_NAME.replace('%(id)s', '(\w+)')
-    for name in ls:
-        match = re.match(pattern, name)
-        if not match:
-            continue # not a camera config file
-        
-        camera_id = match.groups()[0]
-        
-        camera_config_path = os.path.join(config_path, name)
-        
-        logging.info('reading camera config from %(path)s...' % {'path': camera_config_path})
-        
-        try:
-            file = open(camera_config_path, 'r')
-        
-        except Exception as e:
-            logging.error('could not open camera config file %(path)s: %(msg)s' % {
-                    'path': camera_config_path, 'msg': unicode(e)})
-            
-            continue
-        
-        try:
-            lines = [l[:-1] for l in file.readlines()]
-        
-        except Exception as e:
-            logging.error('could not read camera config file %(path)s: %(msg)s' % {
-                    'path': camera_config_path, 'msg': unicode(e)})
-            
-            continue
-        
-        finally:
-            file.close()
-        
-        data = _conf_to_dict(lines)
-        _set_default_motion_camera(data)
-        
-        cameras[camera_id] = data
-        
-    logging.info('loaded %(count)d cameras' % {'count': len(cameras)})
-    
-    return cameras
-        
-
 def get_camera(camera_id):
+    # TODO use a cache
+    
     config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR)
     camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id})
     
@@ -152,7 +98,7 @@ def get_camera(camera_id):
     except Exception as e:
         logging.error('could not open camera config file: %(msg)s' % {'msg': unicode(e)})
         
-        return None
+        raise
     
     try:
         lines = [l[:-1] for l in file.readlines()]
@@ -161,18 +107,17 @@ def get_camera(camera_id):
         logging.error('could not read camera config file %(path)s: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     finally:
         file.close()
     
-    data = _conf_to_dict(lines)
-    _set_default_motion_camera(data)
-    
-    return data
+    return _conf_to_dict(lines)
 
 
 def set_camera(camera_id, data):
+    # TODO use a cache
+    
     config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR)
     camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id})
     
@@ -187,7 +132,7 @@ def set_camera(camera_id, data):
         logging.error('could not open camera config file %(path)s: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     try:
         lines = [l[:-1] for l in file.readlines()]
@@ -196,7 +141,7 @@ def set_camera(camera_id, data):
         logging.error('could not read camera config file %(path)s: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     finally:
         file.close()
@@ -212,7 +157,7 @@ def set_camera(camera_id, data):
         logging.error('could not open camera config file %(path)s for writing: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     lines = _dict_to_conf(lines, data)
     
@@ -223,7 +168,7 @@ def set_camera(camera_id, data):
         logging.error('could not write camera config file %(path)s: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     finally:
         file.close()
@@ -231,41 +176,14 @@ def set_camera(camera_id, data):
     return data
 
 
-def add_camera():
-    config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR)
-    
-    logging.info('loading cameras from directory %(path)s...' % {'path': config_path})
-    
-    try:
-        ls = os.listdir(config_path)
-        
-    except Exception as e:
-        logging.error('could not list contents of %(dir)s: %(msg)s' % {
-                'dir': config_path, 'msg': unicode(e)})
-        
-        return None
+def add_camera(device):
+    # TODO use a cache
     
-    camera_ids = []
-    
-    pattern = _CAMERA_CONFIG_FILE_NAME.replace('%(id)s', '(\w+)')
-    for name in ls:
-        match = re.match(pattern, name)
-        if not match:
-            continue # not a camera config file
-        
-        camera_id = match.groups()[0]
-        try:
-            camera_id = int(camera_id)
-        
-        except ValueError:
-            logging.error('camera id is not an integer: %(id)s' % {'id': camera_id})
-            
-            continue
-            
-        camera_ids.append(camera_id)
-        
-        logging.debug('found camera with id %(id)s' % {'id': camera_id})
+    # determine the last camera id
     
+    cameras = get_general().get('cameras', {})
+    camera_ids = [int(k) for k in cameras.iterkeys()]
+
     last_camera_id = max(camera_ids or [0])
     camera_id = last_camera_id + 1
     
@@ -273,6 +191,7 @@ def add_camera():
         
     # write the configuration to file
     
+    config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR)
     camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id})
     logging.info('writing camera config to %(path)s...' % {'path': camera_config_path})
     
@@ -283,10 +202,11 @@ def add_camera():
         logging.error('could not open camera config file %(path)s for writing: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
-    data = {}
-    _set_default_motion_camera(data)
+    # add the default camera config
+    ui = camera_dict_to_ui(camera_id, {})
+    data = camera_ui_to_dict(camera_id, ui)
     
     lines = _dict_to_conf([], data)
     
@@ -297,15 +217,32 @@ def add_camera():
         logging.error('could not write camera config file %(path)s: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
     
     finally:
         file.close()
     
-    return camera_id, data
+    # add the camera to the general config
+    
+    cameras[camera_id] = {
+        'name': 'camera' + str(camera_id),
+        'device': device,
+        'enabled': True
+    }
+    
+    general_config = get_general()
+    general_config['cameras'] = cameras
+    
+    set_general(general_config)
+    
+    return camera_id, cameras[camera_id]['name'], data
 
 
 def rem_camera(camera_id):
+    # TODO use a cache
+    
+    # TODO remove the camera from general config as well
+    
     config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR)
     camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id})
     
@@ -318,7 +255,220 @@ def rem_camera(camera_id):
         logging.error('could not remove camera config file %(path)s: %(msg)s' % {
                 'path': camera_config_path, 'msg': unicode(e)})
         
-        return None
+        raise
+
+
+def camera_ui_to_dict(camera_id, ui):
+    cameras = get_general().get('cameras', {})
+    camera_info = cameras.get(camera_id, {})
+    camera_name = camera_info.get('name', '(unknown)')
+
+    data = {
+        # device
+        'lightswitch': int(ui.get('light_switch_detect', False) * 5),
+        'auto_brightness': ui.get('auto_brightness', False),
+        'brightness': int(int(ui.get('brightness', 0)) * 2.55),
+        'contrast': int(int(ui.get('contrast', 0)) * 2.55),
+        'saturation': int(int(ui.get('saturation', 0)) * 2.55),
+        'hue': int(int(ui.get('hue', 0))),
+        'width': int(ui.get('resolution', '352x288').split('x')[0]),
+        'height': int(ui.get('resolution', '352x288').split('x')[1]),
+        'framerate': int(ui.get('framerate', 1)),
+        
+        # text overlay
+        'text_left': '',
+        'text_right': '',
+        
+        # streaming
+        'webcam_localhost': not ui.get('video_streaming', True),
+        'webcam_port': int(ui.get('streaming_port', 8080)),
+        'webcam_maxrate': int(ui.get('streaming_framerate', 1)),
+        'webcam_quality': max(1, int(ui.get('streaming_quality', 50))),
+        
+        # still images
+        'output_normal': False,
+        'output_all': False,
+        'output_motion': False,
+        'snapshot_interval': 0,
+        'jpeg_filename': '',
+        'snapshot_filename': '',
+        # TODO preserve images
+        
+        # movies
+        'ffmpeg_variable_bitrate': 0,
+        'ffmpeg_video_codec': 'mpeg4',
+        'ffmpeg_cap_new': True,
+        'movie_filename': '',
+        # TODO preserve movies
+    
+        # motion detection
+        'text_changes': ui.get('show_frame_changes', False),
+        'locate': ui.get('show_frame_changes', False),
+        'threshold': ui.get('frame_change_threshold', 1500),
+        'noise_tune': ui.get('auto_noise_detect', True),
+        'noise_level': max(1, int(int(ui.get('noise_level', 8)) * 2.55)),
+        'gap': int(ui.get('gap', 60)),
+        'pre_capture': int(ui.get('pre_capture', 0)),
+        'post_capture': int(ui.get('post_capture', 0)),
+        
+        # TODO notifications
+    }
+    
+    if ui.get('text_overlay', False):
+        left_text = ui.get('left_text', 'camera-name')
+        if left_text == 'camera-name':
+            data['text_left'] = camera_name
+            
+        elif left_text == 'timestamp':
+            data['text_left'] = '%Y-%m-%d\n%T'
+            
+        else:
+            data['text_left'] = ui.get('custom_left_text', '')
+        
+        right_text = ui.get('right_text', 'timestamp')
+        if right_text == 'camera-name':
+            data['text_right'] = camera_name
+            
+        elif right_text == 'timestamp':
+            data['text_right'] = '%Y-%m-%d\n%T'
+            
+        else:
+            data['text_right'] = ui.get('custom_right_text', '')
+
+    if ui.get('still_images', False):
+        capture_mode = ui.get('capture_mode', 'motion-triggered')
+        if capture_mode == 'motion-triggered':
+            data['output_normal'] = True
+            data['jpeg_filename'] = ui.get('image_file_name', '%Y-%m-%d-%H-%M-%S-%q')  
+            
+        elif capture_mode == 'interval-snapshots':
+            data['snapshot_interval'] = int(ui.get('snapshot_interval'), 300)
+            data['snapshot_filename'] = ui.get('image_file_name', '%Y-%m-%d-%H-%M-%S-%q')
+            
+        elif capture_mode == 'all-frames':
+            data['output_all'] = True
+            data['jpeg_filename'] = ui.get('image_file_name', '%Y-%m-%d-%H-%M-%S')
+            
+        data['quality'] = max(1, int(ui.get('image_quality', 75)))
+        
+    if ui.get('motion_movies', False):
+        data['ffmpeg_variable_bitrate'] = 2 + int((100 - int(ui.get('movie_quality', 50))) * 0.29)
+        data['movie_filename'] = ui.get('movie_file_name', '%Y-%m-%d-%H-%M-%S-%q')
+
+    return data
+    
+
+def camera_dict_to_ui(camera_id, data):
+    # this is where the default values come from
+    
+    cameras = get_general().get('cameras', {})
+    camera_info = cameras.get(camera_id, {})
+    camera_name = camera_info.get('name', '(unknown)')
+    
+    ui = {
+        # device
+        'light_switch_detect': data.get('lightswitch', 0) > 0,
+        'auto_brightness': data.get('auto_brightness', False),
+        'brightness': int(int(data.get('brightness', 0)) / 2.55),
+        'contrast': int(int(data.get('contrast', 0)) / 2.55),
+        'saturation': int(int(data.get('saturation', 0)) / 2.55),
+        'hue': int(int(data.get('hue', 0))),
+        'resolution': str(data.get('width', 352)) + 'x' + str(data.get('height', 288)),
+        'framerate': int(data.get('framerate', 1)),
+        
+        # text overlay
+        'text_overlay': False,
+        'left_text': 'camera-name',
+        'right_text': 'timestamp',
+        
+        # streaming
+        'vudeo_streaming': not data.get('webcam_localhost', False),
+        'streaming_port': int(data.get('webcam_port', 8080)),
+        'streaming_framerate': int(data.get('webcam_maxrate', 1)),
+        'streaming_quality': int(data.get('webcam_quality', 50)),
+        
+        # still images
+        'still_images': False,
+        'capture_mode': 'motion-triggered',
+        'image_file_name': '%Y-%m-%d-%H-%M-%S',
+        'image_quality': 75,
+        # TODO preserve images
+        
+        # motion movies
+        'motion_movies': False,
+        'movie_quality': 50,
+        'movie_file_name': '%Y-%m-%d-%H-%M-%S-%q',
+        # TODO preserve movies
+        
+        # motion detection
+        'show_frame_changes': data.get('text_changes') or data.get('locate'),
+        'frame_change_threshold': data.get('threshold', 1500),
+        'auto_noise_detect': data.get('noise_tune', True),
+        'noise_level': int(int(data.get('noise_level', 32)) / 2.55),
+        'gap': int(data.get('gap', 60)),
+        'pre_capture': int(data.get('pre_capture', 0)),
+        'post_capture': int(data.get('post_capture', 0)),
+        
+        # TODO notifications
+    }
+    
+    text_left = data.get('text_left', '')
+    text_right = data.get('text_right', '') 
+    if text_left or text_right:
+        ui['text_overlay'] = True
+        
+        if text_left == camera_name:
+            ui['left_text'] = 'camera-name'
+            
+        elif text_left == '%Y-%m-%d\n%T':
+            ui['left_text'] = 'timestamp'
+            
+        else:
+            ui['left_text'] = 'custom-text'
+            ui['custom_left_text'] = text_left
+
+        if text_right == camera_name:
+            ui['right_text'] = 'camera-name'
+            
+        elif text_right == '%Y-%m-%d\n%T':
+            ui['right_text'] = 'timestamp'
+            
+        else:
+            ui['right_text'] = 'custom-text'
+            ui['custom_right_text'] = text_right
+
+    output_all = data.get('output_all')
+    output_normal = data.get('output_normal')
+    jpeg_filename = data.get('jpeg_filename')
+    snapshot_interval = data.get('snapshot_interval')
+    snapshot_filename = data.get('snapshpt_filename')
+    
+    if (((output_all or output_normal) and jpeg_filename) or
+        (snapshot_interval and snapshot_filename)):
+        
+        ui['still_images'] = True
+        
+        if output_all:
+            ui['capture_mode'] = 'all-frames'
+            ui['image_file_name'] = jpeg_filename
+            
+        elif data.get('snapshot_interval'):
+            ui['capture-mode'] = 'interval-snapshots'
+            ui['image_file_name'] = snapshot_filename
+            
+        elif data.get('output_normal'):
+            ui['capture-mode'] = 'motion-triggered'
+            ui['image_file_name'] = jpeg_filename  
+            
+        ui['image_quality'] = ui.get('quality', 75)
+    
+    movie_filename = data.get('movie_filename')
+    if movie_filename:
+        ui['motion_movies'] = True
+        ui['movie_quality'] = int((max(2, data.get('ffmpeg_variable_bitrate', 14)) - 2) / 0.29)
+        ui['movie_file_name'] = movie_filename
+    
+    return data
     
 
 def _value_to_python(value):
@@ -419,17 +569,16 @@ def _dict_to_conf(lines, data):
 
 
 def _set_default_general(data):
-    data.set_default('show_advanced', False)
-    data.set_default('admin_username', 'admin')
-    data.set_default('admin_password', '')
-    data.set_default('normal_username', 'user')
-    data.set_default('storage_device', 'local-disk')
-    data.set_default('root_directory', '/')
+    data.setdefault('general_enabled', True)
+    data.setdefault('show_advanced', False)
+    data.setdefault('admin_username', 'admin')
+    data.setdefault('admin_password', '')
+    data.setdefault('normal_username', 'user')
+    data.setdefault('storage_device', 'local-disk')
+    data.setdefault('root_directory', '/')
+    data.setdefault('cameras', {})
 
 
 def _set_default_motion(data):
-    pass
-
+    pass # TODO
 
-def _set_default_motion_camera(data):
-    pass
index dedd2c7bf72d6121379b2ea35d606b45f6304029..7db78c4a40277a95bdbed9ca7acd8e23bade35d7 100644 (file)
@@ -4,6 +4,7 @@ import logging
 
 from tornado.web import RequestHandler, HTTPError
 
+import config
 import template
 
 
@@ -46,24 +47,61 @@ class ConfigHandler(BaseHandler):
             raise HTTPError(400, 'unknown operation')
     
     def get_config(self, camera_id):
+        general_config = config.get_general()
+        
         if camera_id:
-            logging.debug('getting config for camera %(id)s' % {'camera': camera_id})
+            logging.debug('getting config for camera %(id)s' % {'id': camera_id})
+            
+            cameras = general_config.get('cameras', {})
+            if camera_id not in cameras:
+                raise HTTPError(404, 'no such camera')
+            
+            self.finish_json(config.get_camera(camera_id))
             
         else:
             logging.debug('getting general config')
+            
+            self.finish_json(general_config)
     
     def set_config(self, camera_id):
+        general_config = config.get_general()
+        
+        try:
+            data = json.loads(self.request.body)
+            
+        except Exception as e:
+            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+            
+            raise
+        
         if camera_id:
-            logging.debug('setting config for camera %(id)s' % {'camera': camera_id})
+            logging.debug('setting config for camera %(id)s' % {'id': camera_id})
+            
+            cameras = general_config.get('cameras', {})
+            if camera_id not in cameras:
+                raise HTTPError(404, 'no such camera')
             
+            config.set_camera(camera_id, data)
+
         else:
             logging.debug('setting general config')
+            
+            try:
+                data = json.loads(self.request.body)
+                
+            except Exception as e:
+                logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+                
+                raise
+            
+            general_config.update(data)
+            config.set_general(general_config)
     
     def add_camera(self):
         logging.debug('adding new camera')
     
     def rem_camera(self, camera_id):
-        logging.debug('removing camera %(id)s' % {'camera': camera_id})
+        logging.debug('removing camera %(id)s' % {'id': camera_id})
 
 
 class SnapshotHandler(BaseHandler):
@@ -84,11 +122,11 @@ class SnapshotHandler(BaseHandler):
         pass
     
     def list(self, camera_id):
-        logging.debug('listing snapshots for camera %(id)s' % {'camera': camera_id})
+        logging.debug('listing snapshots for camera %(id)s' % {'id': camera_id})
     
     def download(self, camera_id, filename):
         logging.debug('downloading snapshot %(filename)s of camera %(id)s' % {
-                'filename': filename, 'camera': camera_id})
+                'filename': filename, 'id': camera_id})
 
 
 class MovieHandler(BaseHandler):
@@ -103,8 +141,8 @@ class MovieHandler(BaseHandler):
             raise HTTPError(400, 'unknown operation')
     
     def list(self, camera_id):
-        logging.debug('listing movies for camera %(id)s' % {'camera': camera_id})
+        logging.debug('listing movies for camera %(id)s' % {'id': camera_id})
     
     def download(self, camera_id, filename):
         logging.debug('downloading movie %(filename)s of camera %(id)s' % {
-                'filename': filename, 'camera': camera_id})
+                'filename': filename, 'id': camera_id})
diff --git a/src/motionctl.py b/src/motionctl.py
new file mode 100644 (file)
index 0000000..e69de29
index 19c228be9022c2d83cf25f802e4fe915fbd954f3..9daeb524158d471c6ee21f0e8ae1b39f1f69ee7d 100644 (file)
@@ -9,13 +9,13 @@ import template
 application = Application(
     [
         (r'^/$', handlers.MainHandler),
-        (r'^/config/(?P<camera_id>\w+)/(?P<op>get|set|rem)/?$', handlers.ConfigHandler),
-        (r'^/config/(?P<op>add)/?$', handlers.ConfigHandler),
         (r'^/config/general/(?P<op>set|get)/?$', handlers.ConfigHandler),
-        (r'^/snapshot/(?P<camera_id>\w+)/(?P<op>current|list)/?$', handlers.SnapshotHandler),
-        (r'^/snapshot/(?P<camera_id>\w+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.SnapshotHandler),
-        (r'^/movie/(?P<camera_id>\w+)/(?P<op>list)/?$', handlers.MovieHandler),
-        (r'^/movie/(?P<camera_id>\w+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.MovieHandler),
+        (r'^/config/(?P<camera_id>\d+)/(?P<op>get|set|rem)/?$', handlers.ConfigHandler),
+        (r'^/config/(?P<op>add)/?$', handlers.ConfigHandler),
+        (r'^/snapshot/(?P<camera_id>\d+)/(?P<op>current|list)/?$', handlers.SnapshotHandler),
+        (r'^/snapshot/(?P<camera_id>\d+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.SnapshotHandler),
+        (r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
+        (r'^/movie/(?P<camera_id>\d+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.MovieHandler),
     ],
     debug=settings.DEBUG,
     static_path=settings.STATIC_PATH,
diff --git a/src/smbctl.py b/src/smbctl.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/v4l2ctl.py b/src/v4l2ctl.py
new file mode 100644 (file)
index 0000000..e69de29