]> www.vanbest.org Git - motioneye-debian/commitdiff
major refactoring of timelapse feature, zipped download and remote
authorCalin Crisan <ccrisan@gmail.com>
Sun, 28 Dec 2014 16:41:42 +0000 (18:41 +0200)
committerCalin Crisan <ccrisan@gmail.com>
Sun, 28 Dec 2014 16:41:42 +0000 (18:41 +0200)
motioneye cameras

12 files changed:
eventrelay.py
motioneye.py
settings_default.py
src/handlers.py
src/mediafiles.py
src/remote.py
src/utils.py
static/css/main.css
static/css/ui.css
static/js/main.js
static/js/ui.js
templates/main.html

index 8d66bb78b6911355dccb0af3d8db6c4ad6e7c295..ca53da0801c7b1f3df25ff707b791b179ac205a5 100755 (executable)
@@ -18,6 +18,7 @@
 
 import errno
 import hashlib
+import json
 import logging
 import os.path
 import sys
@@ -122,18 +123,22 @@ if __name__ == '__main__':
     
     admin_username, admin_password = get_admin_credentials()
 
-    uri = '/config/%(camera_id)s/_relay_event/?event=%(event)s&username=%(username)s' % {
+    uri = '/config/%(camera_id)s/_relay_event/?event=%(event)s&_username=%(username)s' % {
             'username': admin_username,
             'camera_id': camera_id,
             'event': event}
     
     signature = compute_signature('POST', uri, '', admin_password)
     
-    url = 'http://127.0.0.1:%(port)s' + uri + '&signature=' + signature
+    url = 'http://127.0.0.1:%(port)s' + uri + '&_signature=' + signature
     url = url % {'port': settings.PORT}
     
     try:
-        urllib.urlopen(url, data='')
+        response = urllib.urlopen(url, data='')
+        response = json.load(response)
+        if response.get('error'):
+            raise Exception(response['error'])
+        
         logging.debug('event successfully relayed')
     
     except Exception as e:
index 6b28b8e39e58145707ff55f569fa5054bdb1d9ae..508e6ad636444b4ccfe2b4d456231f05ab1f8e5c 100755 (executable)
@@ -65,7 +65,6 @@ def _configure_settings():
     set_default_setting('SMTP_TIMEOUT', 60)
     set_default_setting('NOTIFY_MEDIA_TIMESPAN', 5)
     set_default_setting('ZIP_TIMEOUT', 500)
-    set_default_setting('TIMELAPSE_TIMEOUT', 500)
 
     length = len(sys.argv) - 1
     for i in xrange(length):
@@ -150,9 +149,9 @@ def _test_requirements():
             print('SMB_SHARES require root privileges')
             return False
 
-        if settings.ENABLE_REBOOT:
-            print('reboot requires root privileges')
-            return False
+        if settings.ENABLE_REBOOT:
+            print('reboot requires root privileges')
+            return False
 
     try:
         import tornado  # @UnusedImport
index 4a9e9a20feedaadc76ee93e08f52740debf67d1a..253893e09ed3120d3c7c9bcb75adfc003c808c40 100644 (file)
@@ -82,6 +82,3 @@ NOTIFY_MEDIA_TIMESPAN = 5
 
 # the time to wait for zip file creation
 ZIP_TIMEOUT = 500
-
-# the time to wait for timelapse movie file creation
-TIMELAPSE_TIMEOUT = 500
index 9bf912af3da7a52c1cbd22158a711168a63afcc3..e2db4b6539e1a4c8bbed74b99f29ab232621ac15 100644 (file)
@@ -69,8 +69,8 @@ class BaseHandler(RequestHandler):
     def get_current_user(self):
         main_config = config.get_main()
         
-        username = self.get_argument('username', None)
-        signature = self.get_argument('signature', None)
+        username = self.get_argument('_username', None)
+        signature = self.get_argument('_signature', None)
         if (username == main_config.get('@admin_username') and
             signature == utils.compute_signature(self.request.method, self.request.uri, self.request.body, main_config.get('@admin_password'))):
             
@@ -108,8 +108,10 @@ class BaseHandler(RequestHandler):
     def auth(admin=False, prompt=True):
         def decorator(func):
             def wrapper(self, *args, **kwargs):
+                _admin = self.get_argument('_admin', None) == 'true'
+                
                 user = self.current_user
-                if (user is None) or (user != 'admin' and admin):
+                if (user is None) or (user != 'admin' and (admin or _admin)):
                     self.set_header('Content-Type', 'application/json')
                     return self.finish_json({'error': 'unauthorized', 'prompt': prompt})
 
@@ -593,7 +595,7 @@ class ConfigHandler(BaseHandler):
         else: # remote camera
             def on_response(remote_ui_config=None, error=None):
                 if error:
-                    self.finish_json({'error': error})
+                    return self.finish_json({'error': error})
 
                 for key, value in camera_config.items():
                     remote_ui_config[key.replace('@', '')] = value
@@ -725,7 +727,7 @@ class PictureHandler(BaseHandler):
                 self.set_cookie('motion_detected_' + str(camera_id), str(motion_detected).lower())
                 self.try_finish(picture)
             
-            remote.get_current_picture(camera_config, on_response, width=width, height=height)
+            remote.get_current_picture(camera_config, width=width, height=height, callback=on_response)
 
     @BaseHandler.auth()
     def list(self, camera_id):
@@ -753,7 +755,7 @@ class PictureHandler(BaseHandler):
 
                 self.finish_json(remote_list)
             
-            remote.list_media(camera_config, on_response, media_type='picture', prefix=self.get_argument('prefix', None))
+            remote.list_media(camera_config, media_type='picture', prefix=self.get_argument('prefix', None), callback=on_response)
 
     @BaseHandler.auth()
     def frame(self, camera_id):
@@ -809,7 +811,7 @@ class PictureHandler(BaseHandler):
                 
                 self.finish(response)
 
-            remote.get_media_content(camera_config, on_response, filename=filename, media_type='picture')
+            remote.get_media_content(camera_config, filename=filename, media_type='picture', callback=on_response)
 
     @BaseHandler.auth()
     def preview(self, camera_id, filename):
@@ -842,9 +844,10 @@ class PictureHandler(BaseHandler):
                 
                 self.finish(content)
             
-            remote.get_media_preview(camera_config, on_response, filename=filename, media_type='picture',
+            remote.get_media_preview(camera_config, filename=filename, media_type='picture',
                     width=self.get_argument('width', None),
-                    height=self.get_argument('height', None))
+                    height=self.get_argument('height', None),
+                    callback=on_response)
     
     @BaseHandler.auth(admin=True)
     def delete(self, camera_id, filename):
@@ -868,38 +871,52 @@ class PictureHandler(BaseHandler):
 
                 self.finish_json()
 
-            remote.del_media_content(camera_config, on_response, filename=filename, media_type='picture')
+            remote.del_media_content(camera_config, filename=filename, media_type='picture', callback=on_response)
 
     @BaseHandler.auth()
     def zipped(self, camera_id, group):
         key = self.get_argument('key', None)
+        camera_config = config.get_camera(camera_id)
+        
         if key:
             logging.debug('serving zip file for group %(group)s of camera %(id)s with key %(key)s' % {
                     'group': group, 'id': camera_id, 'key': key})
             
-            data = mediafiles.get_prepared_cache(key)
-            if not data:
-                logging.error('prepared cache data for key "%s" does not exist' % key)
-                
-                raise HTTPError(404, 'no such key')
-
-            camera_config = config.get_camera(camera_id)
             if utils.local_camera(camera_config):
-                pretty_filename = camera_config['@name'] + '_' + group
-                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
-     
+                data = mediafiles.get_prepared_cache(key)
+                if not data:
+                    logging.error('prepared cache data for key "%s" does not exist' % key)
+                    
+                    raise HTTPError(404, 'no such key')
+    
+                camera_config = config.get_camera(camera_id)
+                if utils.local_camera(camera_config):
+                    pretty_filename = camera_config['@name'] + '_' + group
+                    pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
+         
+                else: # remote camera
+                    pretty_filename = re.sub('[^a-zA-Z0-9]', '_', group)
+    
+                self.set_header('Content-Type', 'application/zip')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.zip;')
+                self.finish(data)
+                
             else: # remote camera
-                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', group)
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to download zip file from %(url)s: %(msg)s.' % {
+                                'url': remote.make_camera_url(camera_config), 'msg': error}})
 
-            self.set_header('Content-Type', 'application/zip')
-            self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.zip;')
-            self.finish(data)
+                    self.set_header('Content-Type', response['content_type'])
+                    self.set_header('Content-Disposition', response['content_disposition'])
+                    self.finish(response['data'])
 
-        else:
+                remote.get_zipped_content(camera_config, media_type='picture', key=key, callback=on_response)
+
+        else: # prepare
             logging.debug('preparing zip file for group %(group)s of camera %(id)s' % {
                     'group': group, 'id': camera_id})
 
-            camera_config = config.get_camera(camera_id)
             if utils.local_camera(camera_config):
                 def on_zip(data):
                     if data is None:
@@ -910,78 +927,124 @@ class PictureHandler(BaseHandler):
                             'group': group, 'id': camera_id, 'key': key})
                     self.finish_json({'key': key})
     
-                mediafiles.get_zipped_content(camera_config, media_type='picture', callback=on_zip, group=group)
+                mediafiles.get_zipped_content(camera_config, media_type='picture', group=group, callback=on_zip)
     
             else: # remote camera
                 def on_response(response=None, error=None):
                     if error:
-                        return self.finish_json({'error': 'Failed to download zip file from %(url)s: %(msg)s.' % {
+                        return self.finish_json({'error': 'Failed to make zip file at %(url)s: %(msg)s.' % {
                                 'url': remote.make_camera_url(camera_config), 'msg': error}})
-     
-                    key = mediafiles.set_prepared_cache(response)
-                    logging.debug('prepared zip file for group %(group)s of camera %(id)s with key %(key)s' % {
-                            'group': group, 'id': camera_id, 'key': key})
-                    self.finish_json({'key': key})
 
-                remote.get_zipped_content(camera_config, media_type='picture', callback=on_response, group=group)
+                    self.finish_json({'key': response['key']})
+
+                remote.make_zipped_content(camera_config, media_type='picture', group=group, callback=on_response)
 
     @BaseHandler.auth()
     def timelapse(self, camera_id, group):
         key = self.get_argument('key', None)
-        if key:
+        check = self.get_argument('check', False)
+        camera_config = config.get_camera(camera_id)
+
+        if key: # download
             logging.debug('serving timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
                     'group': group, 'id': camera_id, 'key': key})
             
-            data = mediafiles.get_prepared_cache(key)
-            if not data:
-                logging.error('prepared cache data for key "%s" does not exist' % key)
-                
-                raise HTTPError(404, 'no such key')
+            if utils.local_camera(camera_config):
+                data = mediafiles.get_prepared_cache(key)
+                if data is None:
+                    logging.error('prepared cache data for key "%s" does not exist' % key)
+
+                    raise HTTPError(404, 'no such key')
+
+                camera_config = config.get_camera(camera_id)
+                if utils.local_camera(camera_config):
+                    pretty_filename = camera_config['@name'] + '_' + group
+                    pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
+    
+                else: # remote camera
+                    pretty_filename = re.sub('[^a-zA-Z0-9]', '_', group)
+
+                self.set_header('Content-Type', 'video/x-msvideo')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.avi;')
+                self.finish(data)
+
+            else: # remote camera
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to download timelapse movie from %(url)s: %(msg)s.' % {
+                                'url': remote.make_camera_url(camera_config), 'msg': error}})
+
+                    self.set_header('Content-Type', response['content_type'])
+                    self.set_header('Content-Disposition', response['content_disposition'])
+                    self.finish(response['data'])
+
+                remote.get_timelapse_movie(camera_config, key, callback=on_response)
+
+        elif check:
+            logging.debug('checking timelapse movie status for group %(group)s of camera %(id)s' % {
+                    'group': group, 'id': camera_id})
 
-            camera_config = config.get_camera(camera_id)
             if utils.local_camera(camera_config):
-                pretty_filename = camera_config['@name'] + '_' + group
-                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
+                status = mediafiles.check_timelapse_movie()
+                if status['progress'] == -1 and status['data']:
+                    key = mediafiles.set_prepared_cache(status['data'])
+                    logging.debug('prepared timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
+                            'group': group, 'id': camera_id, 'key': key})
+                    self.finish_json({'key': key, 'progress': -1})
+
+                else:
+                    self.finish_json(status)
 
             else: # remote camera
-                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', group)
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to check timelapse movie progress at %(url)s: %(msg)s.' % {
+                                'url': remote.make_camera_url(camera_config), 'msg': error}})
 
-            self.set_header('Content-Type', 'video/x-msvideo')
-            self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.avi;')
-            self.finish(data)
+                    if response['progress'] == -1 and response.get('key'):
+                        self.finish_json({'key': response['key'], 'progress': -1})
+                    
+                    else:
+                        self.finish_json(response)
 
-        else:
+                remote.check_timelapse_movie(camera_config, callback=on_response)
+
+        else: # start timelapse
             interval = int(self.get_argument('interval'))
             framerate = int(self.get_argument('framerate'))
 
             logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with rate %(framerate)s/%(int)s' % {
                     'group': group, 'id': camera_id, 'framerate': framerate, 'int': interval})
 
-            camera_config = config.get_camera(camera_id)
             if utils.local_camera(camera_config):
-                def on_timelapse(data):
-                    if data is None:
-                        return self.finish_json({'error': 'Failed to create timelapse movie file.'})
+                status = mediafiles.check_timelapse_movie()
+                if status['progress'] != -1:
+                    self.finish_json({'progress': status['progress']}) # timelapse already active
 
-                    key = mediafiles.set_prepared_cache(data)
-                    logging.debug('prepared timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
-                            'group': group, 'id': camera_id, 'key': key})
-                    self.finish_json({'key': key})
-
-                mediafiles.get_timelapse_movie(camera_config, framerate, interval, callback=on_timelapse, group=group)
+                else:
+                    mediafiles.make_timelapse_movie(camera_config, framerate, interval, group=group)
+                    self.finish_json({'progress': -1})
 
             else: # remote camera
-                def on_response(response=None, error=None):
+                def on_status(response=None, error=None):
                     if error:
-                        return self.finish_json({'error': 'Failed to download timelapse movie from %(url)s: %(msg)s.' % {
+                        return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % {
                                 'url': remote.make_camera_url(camera_config), 'msg': error}})
-
-                    key = mediafiles.set_prepared_cache(response)
-                    logging.debug('prepared timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
-                            'group': group, 'id': camera_id, 'key': key})
-                    self.finish_json({'key': key})
+                    
+                    if response['progress'] != -1:
+                        return self.finish_json({'progress': response['progress']}) # timelapse already active
+    
+                    def on_make(response=None, error=None):
+                        if error:
+                            return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % {
+                                    'url': remote.make_camera_url(camera_config), 'msg': error}})
     
-                remote.get_timelapse_movie(camera_config, framerate, interval, callback=on_response, group=group)
+                        self.finish_json({'progress': -1})
+                    
+                    remote.make_timelapse_movie(camera_config, framerate, interval, group=group, callback=on_make)
+
+                remote.check_timelapse_movie(camera_config, callback=on_status)
+
 
     @BaseHandler.auth(admin=True)
     def delete_all(self, camera_id, group):
@@ -1005,7 +1068,7 @@ class PictureHandler(BaseHandler):
 
                 self.finish_json()
 
-            remote.del_media_group(camera_config, on_response, group=group, media_type='picture')
+            remote.del_media_group(camera_config, group=group, media_type='picture', callback=on_response)
 
     def try_finish(self, content):
         try:
@@ -1077,7 +1140,7 @@ class MovieHandler(BaseHandler):
 
                 self.finish_json(remote_list)
             
-            remote.list_media(camera_config, on_response, media_type='movie', prefix=self.get_argument('prefix', None))
+            remote.list_media(camera_config, media_type='movie', prefix=self.get_argument('prefix', None), callback=on_response)
     
     @BaseHandler.auth()
     def download(self, camera_id, filename):
@@ -1106,7 +1169,7 @@ class MovieHandler(BaseHandler):
                 
                 self.finish(response)
 
-            remote.get_media_content(camera_config, on_response, filename=filename, media_type='movie')
+            remote.get_media_content(camera_config, filename=filename, media_type='movie', callback=on_response)
 
     @BaseHandler.auth()
     def preview(self, camera_id, filename):
@@ -1139,9 +1202,10 @@ class MovieHandler(BaseHandler):
 
                 self.finish(content)
             
-            remote.get_media_preview(camera_config, on_response, filename=filename, media_type='movie',
+            remote.get_media_preview(camera_config, filename=filename, media_type='movie',
                     width=self.get_argument('width', None),
-                    height=self.get_argument('height', None))
+                    height=self.get_argument('height', None),
+                    callback=on_response)
 
     @BaseHandler.auth(admin=True)
     def delete(self, camera_id, filename):
@@ -1165,7 +1229,7 @@ class MovieHandler(BaseHandler):
 
                 self.finish_json()
 
-            remote.del_media_content(camera_config, on_response, filename=filename, media_type='movie')
+            remote.del_media_content(camera_config, filename=filename, media_type='movie', callback=on_response)
 
     @BaseHandler.auth(admin=True)
     def delete_all(self, camera_id, group):
@@ -1189,7 +1253,7 @@ class MovieHandler(BaseHandler):
 
                 self.finish_json()
 
-            remote.del_media_group(camera_config, on_response, group=group, media_type='movie')
+            remote.del_media_group(camera_config, group=group, media_type='movie', callback=on_response)
 
 
 class UpdateHandler(BaseHandler):
index b03ed36853a4aa0710fa6a91e20a6d920be25cf4..8004d350f73d7648e24730b9346e6ca0e3ee5e7a 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 
 import datetime
+import errno
+import fcntl
+import functools
 import hashlib
 import logging
 import multiprocessing
 import os.path
+import re
 import stat
 import StringIO
 import subprocess
@@ -48,6 +52,9 @@ _previewless_movie_files = []
 # a cache of prepared files (whose preparing time is significant)
 _prepared_files = {}
 
+_timelapse_process = None
+_timelapse_data = None
+
 
 def _list_media_files(dir, exts, prefix=None):
     media_files = []
@@ -313,7 +320,7 @@ def get_media_content(camera_config, path, media_type):
         return None
 
 
-def get_zipped_content(camera_config, media_type, callback, group):
+def get_zipped_content(camera_config, media_type, group, callback):
     target_dir = camera_config.get('target_dir')
 
     if media_type == 'picture':
@@ -405,7 +412,10 @@ def get_zipped_content(camera_config, media_type, callback, group):
     poll_process()
 
 
-def get_timelapse_movie(camera_config, framerate, interval, callback, group):
+def make_timelapse_movie(camera_config, framerate, interval, group):
+    global _timelapse_process
+    global _timelapse_data
+    
     target_dir = camera_config.get('target_dir')
     
     # create a subprocess to retrieve media files
@@ -424,8 +434,9 @@ def get_timelapse_movie(camera_config, framerate, interval, callback, group):
     logging.debug('starting media listing process...')
     
     (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
-    process = [multiprocessing.Process(target=do_list_media, args=(child_pipe, ))]
-    process[0].start()
+    _timelapse_process = multiprocessing.Process(target=do_list_media, args=(child_pipe, ))
+    _timelapse_process.progress = 0
+    _timelapse_process.start()
 
     started = [datetime.datetime.now()]
     media_list = []
@@ -438,24 +449,26 @@ def get_timelapse_movie(camera_config, framerate, interval, callback, group):
         
     def poll_media_list_process():
         ioloop = tornado.ioloop.IOLoop.instance()
-        if process[0].is_alive(): # not finished yet
+        if _timelapse_process.is_alive(): # not finished yet
             now = datetime.datetime.now()
             delta = now - started[0]
-            if delta.seconds < 120:
+            if delta.seconds < 300: # the subprocess has 5 minutes to complete its job
                 ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_media_list_process)
                 read_media_list()
 
             else: # process did not finish within 2 minutes
                 logging.error('timeout waiting for the media listing process to finish')
                 
-                callback(None)
+                _timelapse_process.progress = -1
 
         else: # finished
             read_media_list()
             logging.debug('media listing process has returned %(count)s files' % {'count': len(media_list)})
             
             if not media_list:
-                return callback(None)
+                _timelapse_process.progress = -1
+                
+                return
 
             pictures = select_pictures(media_list)
             make_movie(pictures)
@@ -484,59 +497,89 @@ def get_timelapse_movie(camera_config, framerate, interval, callback, group):
         return selected
 
     def make_movie(pictures):
+        global _timelapse_process
+
         cmd =  'rm -f %(tmp_filename)s;'
         cmd += 'cat %(jpegs)s | ffmpeg -framerate %(framerate)s -f image2pipe -vcodec mjpeg -i - -vcodec mpeg4 -b:v %(bitrate)s -q:v 0 -f avi %(tmp_filename)s'
-        
+
         bitrate = 9999999
 
         cmd = cmd % {
             'tmp_filename': tmp_filename,
-            'jpegs': ' '.join((p['path'] for p in pictures)),
+            'jpegs': ' '.join((('"' + p['path'] + '"') for p in pictures)),
             'framerate': framerate,
             'bitrate': bitrate
         }
         
         logging.debug('executing "%s"' % cmd)
         
-        process[0] = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None, shell=True)
-        started[0] = datetime.datetime.now()
+        _timelapse_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
+        _timelapse_process.progress = 0.01 # 1%
+        
+        # make subprocess stdout pipe non-blocking
+        fd = _timelapse_process.stdout.fileno()
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
 
-        poll_movie_process()
+        poll_movie_process(pictures)
 
-    def poll_movie_process():
+    def poll_movie_process(pictures):
+        global _timelapse_process
+        global _timelapse_data
+        
         ioloop = tornado.ioloop.IOLoop.instance()
-        if process[0].poll() is None: # not finished yet
-            now = datetime.datetime.now()
-            delta = now - started[0]
-            if delta.seconds < 120:
-                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_movie_process)
+        if _timelapse_process.poll() is None: # not finished yet
+            ioloop.add_timeout(datetime.timedelta(seconds=0.5), functools.partial(poll_movie_process, pictures))
 
-            else: # process did not finish within 2 minutes
-                logging.error('timeout waiting for the timelapse movie process to finish')
+            try:
+                output = _timelapse_process.stdout.read()
+            
+            except IOError as e:
+                if e.errno == errno.EAGAIN:
+                    output = ''
                 
-                callback(None)
+                else:
+                    raise
+                
+            frame_index = re.findall('frame=\s*(\d+)', output)
+            try:
+                frame_index = int(frame_index[-1])
+            
+            except (IndexError, ValueError):
+                return
+
+            _timelapse_process.progress = max(0.01, float(frame_index) / len(pictures))
+            
+            logging.debug('timelapse progress: %s' % int(100 * _timelapse_process.progress))
 
         else: # finished
+            _timelapse_process = None
+
             logging.debug('reading timelapse movie file "%s" into memory' % tmp_filename)
 
             try:
                 with open(tmp_filename, mode='r') as f:
-                    data = f.read()
+                    _timelapse_data = f.read()
+
+                logging.debug('timelapse movie process has returned %d bytes' % len(_timelapse_data))
 
-                logging.debug('timelapse movie process has returned %d bytes' % len(data))
-    
             except Exception as e:
                 logging.error('failed to read timelapse movie file "%s": %s' % (tmp_filename, e))
-                return callback(None)
 
             finally:
                 os.remove(tmp_filename)
 
-            callback(data)
-
     poll_media_list_process()
 
 
+def check_timelapse_movie():
+    if _timelapse_process and _timelapse_process.poll() is None:
+        return {'progress': _timelapse_process.progress, 'data': None}
+
+    else:
+        return {'progress': -1, 'data': _timelapse_data}
+
+
 def get_media_preview(camera_config, path, media_type, width, height):
     target_dir = camera_config.get('target_dir')
     full_path = os.path.join(target_dir, path)
@@ -689,7 +732,7 @@ def set_prepared_cache(data):
         if _prepared_files.pop(key, None) is not None:
             logging.warn('key "%s" was still present in the prepared cache, removed' % key)
 
-    timeout = max(settings.ZIP_TIMEOUT, settings.TIMELAPSE_TIMEOUT)
+    timeout = 3600 # the user has 1 hour to download the file after creation
     ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=timeout), clear)
 
     return key
index 77cf5f2d67fdaa10b554e78dca2ba331a784c798..120358fdb10b8b27eb2957f64e994770116f8aac 100644 (file)
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 
+import functools
 import json
 import logging
 
 from tornado.httpclient import AsyncHTTPClient, HTTPRequest
 
 import settings
+import utils
 
 
 def _make_request(host, port, username, password, uri, method='GET', data=None, query=None, timeout=None):
@@ -30,16 +32,42 @@ def _make_request(host, port, username, password, uri, method='GET', data=None,
             'port': ':' + str(port) if port else '',
             'uri': uri or ''}
     
-    if query:
-        url += '?' + '&'.join([(n + '=' + v) for (n, v) in query.iteritems()])
+    query = dict(query or {})
+    query['_username'] = username
+    query['_admin'] = 'true' # always use the admin account
     
+    if url.count('?'):
+        url += '&'
+        
+    else:
+        url += '?'
+    
+    url += '&'.join([(n + '=' + v) for (n, v) in query.iteritems()])
+    url += '&_signature=' + utils.compute_signature(method, url, data, password)
+
     if timeout is None:
         timeout = settings.REMOTE_REQUEST_TIMEOUT
         
-    request = HTTPRequest(url, method, body=data, auth_username=username, auth_password=password,
-            connect_timeout=timeout, request_timeout=timeout)
+    return HTTPRequest(url, method, body=data, connect_timeout=timeout, request_timeout=timeout)
+
+
+def _callback_wrapper(callback):
+    @functools.wraps(callback)
+    def wrapper(response):
+        try:
+            decoded = json.loads(response.body)
+            if decoded['error'] == 'unauthorized':
+                response.error = 'Authentication Error'
+                
+            elif decoded['error']:
+                response.error = decoded['error']
+
+        except:
+            pass
+        
+        return callback(response)
     
-    return request
+    return wrapper
 
 
 def make_camera_url(local_config, camera=True):
@@ -95,10 +123,10 @@ def list_cameras(local_config, callback):
             
             return callback(error=unicode(e))
         
-        return callback(response['cameras'])
+        callback(response['cameras'])
     
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
     
 
 def get_config(local_config, callback):
@@ -140,7 +168,7 @@ def get_config(local_config, callback):
         callback(response)
     
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
     
 
 def set_config(local_config, ui_config, callback):
@@ -180,7 +208,7 @@ def set_config(local_config, ui_config, callback):
         callback()
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
 def set_preview(local_config, controls, callback):
@@ -211,10 +239,10 @@ def set_preview(local_config, controls, callback):
         callback()
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def get_current_picture(local_config, callback, width, height):
+def get_current_picture(local_config, width, height, callback):
     host = local_config.get('@host', local_config.get('host')) 
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -257,10 +285,10 @@ def get_current_picture(local_config, callback, width, height):
         callback(motion_detected, response.body)
     
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def list_media(local_config, callback, media_type, prefix=None):
+def list_media(local_config, media_type, prefix, callback):
     host = local_config.get('@host', local_config.get('host')) 
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -302,10 +330,10 @@ def list_media(local_config, callback, media_type, prefix=None):
         return callback(response)
     
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def get_media_content(local_config, callback, filename, media_type):
+def get_media_content(local_config, filename, media_type, callback):
     host = local_config.get('@host', local_config.get('host'))
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -339,10 +367,10 @@ def get_media_content(local_config, callback, filename, media_type):
         return callback(response.body)
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def get_zipped_content(local_config, media_type, callback, group):
+def make_zipped_content(local_config, media_type, group, callback):
     host = local_config.get('@host', local_config.get('host'))
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -350,7 +378,7 @@ def get_zipped_content(local_config, media_type, callback, group):
     uri = local_config.get('@uri', local_config.get('uri')) or ''
     camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
     
-    logging.debug('downloading zip file for group %(group)s of remote camera %(id)s on %(url)s' % {
+    logging.debug('preparing zip file for group %(group)s of remote camera %(id)s on %(url)s' % {
             'group': group,
             'id': camera_id,
             'url': make_camera_url(local_config)})
@@ -363,9 +391,9 @@ def get_zipped_content(local_config, media_type, callback, group):
     # timeout here is 100 times larger than usual - we expect a big delay
     request = _make_request(host, port, username, password, prepare_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
 
-    def on_prepare(response):
+    def on_response(response):
         if response.error:
-            logging.error('failed to download zip file for group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
+            logging.error('failed to prepare zip file for group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
                     'group': group,
                     'id': camera_id,
                     'url': make_camera_url(local_config),
@@ -375,7 +403,7 @@ def get_zipped_content(local_config, media_type, callback, group):
         
         try:
             key = json.loads(response.body)['key']
-            
+
         except Exception as e:
             logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
                     'url': make_camera_url(local_config),
@@ -383,34 +411,49 @@ def get_zipped_content(local_config, media_type, callback, group):
 
             return callback(error=unicode(e))
 
-        download_uri = uri + '/%(media_type)s/%(id)s/zipped/%(group)s/?key=%(key)s' % {
-                'media_type': media_type,
-                'id': camera_id,
-                'group': group,
-                'key': key}
+        callback({'key': key})
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
 
-        request = _make_request(host, port, username, password, download_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
 
-        def on_download(response):
-            if response.error:
-                logging.error('failed to download zip file for group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
-                        'group': group,
-                        'id': camera_id,
-                        'url': make_camera_url(local_config),
-                        'msg': unicode(response.error)})
+def get_zipped_content(local_config, media_type, key, callback):
+    host = local_config.get('@host', local_config.get('host')) 
+    port = local_config.get('@port', local_config.get('port'))
+    username = local_config.get('@username', local_config.get('username'))
+    password = local_config.get('@password', local_config.get('password'))
+    uri = local_config.get('@uri', local_config.get('uri')) or ''
+    camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
     
-                return callback(error=unicode(response.error))
+    logging.debug('downloading zip file for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': make_camera_url(local_config)})
+    
+    request = _make_request(host, port, username, password, uri + '/%(media_type)s/%(id)s/zipped/nevermind/?key=%(key)s' % {
+            'media_type': media_type,
+            'id': camera_id,
+            'key': key})
 
-            callback(response.body)
+    def on_response(response):
+        if response.error:
+            logging.error('failed to download zip file for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': make_camera_url(local_config),
+                    'msg': unicode(response.error)})
+
+            return callback(error=unicode(response.error))
 
-        http_client = AsyncHTTPClient()
-        http_client.fetch(request, on_download)
+        callback({
+            'data': response.body,
+            'content_type': response.headers.get('Content-Type'),
+            'content_disposition': response.headers.get('Content-Disposition')
+        })
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_prepare)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def get_timelapse_movie(local_config, framerate, interval, callback, group):
+def make_timelapse_movie(local_config, framerate, interval, group, callback):
     host = local_config.get('@host', local_config.get('host'))
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -418,25 +461,24 @@ def get_timelapse_movie(local_config, framerate, interval, callback, group):
     uri = local_config.get('@uri', local_config.get('uri')) or ''
     camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
 
-    logging.debug('downloading timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s' % {
+    logging.debug('making timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s' % {
             'group': group,
             'id': camera_id,
             'framerate': framerate,
             'int': interval,
             'url': make_camera_url(local_config)})
 
-    prepare_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s&framerate=%(framerate)s' % {
+    uri += '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s&framerate=%(framerate)s' % {
             'id': camera_id,
             'int': interval,
             'framerate': framerate,
             'group': group}
+    
+    request = _make_request(host, port, username, password, uri)
 
-    # timeout here is 100 times larger than usual - we expect a big delay
-    request = _make_request(host, port, username, password, prepare_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_prepare(response):
+    def on_response(response):
         if response.error:
-            logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s: %(msg)s' % {
+            logging.error('failed to make timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s: %(msg)s' % {
                     'group': group,
                     'id': camera_id,
                     'url': make_camera_url(local_config),
@@ -447,44 +489,96 @@ def get_timelapse_movie(local_config, framerate, interval, callback, group):
             return callback(error=unicode(response.error))
         
         try:
-            key = json.loads(response.body)['key']
-            
+            response = json.loads(response.body)
+
         except Exception as e:
             logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
                     'url': make_camera_url(local_config),
                     'msg': unicode(e)})
 
             return callback(error=unicode(e))
+        
+        callback(response)
 
-        download_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % {
-                'id': camera_id,
-                'group': group,
-                'key': key}
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
 
-        request = _make_request(host, port, username, password, download_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
 
-        def on_download(response):
-            if response.error:
-                logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s: %(msg)s' % {
-                        'group': group,
-                        'id': camera_id,
-                        'url': make_camera_url(local_config),
-                        'int': interval,
-                        'framerate': framerate,
-                        'msg': unicode(response.error)})
+def check_timelapse_movie(local_config, callback):
+    host = local_config.get('@host', local_config.get('host')) 
+    port = local_config.get('@port', local_config.get('port'))
+    username = local_config.get('@username', local_config.get('username'))
+    password = local_config.get('@password', local_config.get('password'))
+    uri = local_config.get('@uri', local_config.get('uri')) or ''
+    camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
     
-                return callback(error=unicode(response.error))
+    logging.debug('checking timelapse movie status for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': make_camera_url(local_config)})
+    
+    request = _make_request(host, port, username, password, uri + '/picture/%(id)s/timelapse/nevermind/?check=true' % {'id': camera_id})
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to check timelapse movie status for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': make_camera_url(local_config),
+                    'msg': unicode(response.error)})
+
+            return callback(error=unicode(response.error))
+        
+        try:
+            response = json.loads(response.body)
+
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': make_camera_url(local_config),
+                    'msg': unicode(e)})
+
+            return callback(error=unicode(e))
+        
+        callback(response)
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def get_timelapse_movie(local_config, key, callback):
+    host = local_config.get('@host', local_config.get('host')) 
+    port = local_config.get('@port', local_config.get('port'))
+    username = local_config.get('@username', local_config.get('username'))
+    password = local_config.get('@password', local_config.get('password'))
+    uri = local_config.get('@uri', local_config.get('uri')) or ''
+    camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
+    
+    logging.debug('downloading timelapse movie for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': make_camera_url(local_config)})
+    
+    request = _make_request(host, port, username, password, uri + '/picture/%(id)s/timelapse/nevermind/?key=%(key)s' % {
+            'id': camera_id,
+            'key': key})
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to download timelapse movie for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': make_camera_url(local_config),
+                    'msg': unicode(response.error)})
 
-            callback(response.body)
+            return callback(error=unicode(response.error))
 
-        http_client = AsyncHTTPClient()
-        http_client.fetch(request, on_download)
+        callback({
+            'data': response.body,
+            'content_type': response.headers.get('Content-Type'),
+            'content_disposition': response.headers.get('Content-Disposition')
+        })
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_prepare)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def get_media_preview(local_config, callback, filename, media_type, width, height):
+def get_media_preview(local_config, filename, media_type, width, height, callback):
     host = local_config.get('@host', local_config.get('host'))
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -522,13 +616,13 @@ def get_media_preview(local_config, callback, filename, media_type, width, heigh
             
             return callback(error=unicode(response.error))
         
-        return callback(response.body)
+        callback(response.body)
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def del_media_content(local_config, callback, filename, media_type):
+def del_media_content(local_config, filename, media_type, callback):
     host = local_config.get('@host', local_config.get('host'))
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -558,13 +652,13 @@ def del_media_content(local_config, callback, filename, media_type):
             
             return callback(error=unicode(response.error))
         
-        return callback()
+        callback()
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
 
 
-def del_media_group(local_config, callback, group, media_type):
+def del_media_group(local_config, group, media_type, callback):
     host = local_config.get('@host', local_config.get('host'))
     port = local_config.get('@port', local_config.get('port'))
     username = local_config.get('@username', local_config.get('username'))
@@ -577,7 +671,7 @@ def del_media_group(local_config, callback, group, media_type):
             'id': camera_id,
             'url': make_camera_url(local_config)})
     
-    uri += '/%(media_type)s/%(id)s/delete/%(group)s' % {
+    uri += '/%(media_type)s/%(id)s/delete_all/%(group)s/' % {
             'media_type': media_type,
             'id': camera_id,
             'group': group}
@@ -594,7 +688,7 @@ def del_media_group(local_config, callback, group, media_type):
             
             return callback(error=unicode(response.error))
         
-        return callback()
+        callback()
 
     http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
+    http_client.fetch(request, _callback_wrapper(on_response))
index c768105e823d06126f2dd086db81acd2efa3e8f2..502c4fce9cdd7dac24e591d7281e95ec136d19d3 100644 (file)
@@ -292,7 +292,7 @@ def test_netcam_url(data, callback):
 
 def compute_signature(method, uri, body, key):
     parts = list(urlparse.urlsplit(uri))
-    query = [q for q in urlparse.parse_qsl(parts[3]) if (q[0] != 'signature')]
+    query = [q for q in urlparse.parse_qsl(parts[3], keep_blank_values=True) if (q[0] != '_signature')]
     query.sort(key=lambda q: q[0])
     query = urllib.urlencode(query)
     parts[0] = parts[1] = ''
index a50f1ec7e7f02328044e88c04214816d3730c1a1..f8b4d9a2ec32c134458f74c4701794fa5871bbe9 100644 (file)
@@ -430,33 +430,8 @@ input[type=text].working-schedule.number {
     width: 50px;
 }
 
-span.disk-usage-text {
-    vertical-align: middle;
-}
-
-div.disk-usage-bar-container {
-    position: relative;
+#diskUsageProgressBar {
     width: 90%;
-    height: 1em;
-    border: 1px solid #555;
-    vertical-align: middle;
-    margin: 0px 0.2em;
-    text-align: center;
-    line-height: 1em;
-}
-
-div.disk-usage-bar-fill {
-    position: absolute;
-    left: 0px;
-    top: 0px;
-    bottom: 0px;
-    width: 0%;
-    background-color: #555;
-}
-
-span.disk-usage-percent {
-    font-size: 0.8em;
-    position: relative;
 }
 
 div.hidden,
@@ -727,6 +702,16 @@ table.timelapse-dialog select {
     width: 10em;
 }
 
+td.timelapse-warning {
+    font-size: 80%;
+    display: none;
+    color: red;
+    max-width: 20em;
+    text-align: center;
+    white-space: normal;
+    padding-bottom: 1em;
+}
+
 div.media-dialog-delete-all-button {
     margin-top: 0.1em;
     margin-bottom: 0.4em;
index e3017d7197453136a65b49d10c4cb6f6dad89c18..866331553c07fb2291a356b37c3d6e2a31e31f85 100644 (file)
@@ -287,6 +287,35 @@ div.slider-cursor-label {
 }
 
 
+    /* progress bar */
+
+div.progress-bar-container {
+    position: relative;
+    height: 1em;
+    border: 1px solid #555;
+    vertical-align: middle;
+    margin: 0px 0.2em;
+    text-align: center;
+    line-height: 1em;
+}
+
+div.progress-bar-fill {
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    bottom: 0px;
+    width: 0%;
+    background-color: #555;
+    transition: width 0.1s ease;
+}
+
+span.progress-bar-text {
+    vertical-align: middle;
+    font-size: 0.8em;
+    position: relative;
+}
+
+
     /* modal dialogs */
     
 div.modal-glass {
index 0f18d3468d184eb9360b52d2fb22651ca70eb63e..5cc0a1d5f790a7fe02d0139c707d45a1e7a50af0 100644 (file)
@@ -160,9 +160,9 @@ function addAuthParams(method, url, body) {
         url += '&';;
     }
     
-    url += 'username=' + window.username;
+    url += '_username=' + window.username;
     var signature = computeSignature(method, url, body);
-    url += '&signature=' + signature;
+    url += '&_signature=' + signature;
     
     return url;
 }
@@ -497,6 +497,9 @@ function initUI() {
     makeTimeValidator($('#sundayFromEntry'));
     makeTimeValidator($('#sundayToEntry'));
     
+    /* progress bars */
+    makeProgressBar($('#diskUsageProgressBar'));
+    
     /* ui elements that enable/disable other ui elements */
     $('#motionEyeSwitch').change(updateConfigUi);
     $('#showAdvancedSwitch').change(updateConfigUi);
@@ -1134,9 +1137,11 @@ function dict2CameraUi(dict) {
     if (dict['disk_total'] != 0) {
         percent = parseInt(dict['disk_used'] * 100 / dict['disk_total']);
     }
-    $('#diskUsageBarFill').css('width', percent + '%');
-    $('#diskUsageText').html(
-            (dict['disk_used'] / 1073741824).toFixed(1)  + '/' + (dict['disk_total'] / 1073741824).toFixed(1) + ' GB (' + percent + '%)');
+    
+    $('#diskUsageProgressBar').each(function () {
+        this.setProgress(percent);
+        this.setText((dict['disk_used'] / 1073741824).toFixed(1)  + '/' + (dict['disk_total'] / 1073741824).toFixed(1) + ' GB (' + percent + '%)');
+    });
     
     /* text overlay */
     $('#textOverlaySwitch')[0].checked = dict['text_overlay'];
@@ -1883,8 +1888,6 @@ function runLoginDialog(retry) {
                 if (retry) {
                     retry();
                 }
-                
-                //return false;
             }},
             {caption: 'Login', isDefault: true, click: function () {
                 window.username = usernameEntry.val();
@@ -1900,8 +1903,6 @@ function runLoginDialog(retry) {
                 if (retry) {
                     retry();
                 }
-                
-                //return false;
             }}
         ],
     };
@@ -2249,6 +2250,7 @@ function runAddCameraDialog() {
 function runTimelapseDialog(cameraId, groupKey, group) {
     var content = 
             $('<table class="timelapse-dialog">' +
+                '<tr><td colspan="2" class="timelapse-warning"></td></tr>' +
                 '<tr>' +
                     '<td class="dialog-item-label"><span class="dialog-item-label">Group</span></td>' +
                     '<td class="dialog-item-value">' + groupKey + '</td>' +
@@ -2279,6 +2281,12 @@ function runTimelapseDialog(cameraId, groupKey, group) {
 
     var intervalSelect = content.find('#intervalSelect');
     var framerateSlider = content.find('#framerateSlider');
+    var timelapseWarning = content.find('td.timelapse-warning');
+    
+    if (group.length > 1440) { /* one day worth of pictures, taken 1 minute apart */
+        timelapseWarning.html('Given the large number of pictures, creating your timelapse might take a while!');
+        timelapseWarning.css('display', 'table-cell');
+    }
     
     makeSlider(framerateSlider, 1, 100, 0, [
         {value: 1, label: '1'},
@@ -2298,14 +2306,68 @@ function runTimelapseDialog(cameraId, groupKey, group) {
         buttons: 'okcancel',
         content: content,
         onOk: function () {
-            showModalDialog('<div class="modal-progress"></div>', null, null, true);
-            ajax('GET', '/picture/' + cameraId + '/timelapse/' + groupKey + '/',
-                    {interval: intervalSelect.val(), framerate: framerateSlider.val()}, function (data) {
-
-                hideModalDialog(); /* progress */
-                hideModalDialog(); /* timelapse dialog */
-                downloadFile('/picture/' + cameraId + '/timelapse/' + groupKey + '/?key=' + data.key);
+            var progressBar = $('<div style=""></div>');
+            makeProgressBar(progressBar);
+            
+            runModalDialog({
+                title: 'Creating Timelapse Movie...',
+                content: progressBar,
+                stack: true,
+                noKeys: true
             });
+            
+            var url = '/picture/' + cameraId + '/timelapse/' + groupKey + '/';
+            var data = {interval: intervalSelect.val(), framerate: framerateSlider.val()};
+            var first = true;
+            
+            function checkTimelapse() {
+                var actualUrl = url;
+                if (!first) {
+                    actualUrl += '?check=true';
+                }
+
+                ajax('GET', actualUrl, data, function (data) {
+                    if (data == null || data.error) {
+                        hideModalDialog(); /* progress */
+                        hideModalDialog(); /* timelapse dialog */
+                        showErrorMessage(data && data.error);
+                        return;
+                    }
+                    
+                    if (data.progress != -1 && first) {
+                        hideModalDialog(); /* progress */
+                        hideModalDialog(); /* timelapse dialog */
+                        showErrorMessage('A timelapse movie is already being created.');
+                        return;
+                    }
+                    
+                    if (data.progress == -1 && !first && !data.key) {
+                        hideModalDialog(); /* progress */
+                        hideModalDialog(); /* timelapse dialog */
+                        showErrorMessage('The timelapse movie could not be created.');
+                        return;
+                    }
+                    
+                    if (data.progress == -1) {
+                        data.progress = 0;
+                    }
+
+                    if (data.key) {
+                        hideModalDialog(); /* progress */
+                        hideModalDialog(); /* timelapse dialog */
+                        downloadFile('/picture/' + cameraId + '/timelapse/' + groupKey + '/?key=' + data.key);
+                    }
+                    else {
+                        progressBar[0].setProgress(data.progress * 100);
+                        progressBar[0].setText(parseInt(data.progress * 100) + '%');
+                        setTimeout(checkTimelapse, 1000);
+                    }
+
+                    first = false;
+                });
+            }
+            
+            checkTimelapse();
 
             return false;
         },
@@ -2359,7 +2421,7 @@ function runMediaDialog(cameraId, mediaType) {
         
         function addEntries() {
             /* add the entries to the media list */
-            entries.forEach(function (entry) {
+            entries.forEach(function (entry, i) {
                 var entryDiv = entry.div;
                 var detailsDiv = null;
                 
@@ -2393,6 +2455,18 @@ function runMediaDialog(cameraId, mediaType) {
                     deleteButton.click(function () {
                         doDeleteFile('/' + mediaType + '/' + cameraId + '/delete' + entry.path, function () {
                             entryDiv.remove();
+                            entries.splice(i, 1); /* remove entry from group */
+
+                            /* update text on group button */
+                            groupsDiv.find('div.media-dialog-group-button').each(function () {
+                                var $this = $(this);
+                                if (this.key == groupKey) {
+                                    var text = this.innerHTML;
+                                    text = text.substring(0, text.lastIndexOf(' '));
+                                    text += ' (' + entries.length + ')';
+                                    this.innerHTML = text;
+                                }
+                            });
                         });
                         
                         return false;
@@ -2469,7 +2543,6 @@ function runMediaDialog(cameraId, mediaType) {
     }
     
     if (mediaType == 'picture') {
-        
         var zippedButton = $('<div class="media-dialog-button">Zipped</div>');
         buttonsDiv.append(zippedButton);
         
index f53df84b34841658d9c1698170608623f679baea..49edb6964a2437b0d35fb2063f8f9c0f3a374ec9 100644 (file)
@@ -280,13 +280,13 @@ function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decima
         this.update = input2slider;
     });
     
-    slider.setMinVal = function (mv) {
+    slider[0].setMinVal = function (mv) {
         minVal = mv;
 
         makeTicks();
     };
     
-    slider.setMaxVal = function (mv) {
+    slider[0].setMaxVal = function (mv) {
         maxVal = mv;
 
         makeTicks();
@@ -559,6 +559,26 @@ function makeUrlValidator($input) {
     });
 }
 
+function makeProgressBar($div) {
+    $div.addClass('progress-bar-container');
+    var fillDiv = $('<div class="progress-bar-fill"></div>');
+    var textSpan = $('<span class="progress-bar-text"></span>');
+
+    $div.append(fillDiv);
+    $div.append(textSpan);
+    
+    $div[0].setProgress = function (progress) {
+        $div.progress = progress;
+        fillDiv.width(progress + '%');
+    };
+    
+    $div[0].setText = function (text) {
+        textSpan.html(text);
+    };
+
+    return $div;
+}
+
 
     /* modal dialog */
 
@@ -758,6 +778,7 @@ function runModalDialog(options) {
      * * onClose: Function
      * * onShow: Function
      * * stack: Boolean
+     * * noKeys: Boolean
      */
     
     var content = $('<div></div>');
@@ -828,7 +849,7 @@ function runModalDialog(options) {
         buttonsDiv.css('margin-top', '5px');
     }
     
-    var handleKeyUp = function (e) {
+    var handleKeyUp = !options.noKeys && function (e) {
         if (!content.is(':visible')) {
             return;
         }
index 3e3216dbb4eb6ca891237bd4a8c524cb1a90b2cb..136a7eb2f5e4789c6438dd2f46b0ac2793dfeba5 100644 (file)
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Disk Usage</span></td>
                         <td class="settings-item-value">
-                            <div class="disk-usage-bar-container">
-                                <div class="disk-usage-bar-fill" id="diskUsageBarFill"></div>
-                                <span class="disk-usage-percent" id="diskUsageText"></span>
-                            </div>
+                            <div id="diskUsageProgressBar"></div>
                         </td>
                         <td><span class="help-mark" title="the used/total size of the disk where the root directory resides">?</span></td>
                     </tr>