import errno
import hashlib
+import json
import logging
import os.path
import sys
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:
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):
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
# the time to wait for zip file creation
ZIP_TIMEOUT = 500
-
-# the time to wait for timelapse movie file creation
-TIMELAPSE_TIMEOUT = 500
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'))):
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})
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
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):
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):
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):
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):
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:
'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):
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:
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):
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):
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):
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):
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):
# 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
# 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 = []
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':
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
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 = []
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)
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)
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
# 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):
'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):
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):
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):
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):
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'))
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'))
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'))
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'))
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)})
# 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),
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),
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'))
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),
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'))
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'))
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'))
'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}
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 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] = ''
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,
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;
}
+ /* 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 {
url += '&';;
}
- url += 'username=' + window.username;
+ url += '_username=' + window.username;
var signature = computeSignature(method, url, body);
- url += '&signature=' + signature;
+ url += '&_signature=' + signature;
return url;
}
makeTimeValidator($('#sundayFromEntry'));
makeTimeValidator($('#sundayToEntry'));
+ /* progress bars */
+ makeProgressBar($('#diskUsageProgressBar'));
+
/* ui elements that enable/disable other ui elements */
$('#motionEyeSwitch').change(updateConfigUi);
$('#showAdvancedSwitch').change(updateConfigUi);
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'];
if (retry) {
retry();
}
-
- //return false;
}},
{caption: 'Login', isDefault: true, click: function () {
window.username = usernameEntry.val();
if (retry) {
retry();
}
-
- //return false;
}}
],
};
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>' +
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'},
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;
},
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;
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;
}
if (mediaType == 'picture') {
-
var zippedButton = $('<div class="media-dialog-button">Zipped</div>');
buttonsDiv.append(zippedButton);
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();
});
}
+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 */
* * onClose: Function
* * onShow: Function
* * stack: Boolean
+ * * noKeys: Boolean
*/
var content = $('<div></div>');
buttonsDiv.css('margin-top', '5px');
}
- var handleKeyUp = function (e) {
+ var handleKeyUp = !options.noKeys && function (e) {
if (!content.is(':visible')) {
return;
}
<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>