From: Calin Crisan Date: Sat, 27 Dec 2014 14:13:12 +0000 (+0200) Subject: media browser: it is now possible to remove an entire group of media X-Git-Url: http://www.vanbest.org/gitweb/?a=commitdiff_plain;h=cb68569ee81d24520ddb93ef3b14a4c2fbc5ca92;p=motioneye-debian media browser: it is now possible to remove an entire group of media files --- diff --git a/src/handlers.py b/src/handlers.py index b938048..3396bfe 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -683,6 +683,9 @@ class PictureHandler(BaseHandler): if op == 'delete': self.delete(camera_id, filename) + elif op == 'delete_all': + self.delete_all(camera_id, group) + else: raise HTTPError(400, 'unknown operation') @@ -843,7 +846,7 @@ class PictureHandler(BaseHandler): width=self.get_argument('width', None), height=self.get_argument('height', None)) - @BaseHandler.auth() + @BaseHandler.auth(admin=True) def delete(self, camera_id, filename): logging.debug('deleting picture %(filename)s of camera %(id)s' % { 'filename': filename, 'id': camera_id}) @@ -980,6 +983,30 @@ class PictureHandler(BaseHandler): remote.get_timelapse_movie(camera_config, framerate, interval, callback=on_response, group=group) + @BaseHandler.auth(admin=True) + def delete_all(self, camera_id, group): + logging.debug('deleting picture 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): + try: + mediafiles.del_media_group(camera_config, group, 'picture') + self.finish_json() + + except Exception as e: + self.finish_json({'error': unicode(e)}) + + else: # remote camera + def on_response(response=None, error=None): + if error: + return self.finish_json({'error': 'Failed to delete picture group from %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(camera_config), 'msg': error}}) + + self.finish_json() + + remote.del_media_group(camera_config, on_response, group=group, media_type='picture') + def try_finish(self, content): try: self.finish(content) @@ -1009,7 +1036,7 @@ class MovieHandler(BaseHandler): raise HTTPError(400, 'unknown operation') @asynchronous - def post(self, camera_id, op, filename=None): + def post(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(): @@ -1018,6 +1045,9 @@ class MovieHandler(BaseHandler): if op == 'delete': self.delete(camera_id, filename) + elif op == 'delete_all': + self.delete_all(camera_id, group) + else: raise HTTPError(400, 'unknown operation') @@ -1113,6 +1143,7 @@ class MovieHandler(BaseHandler): width=self.get_argument('width', None), height=self.get_argument('height', None)) + @BaseHandler.auth(admin=True) def delete(self, camera_id, filename): logging.debug('deleting movie %(filename)s of camera %(id)s' % { 'filename': filename, 'id': camera_id}) @@ -1136,6 +1167,30 @@ class MovieHandler(BaseHandler): remote.del_media_content(camera_config, on_response, filename=filename, media_type='movie') + @BaseHandler.auth(admin=True) + def delete_all(self, camera_id, group): + logging.debug('deleting movie 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): + try: + mediafiles.del_media_group(camera_config, group, 'movie') + self.finish_json() + + except Exception as e: + self.finish_json({'error': unicode(e)}) + + else: # remote camera + def on_response(response=None, error=None): + if error: + return self.finish_json({'error': 'Failed to delete movie group from %(url)s: %(msg)s.' % { + 'url': remote.make_camera_url(camera_config), 'msg': error}}) + + self.finish_json() + + remote.del_media_group(camera_config, on_response, group=group, media_type='movie') + class UpdateHandler(BaseHandler): @BaseHandler.auth(admin=True) diff --git a/src/mediafiles.py b/src/mediafiles.py index dc7e155..b03ed36 100644 --- a/src/mediafiles.py +++ b/src/mediafiles.py @@ -583,12 +583,34 @@ def del_media_content(camera_config, path, media_type): os.remove(full_path) except Exception as e: - logging.error('failed to read file %(path)s: %(msg)s' % { + logging.error('failed to remove file %(path)s: %(msg)s' % { 'path': full_path, 'msg': unicode(e)}) raise +def del_media_group(camera_config, group, media_type): + if media_type == 'picture': + exts = _PICTURE_EXTS + + elif media_type == 'movie': + exts = _MOVIE_EXTS + + target_dir = camera_config.get('target_dir') + full_path = os.path.join(target_dir, group) + + mf = _list_media_files(target_dir, exts=exts, prefix=group) + for (path, st) in mf: # @UnusedVariable + try: + os.remove(path) + + except Exception as e: + logging.error('failed to remove file %(path)s: %(msg)s' % { + 'path': full_path, 'msg': unicode(e)}) + + raise + + def get_current_picture(camera_config, width, height): import mjpgclient diff --git a/src/remote.py b/src/remote.py index 92d5b31..77cf5f2 100644 --- a/src/remote.py +++ b/src/remote.py @@ -562,3 +562,39 @@ def del_media_content(local_config, callback, filename, media_type): http_client = AsyncHTTPClient() http_client.fetch(request, on_response) + + +def del_media_group(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('deleting group %(group)s of remote camera %(id)s on %(url)s' % { + 'group': group, + 'id': camera_id, + 'url': make_camera_url(local_config)}) + + uri += '/%(media_type)s/%(id)s/delete/%(group)s' % { + 'media_type': media_type, + 'id': camera_id, + 'group': group} + + request = _make_request(host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT) + + def on_response(response): + if response.error: + logging.error('failed to delete 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)) + + return callback() + + http_client = AsyncHTTPClient() + http_client.fetch(request, on_response) diff --git a/src/server.py b/src/server.py index 3098ac5..5041419 100644 --- a/src/server.py +++ b/src/server.py @@ -46,9 +46,10 @@ application = Application( (r'^/config/(?Padd|list|list_devices)/?$', handlers.ConfigHandler), (r'^/picture/(?P\d+)/(?Pcurrent|list|frame)/?$', handlers.PictureHandler), (r'^/picture/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.PictureHandler), - (r'^/picture/(?P\d+)/(?Pzipped|timelapse)/(?P.+?)/?$', handlers.PictureHandler), + (r'^/picture/(?P\d+)/(?Pzipped|timelapse|delete_all)/(?P.+?)/?$', handlers.PictureHandler), (r'^/movie/(?P\d+)/(?Plist)/?$', handlers.MovieHandler), (r'^/movie/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.MovieHandler), + (r'^/movie/(?P\d+)/(?Pdelete_all)/(?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 f20544c..fa14bb7 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -500,6 +500,8 @@ div.media-dialog-groups.small-screen { div.media-dialog-group-button { height: 1.5em; + width: 9.5em; + box-sizing: border-box; line-height: 1.5em; text-align: center; margin: 0em 0.2em 0.2em 0.2em; @@ -721,6 +723,20 @@ table.timelapse-dialog select { width: 10em; } +div.media-dialog-delete-all-button { + margin-top: 0.1em; + margin-bottom: 0.4em; + background: #c0392b; +} + +div.media-dialog-delete-all-button:HOVER { + background-color: #D43F2F; +} + +div.media-dialog-delete-all-button:ACTIVE { + background-color: #B03427; +} + /* camera frames */ diff --git a/static/js/main.js b/static/js/main.js index 8ad2974..1e5b258 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1550,7 +1550,11 @@ function doDeleteFile(uri, callback) { url = parts.slice(0, 3).join('/') + uri; runConfirmDialog('Really delete this file?', function () { + showModalDialog('', null, null, true); ajax('POST', url, null, function (data) { + hideModalDialog(); /* progress */ + hideModalDialog(); /* confirm */ + if (data == null || data.error) { showErrorMessage(data && data.error); return; @@ -1560,9 +1564,31 @@ function doDeleteFile(uri, callback) { callback(); } }); + + return false; }, {stack: true}); } +function doDeleteAllFiles(mediaType, cameraId, groupKey, callback) { + runConfirmDialog('Really delete all ' + mediaType + 's in ' + groupKey + '?', function () { + showModalDialog('', null, null, true); + ajax('POST', '/' + mediaType + '/' + cameraId + '/delete_all/' + groupKey + '/', null, function (data) { + hideModalDialog(); /* progress */ + hideModalDialog(); /* confirm */ + + if (data == null || data.error) { + showErrorMessage(data && data.error); + return; + } + + if (callback) { + callback(); + } + }); + + return false; + }, {stack: true}); +} /* fetch & push */ @@ -2259,28 +2285,199 @@ function runMediaDialog(cameraId, mediaType) { dialogDiv.append(groupsDiv); dialogDiv.append(mediaListDiv); + dialogDiv.append(buttonsDiv); + + /* add a temporary div to compute 3em in px */ + var tempDiv = $('
'); + $('div.modal-container').append(tempDiv); + var height = tempDiv.height(); + tempDiv.remove(); + + function showGroup(key) { + groupKey = key; + + if (mediaListDiv.find('img.media-list-progress').length) { + return; /* already in progress of loading */ + } + + /* (re)set the current state of the group buttons */ + groupsDiv.find('div.media-dialog-group-button').each(function () { + var $this = $(this); + if (this.key == key) { + $this.addClass('current'); + } + else { + $this.removeClass('current'); + } + }); + + var mediaListByName = {}; + var entries = groups[key]; + + /* cleanup the media list */ + mediaListDiv.children('div.media-list-entry').detach(); + mediaListDiv.html(''); + + function addEntries() { + /* add the entries to the media list */ + entries.forEach(function (entry) { + var entryDiv = entry.div; + var detailsDiv = null; + + if (!entryDiv) { + entryDiv = $('
'); + + var previewImg = $(''); + entryDiv.append(previewImg); + previewImg[0]._src = addAuthParams('GET', '/' + mediaType + '/' + cameraId + '/preview' + entry.path + '?height=' + height); + + var downloadButton = $('
Download
'); + entryDiv.append(downloadButton); + + var deleteButton = $('
Delete
'); + + if (username === adminUsername) { + entryDiv.append(deleteButton); + } + + var nameDiv = $('
' + entry.name + '
'); + entryDiv.append(nameDiv); + + detailsDiv = $('
'); + entryDiv.append(detailsDiv); + + downloadButton.click(function () { + downloadFile('/' + mediaType + '/' + cameraId + '/download' + entry.path); + return false; + }); + + deleteButton.click(function () { + doDeleteFile('/' + mediaType + '/' + cameraId + '/delete' + entry.path, function () { + entryDiv.remove(); + }); + + return false; + }); + + entryDiv.click(function () { + var pos = entries.indexOf(entry); + runPictureDialog(entries, pos, mediaType); + }); + + entry.div = entryDiv; + } + else { + detailsDiv = entry.div.find('div.media-list-entry-details'); + } + + var momentSpan = $('' + entry.momentStr + ', '); + var momentShortSpan = $('' + entry.momentStrShort + ''); + var sizeSpan = $('' + entry.sizeStr + ''); + detailsDiv.empty(); + detailsDiv.append(momentSpan); + detailsDiv.append(momentShortSpan); + detailsDiv.append(sizeSpan); + mediaListDiv.append(entryDiv); + }); + + /* trigger a scroll event */ + mediaListDiv.scroll(); + } + + /* if details are already fetched, simply add the entries and return */ + if (entries[0].timestamp) { + return addEntries(); + } + + var previewImg = $(''); + mediaListDiv.append(previewImg); + + var url = '/' + mediaType + '/' + cameraId + '/list/?prefix=' + (key || 'ungrouped'); + ajax('GET', url, null, function (data) { + previewImg.remove(); + + if (data == null || data.error) { + hideModalDialog(); + showErrorMessage(data && data.error); + return; + } + + /* index the media list by name */ + data.mediaList.forEach(function (media) { + var path = media.path; + var parts = path.split('/'); + var name = parts[parts.length - 1]; + + mediaListByName[name] = media; + }); + + /* assign details to entries */ + entries.forEach(function (entry) { + var media = mediaListByName[entry.name]; + if (media) { + entry.momentStr = media.momentStr; + entry.momentStrShort = media.momentStrShort; + entry.sizeStr = media.sizeStr; + entry.timestamp = media.timestamp; + } + }); + + /* sort the entries by timestamp */ + entries.sortKey(function (e) {return e.timestamp || e.name;}, true); + + addEntries(); + }); + } if (mediaType == 'picture') { - dialogDiv.append(buttonsDiv); - var zippedButton = $('
Zipped Pictures
'); + var zippedButton = $('
Zipped
'); buttonsDiv.append(zippedButton); zippedButton.click(function () { - if (groupKey) { + if (groupKey != null) { doDownloadZipped(cameraId, groupKey); } }); - var timelapseButton = $('
Timelapse Movie
'); + var timelapseButton = $('
Timelapse
'); buttonsDiv.append(timelapseButton); timelapseButton.click(function () { - if (groupKey) { + if (groupKey != null) { runTimelapseDialog(cameraId, groupKey, groups[groupKey]); } }); } + + var deleteAllButton = $('
Delete
'); + buttonsDiv.append(deleteAllButton); + + deleteAllButton.click(function () { + if (groupKey != null) { + doDeleteAllFiles(mediaType, cameraId, groupKey, function () { + /* delete th group button */ + groupsDiv.find('div.media-dialog-group-button').each(function () { + var $this = $(this); + if (this.key == groupKey) { + $this.remove(); + } + }); + + /* delete the group itself */ + delete groups[groupKey]; + + /* show the first existing group, if any */ + var keys = Object.keys(groups); + if (keys.length) { + showGroup(keys[0]); + } + else { + hideModalDialog(); + } + }); + } + }); function updateDialogSize() { var windowWidth = $(window).width(); @@ -2361,148 +2558,6 @@ function runMediaDialog(cameraId, mediaType) { keys.sort(); keys.reverse(); - /* add a temporary div to compute 3em in px */ - var tempDiv = $('
'); - $('div.modal-container').append(tempDiv); - var height = tempDiv.height(); - tempDiv.remove(); - - function showGroup(key) { - groupKey = key; - - if (mediaListDiv.find('img.media-list-progress').length) { - return; /* already in progress of loading */ - } - - /* (re)set the current state of the group buttons */ - groupsDiv.find('div.media-dialog-group-button').each(function () { - var $this = $(this); - if (this.key == key) { - $this.addClass('current'); - } - else { - $this.removeClass('current'); - } - }); - - var mediaListByName = {}; - var entries = groups[key]; - - /* cleanup the media list */ - mediaListDiv.children('div.media-list-entry').detach(); - mediaListDiv.html(''); - - function addEntries() { - /* add the entries to the media list */ - entries.forEach(function (entry) { - var entryDiv = entry.div; - var detailsDiv = null; - - if (!entryDiv) { - entryDiv = $('
'); - - var previewImg = $(''); - entryDiv.append(previewImg); - previewImg[0]._src = addAuthParams('GET', '/' + mediaType + '/' + cameraId + '/preview' + entry.path + '?height=' + height); - - var downloadButton = $('
Download
'); - entryDiv.append(downloadButton); - - var deleteButton = $('
Delete
'); - - if (username === adminUsername) { - entryDiv.append(deleteButton); - } - - var nameDiv = $('
' + entry.name + '
'); - entryDiv.append(nameDiv); - - detailsDiv = $('
'); - entryDiv.append(detailsDiv); - - downloadButton.click(function () { - downloadFile('/' + mediaType + '/' + cameraId + '/download' + entry.path); - return false; - }); - - deleteButton.click(function () { - doDeleteFile('/' + mediaType + '/' + cameraId + '/delete' + entry.path, function () { - entryDiv.remove(); - }); - - return false; - }); - - entryDiv.click(function () { - var pos = entries.indexOf(entry); - runPictureDialog(entries, pos, mediaType); - }); - - entry.div = entryDiv; - } - else { - detailsDiv = entry.div.find('div.media-list-entry-details'); - } - - var momentSpan = $('' + entry.momentStr + ', '); - var momentShortSpan = $('' + entry.momentStrShort + ''); - var sizeSpan = $('' + entry.sizeStr + ''); - detailsDiv.empty(); - detailsDiv.append(momentSpan); - detailsDiv.append(momentShortSpan); - detailsDiv.append(sizeSpan); - mediaListDiv.append(entryDiv); - }); - - /* trigger a scroll event */ - mediaListDiv.scroll(); - } - - /* if details are already fetched, simply add the entries and return */ - if (entries[0].timestamp) { - return addEntries(); - } - - var previewImg = $(''); - mediaListDiv.append(previewImg); - - var url = '/' + mediaType + '/' + cameraId + '/list/?prefix=' + (key || 'ungrouped'); - ajax('GET', url, null, function (data) { - previewImg.remove(); - - if (data == null || data.error) { - hideModalDialog(); - showErrorMessage(data && data.error); - return; - } - - /* index the media list by name */ - data.mediaList.forEach(function (media) { - var path = media.path; - var parts = path.split('/'); - var name = parts[parts.length - 1]; - - mediaListByName[name] = media; - }); - - /* assign details to entries */ - entries.forEach(function (entry) { - var media = mediaListByName[entry.name]; - if (media) { - entry.momentStr = media.momentStr; - entry.momentStrShort = media.momentStrShort; - entry.sizeStr = media.sizeStr; - entry.timestamp = media.timestamp; - } - }); - - /* sort the entries by timestamp */ - entries.sortKey(function (e) {return e.timestamp || e.name;}, true); - - addEntries(); - }); - } - if (keys.length) { keys.forEach(function (key) { var groupButton = $('
');