From: Calin Crisan Date: Sat, 27 Sep 2014 17:41:56 +0000 (+0300) Subject: implemented zip of pictures download X-Git-Url: http://www.vanbest.org/gitweb/?a=commitdiff_plain;h=771034e190a8404d2e88f92ebacc0d9e39b5b5ea;p=motioneye-debian implemented zip of pictures download --- diff --git a/motioneye.py b/motioneye.py index 3fed3df..8448a4e 100755 --- a/motioneye.py +++ b/motioneye.py @@ -64,6 +64,8 @@ def _configure_settings(): set_default_setting('ENABLE_REBOOT', False) 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): diff --git a/settings_default.py b/settings_default.py index 294420f..4a9e9a2 100644 --- a/settings_default.py +++ b/settings_default.py @@ -79,3 +79,9 @@ SMTP_TIMEOUT = 60 # the interval in seconds to consider around the moment of the event when attaching media files to notifications 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 diff --git a/src/handlers.py b/src/handlers.py index f65ce0c..2f48811 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -20,6 +20,7 @@ import datetime import json import logging import os +import re import socket from tornado.web import RequestHandler, HTTPError, asynchronous @@ -624,7 +625,7 @@ class ConfigHandler(BaseHandler): class PictureHandler(BaseHandler): @asynchronous - def get(self, camera_id, op, filename=None): + def get(self, camera_id, op, filename=None, group=None): if camera_id is not None: camera_id = int(camera_id) if camera_id not in config.get_camera_ids(): @@ -645,6 +646,12 @@ class PictureHandler(BaseHandler): elif op == 'preview': self.preview(camera_id, filename) + elif op == 'zipped': + self.zipped(camera_id, group) + + elif op == 'timelapse': + self.timelapse(camera_id, group) + else: raise HTTPError(400, 'unknown operation') @@ -794,6 +801,124 @@ class PictureHandler(BaseHandler): width=self.get_argument('width', None), height=self.get_argument('height', None)) + @BaseHandler.auth() + def zipped(self, camera_id, group): + if camera_id not in config.get_camera_ids(): + raise HTTPError(404, 'no such camera') + + key = self.get_argument('key', None) + 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) + + 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: + 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: + return self.finish_json({'error': 'Failed to create zip file.'}) + + key = mediafiles.set_prepared_cache(data) + 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}) + + mediafiles.get_zipped_content(camera_config, media_type='picture', callback=on_zip, prefix=group) + + 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.' % { + '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) + + @BaseHandler.auth() + def timelapse(self, camera_id, group): + if camera_id not in config.get_camera_ids(): + raise HTTPError(404, 'no such camera') + + key = self.get_argument('key', None) + if key: + 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') + + 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: + interval = int(self.get_argument('interval')) + + logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with interval %(int)s' % { + 'group': group, 'id': camera_id, '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.'}) + + 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, interval, callback=on_timelapse, group=group) + + 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}) + + 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}) + + remote.get_timelapse_movie(camera_config, interval, callback=on_response, group=group) + def try_finish(self, content): try: self.finish(content) diff --git a/src/mediafiles.py b/src/mediafiles.py index fc79dde..38730ff 100644 --- a/src/mediafiles.py +++ b/src/mediafiles.py @@ -16,15 +16,19 @@ # along with this program. If not, see . import datetime +import hashlib import logging import multiprocessing import os.path import stat import StringIO import subprocess +import time import tornado +import zipfile from PIL import Image +from tornado import ioloop import config import settings @@ -41,6 +45,9 @@ _current_pictures_cache = {} # a cache list of paths to movies without preview _previewless_movie_files = [] +# a cache of prepared files (whose preparing time is significant) +_prepared_files = {} + def _list_media_files(dir, exts, prefix=None): media_files = [] @@ -306,6 +313,184 @@ def get_media_content(camera_config, path, media_type): return None +def get_zipped_content(camera_config, media_type, callback, prefix): + target_dir = camera_config.get('target_dir') + + if media_type == 'picture': + exts = _PICTURE_EXTS + + elif media_type == 'movie': + exts = _MOVIE_EXTS + + working = multiprocessing.Value('b') + working.value = True + + # create a subprocess to add files to zip + def do_zip(pipe): + mf = _list_media_files(target_dir, exts=exts, prefix=prefix) + paths = [] + for (p, st) in mf: # @UnusedVariable + path = p[len(target_dir):] + if path.startswith('/'): + path = path[1:] + + paths.append(path) + + zip_filename = os.path.join(settings.MEDIA_PATH, '.zip-%s' % int(time.time())) + logging.debug('adding %d files to zip file "%s"' % (len(paths), zip_filename)) + + try: + with zipfile.ZipFile(zip_filename, mode='w') as f: + for path in paths: + full_path = os.path.join(target_dir, path) + f.write(full_path, path) + + except Exception as e: + logging.error('failed to create zip file "%s": %s' % (zip_filename, e)) + + working.value = False + pipe.close() + return + + logging.debug('reading zip file "%s" into memory' % zip_filename) + + try: + with open(zip_filename, mode='r') as f: + data = f.read() + + working.value = False + pipe.send(data) + logging.debug('zip data ready') + + except Exception as e: + logging.error('failed to read zip file "%s": %s' % (zip_filename, e)) + working.value = False + + finally: + os.remove(zip_filename) + pipe.close() + + logging.debug('starting zip process...') + + (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False) + process = multiprocessing.Process(target=do_zip, args=(child_pipe, )) + process.start() + + # poll the subprocess to see when it has finished + started = datetime.datetime.now() + + def poll_process(): + ioloop = tornado.ioloop.IOLoop.instance() + if working.value: + now = datetime.datetime.now() + delta = now - started + if delta.seconds < settings.ZIP_TIMEOUT: + ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process) + + else: # process did not finish within 2 minutes + logging.error('timeout waiting for the zip process to finish') + + callback(None) + + else: # finished + try: + data = parent_pipe.recv() + logging.debug('zip process has returned %d bytes' % len(data)) + + except: + data = None + + callback(data) + + poll_process() + + +def get_timelapse_movie(camera_config, interval, callback, prefix): + target_dir = camera_config.get('target_dir') + +# working = multiprocessing.Value('b') +# working.value = True + +# # create a subprocess to create the files +# def do_zip(pipe): +# mf = _list_media_files(target_dir, exts=exts, prefix=prefix) +# paths = [] +# for (p, st) in mf: # @UnusedVariable +# path = p[len(target_dir):] +# if path.startswith('/'): +# path = path[1:] +# +# paths.append(path) +# +# zip_filename = os.path.join(settings.MEDIA_PATH, '.zip-%s' % int(time.time())) +# logging.debug('adding %d files to zip file "%s"' % (len(paths), zip_filename)) +# +# try: +# with zipfile.ZipFile(zip_filename, mode='w') as f: +# for path in paths: +# full_path = os.path.join(target_dir, path) +# f.write(full_path, path) +# +# except Exception as e: +# logging.error('failed to create zip file "%s": %s' % (zip_filename, e)) +# +# working.value = False +# pipe.close() +# return +# +# logging.debug('reading zip file "%s" into memory' % zip_filename) +# +# try: +# with open(zip_filename, mode='r') as f: +# data = f.read() +# +# working.value = False +# pipe.send(data) +# logging.debug('zip data ready') +# +# except Exception as e: +# logging.error('failed to read zip file "%s": %s' % (zip_filename, e)) +# working.value = False +# +# finally: +# os.remove(zip_filename) +# pipe.close() +# +# logging.debug('starting zip process...') +# +# (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False) +# process = multiprocessing.Process(target=do_zip, args=(child_pipe, )) +# process.start() +# +# # poll the subprocess to see when it has finished +# started = datetime.datetime.now() +# +# def poll_process(): +# ioloop = tornado.ioloop.IOLoop.instance() +# if working.value: +# now = datetime.datetime.now() +# delta = now - started +# if delta.seconds < settings.ZIP_TIMEOUT: +# ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process) +# +# else: # process did not finish within 2 minutes +# logging.error('timeout waiting for the zip process to finish') +# +# callback(None) +# +# else: # finished +# try: +# data = parent_pipe.recv() +# logging.debug('zip process has returned %d bytes' % len(data)) +# +# except: +# data = None +# +# callback(data) +# +# poll_process() + + 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) @@ -379,8 +564,6 @@ def get_current_picture(camera_config, width, height): def set_picture_cache(camera_id, sequence, width, content): - global _current_pictures_cache - if not content: return @@ -393,8 +576,6 @@ def set_picture_cache(camera_id, sequence, width, content): def get_picture_cache(camera_id, sequence, width): - global _current_pictures_cache - cache = _current_pictures_cache.setdefault(camera_id, []) now = datetime.datetime.utcnow() @@ -407,3 +588,25 @@ def get_picture_cache(camera_id, sequence, width): return content return None + + +def get_prepared_cache(key): + return _prepared_files.get(key) + + +def set_prepared_cache(data): + key = hashlib.sha1(str(time.time())).hexdigest() + + if key in _prepared_files: + logging.warn('key "%s" already present in prepared cache' % key) + + _prepared_files[key] = data + + def clear(): + 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) + ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=timeout), clear) + + return key diff --git a/src/remote.py b/src/remote.py index 8bc29a6..466f1e5 100644 --- a/src/remote.py +++ b/src/remote.py @@ -333,6 +333,145 @@ def get_media_content(local_config, callback, filename, media_type): http_client.fetch(request, on_response) +def get_zipped_content(local_config, callback, group, media_type): + 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 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)}) + + prepare_uri = uri + '/%(media_type)s/%(id)s/zipped/%(group)s/' % { + 'media_type': media_type, + 'id': camera_id, + 'group': 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): + 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)}) + + return callback(error=unicode(response.error)) + + 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), + 'msg': unicode(e)}) + + 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} + + 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)}) + + return callback(error=unicode(response.error)) + + callback(response.body) + + http_client = AsyncHTTPClient() + http_client.fetch(request, on_download) + + http_client = AsyncHTTPClient() + http_client.fetch(request, on_prepare) + + +def get_timelapse_movie(local_config, interval, callback, group): + 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 group %(group)s of remote camera %(id)s with interval %(int)s on %(url)s' % { + 'group': group, + 'id': camera_id, + 'int': interval, + 'url': make_camera_url(local_config)}) + + prepare_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s' % { + 'id': camera_id, + 'int': interval, + 'group': 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): + if response.error: + logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with interval %(int)s on %(url)s: %(msg)s' % { + 'group': group, + 'id': camera_id, + 'url': make_camera_url(local_config), + 'int': interval, + 'msg': unicode(response.error)}) + + return callback(error=unicode(response.error)) + + 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), + 'msg': unicode(e)}) + + return callback(error=unicode(e)) + + download_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % { + 'id': camera_id, + 'group': group, + 'int': interval, + 'key': key} + + 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 interval %(int)s on %(url)s: %(msg)s' % { + 'group': group, + 'id': camera_id, + 'url': make_camera_url(local_config), + 'int': interval, + 'msg': unicode(response.error)}) + + return callback(error=unicode(response.error)) + + callback(response.body) + + http_client = AsyncHTTPClient() + http_client.fetch(request, on_download) + + http_client = AsyncHTTPClient() + http_client.fetch(request, on_prepare) + + def get_media_preview(local_config, callback, filename, media_type, width, height): host = local_config.get('@host', local_config.get('host')) port = local_config.get('@port', local_config.get('port')) diff --git a/src/server.py b/src/server.py index 8df6d92..f42758c 100644 --- a/src/server.py +++ b/src/server.py @@ -45,9 +45,10 @@ application = Application( (r'^/config/(?P\d+)/(?Pget|set|rem|set_preview)/?$', handlers.ConfigHandler), (r'^/config/(?Padd|list|list_devices)/?$', handlers.ConfigHandler), (r'^/picture/(?P\d+)/(?Pcurrent|list|frame)/?$', handlers.PictureHandler), - (r'^/picture/(?P\d+)/(?Pdownload|preview)/(?P.+)/?$', handlers.PictureHandler), + (r'^/picture/(?P\d+)/(?Pdownload|preview)/(?P.+?)/?$', handlers.PictureHandler), + (r'^/picture/(?P\d+)/(?Pzipped|timelapse)/(?P.+?)/?$', handlers.PictureHandler), (r'^/movie/(?P\d+)/(?Plist)/?$', handlers.MovieHandler), - (r'^/movie/(?P\d+)/(?Pdownload|preview)/(?P.+)/?$', handlers.MovieHandler), + (r'^/movie/(?P\d+)/(?Pdownload|preview)/(?P.+?)/?$', handlers.MovieHandler), (r'^/update/?$', handlers.UpdateHandler), (r'^/power/(?Pshutdown)/?$', handlers.PowerHandler), (r'^/version/?$', handlers.VersionHandler), diff --git a/static/css/main.css b/static/css/main.css index e9f9f65..b223588 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -601,6 +601,33 @@ div.media-list-download-button:ACTIVE { background-color: #317CAD; } +div.media-dialog-buttons { + margin: 0.5em 0px 0px 0px; + text-align: center; +} + +div.media-dialog-button { + cursor: pointer; + display: inline-block; + height: 1.5em; + line-height: 1.5em; + text-align: center; + padding: 0px 0.5em; + margin: 0px 5px 0px 0px; + color: white; + background-color: #317CAD; + border-radius: 3px; + transition: all 0.1s linear; +} + +div.media-dialog-button:HOVER { + background-color: #3498db; +} + +div.media-dialog-button:ACTIVE { + background-color: #317CAD; +} + div.picture-dialog-content { position: relative; text-align: center; @@ -644,6 +671,10 @@ img.picture-dialog-progress { opacity: 0.7; } +table.timelapse-dialog select#intervalSelect { + width: 10em; +} + /* camera frames */ diff --git a/static/js/main.js b/static/js/main.js index 708200a..c16b375 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1073,7 +1073,7 @@ function endProgress() { }, 500); } -function downloadMediaFile(uri) { +function downloadFile(uri) { var url = window.location.href; var parts = url.split('/'); url = parts.slice(0, 3).join('/') + uri; @@ -1304,6 +1304,14 @@ function doUpdate() { }); } +function doDownloadZipped(cameraId, groupKey) { + showModalDialog('', null, null, true); + ajax('GET', '/picture/' + cameraId + '/zipped/' + groupKey + '/', null, function (data) { + hideModalDialog(); /* progress */ + downloadFile('/picture/' + cameraId + '/zipped/' + groupKey + '/?key=' + data.key); + }); +} + /* fetch & push */ @@ -1581,7 +1589,7 @@ function runPictureDialog(entries, pos, mediaType) { {caption: 'Close'}, {caption: 'Download', isDefault: true, click: function () { var entry = entries[pos]; - downloadMediaFile('/' + mediaType + '/' + entry.cameraId + '/download' + entry.path); + downloadFile('/' + mediaType + '/' + entry.cameraId + '/download' + entry.path); return false; }} @@ -1839,15 +1847,87 @@ function runAddCameraDialog() { }); } +function runTimelapseDialog(cameraId, groupKey, group) { + var content = + $('' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Group' + groupKey + '
Include a picture every' + + '' + + '?
'); + + var intervalSelect = content.find('#intervalSelect'); + + runModalDialog({ + title: 'Create Timelapse Movie', + closeButton: true, + buttons: 'okcancel', + content: content, + onOk: function () { + showModalDialog('', null, null, true); + ajax('GET', '/picture/' + cameraId + '/timelapse/' + groupKey + '/', {interval: intervalSelect.val()}, function (data) { + hideModalDialog(); /* progress */ + hideModalDialog(); /* timelapse dialog */ + downloadFile('/picture/' + cameraId + '/timelapse/' + groupKey + '/key=' + data.key); + }); + + return false; + }, + stack: true + }); +} function runMediaDialog(cameraId, mediaType) { var dialogDiv = $('
'); var mediaListDiv = $('
'); var groupsDiv = $('
'); + var groups = {}; + var groupKey = null; + dialogDiv.append(groupsDiv); dialogDiv.append(mediaListDiv); + if (mediaType == 'picture') { + var buttonsDiv = $('
'); + dialogDiv.append(buttonsDiv); + + var zippedButton = $('
Zipped Pictures
'); + buttonsDiv.append(zippedButton); + + zippedButton.click(function () { + if (groupKey) { + doDownloadZipped(cameraId, groupKey); + } + }); + + var timelapseButton = $('
Timelapse Movie
'); + buttonsDiv.append(timelapseButton); + + timelapseButton.click(function () { + if (groupKey) { + runTimelapseDialog(cameraId, groupKey, groups[groupKey]); + } + }); + } + function setDialogSize() { var windowWidth = $(window).width(); var windowHeight = $(window).height(); @@ -1888,7 +1968,6 @@ function runMediaDialog(cameraId, mediaType) { } /* group the media */ - var groups = {}; data.mediaList.forEach(function (media) { var path = media.path; var parts = path.split('/'); @@ -1920,6 +1999,8 @@ function runMediaDialog(cameraId, mediaType) { tempDiv.remove(); function showGroup(key) { + groupKey = key; + if (mediaListDiv.find('img.media-list-progress').length) { return; /* already in progress of loading */ } @@ -1955,7 +2036,7 @@ function runMediaDialog(cameraId, mediaType) { entryDiv.append(previewImg); previewImg[0]._src = '/' + mediaType + '/' + cameraId + '/preview' + entry.path + '?height=' + height; - var downloadButton = $('
download
'); + var downloadButton = $('
Download
'); entryDiv.append(downloadButton); var nameDiv = $('
' + entry.name + '
'); @@ -1965,7 +2046,7 @@ function runMediaDialog(cameraId, mediaType) { entryDiv.append(detailsDiv); downloadButton.click(function () { - downloadMediaFile('/picture/' + cameraId + '/download' + entry.path); + downloadFile('/picture/' + cameraId + '/download' + entry.path); return false; });