Merge branch 'maintenance' into devel

This commit is contained in:
Gina Häußge 2016-06-02 12:05:08 +02:00
commit db66e4afea
4 changed files with 91 additions and 44 deletions

View file

@ -62,7 +62,7 @@ def downloadTimelapse(filename):
@api.route("/timelapse/<filename>", methods=["DELETE"])
@restricted_access
def deleteTimelapse(filename):
if util.is_allowed_file(filename, ["mpg"]):
if util.is_allowed_file(filename, ["mpg", "mpeg", "mp4"]):
timelapse_folder = settings().getBaseFolder("timelapse")
full_path = os.path.realpath(os.path.join(timelapse_folder, filename))
if full_path.startswith(timelapse_folder) and os.path.exists(full_path):

View file

@ -276,13 +276,13 @@ $(function() {
self.onEventMovieRendering = function(payload) {
self.displayTimelapsePopup({
title: gettext("Rendering timelapse"),
text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s. Due to performance reasons it is not recommended to start a print job while a movie is still rendering."), payload),
text: _.sprintf(gettext("Now rendering timelapse %(movie_prefix)s. Due to performance reasons it is not recommended to start a print job while a movie is still rendering."), payload),
hide: false
});
};
self.onEventMovieFailed = function(payload) {
var html = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failed with return code %(returncode)s"), payload) + "</p>";
var html = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_prefix)s failed with return code %(returncode)s"), payload) + "</p>";
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + payload.error + '</pre>');
self.displayTimelapsePopup({
@ -296,7 +296,7 @@ $(function() {
self.onEventMovieDone = function(payload) {
self.displayTimelapsePopup({
title: gettext("Timelapse ready"),
text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload),
text: _.sprintf(gettext("New timelapse %(movie_prefix)s is done rendering."), payload),
type: "success",
callbacks: {
before_close: function(notice) {

View file

@ -103,7 +103,10 @@
<td class="timelapse_unrendered_name" data-bind="text: name"></td>
<td class="timelapse_unrendered_count" data-bind="text: count"></td>
<td class="timelapse_unrendered_size" data-bind="text: size"></td>
<td class="timelapse_unrendered_action"><a href="javascript:void(0)" title="{{ _('Delete unrendered timelapse') }}" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeUnrendered($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="javascript:void(0)" title="{{ _('Render timelapse') }}" class="icon-facetime-video" data-bind="click: function() { if ($root.loginState.isUser() && !$root.isBusy()) { $parent.renderUnrendered($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser() || $root.isBusy()}"></a></td>
<td class="timelapse_unrendered_action">
<span data-bind="visible: processing"><i class="icon-refresh icon-spin"></i></span>
<span data-bind="visible: !processing"><a href="javascript:void(0)" title="{{ _('Delete unrendered timelapse') }}" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeUnrendered($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="javascript:void(0)" title="{{ _('Render timelapse') }}" class="icon-facetime-video" data-bind="click: function() { if ($root.loginState.isUser() && !$root.isBusy()) { $parent.renderUnrendered($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser() || $root.isBusy()}"></a></span>
</td>
</tr>
</tbody>
</table>

View file

@ -44,6 +44,12 @@ _valid_timelapse_types = ["off", "timed", "zchange"]
# callbacks for timelapse config updates
_update_callbacks = []
# lock for timelapse cleanup, must be re-entrant
_cleanup_lock = threading.RLock()
# lock for timelapse job
_job_lock = threading.RLock()
def _extract_prefix(filename):
"""
@ -75,10 +81,14 @@ def get_finished_timelapses():
def get_unrendered_timelapses():
global _job_lock
global current
delete_old_unrendered_timelapses()
basedir = settings().getBaseFolder("timelapse_tmp")
jobs = collections.defaultdict(lambda: dict(count=0, size=None, bytes=0, date=None, timestamp=None))
for osFile in os.listdir(basedir):
if not fnmatch.fnmatch(osFile, "*.jpg"):
continue
@ -93,23 +103,37 @@ def get_unrendered_timelapses():
if jobs[prefix]["timestamp"] is None or statResult.st_ctime < jobs[prefix]["timestamp"]:
jobs[prefix]["timestamp"] = statResult.st_ctime
def finalize_fields(job):
job["size"] = util.get_formatted_size(job["bytes"])
job["date"] = util.get_formatted_datetime(datetime.datetime.fromtimestamp(job["timestamp"]))
del job["timestamp"]
return job
with _job_lock:
global current_render_job
return sorted([util.dict_merge(dict(name=key), finalize_fields(value)) for key, value in jobs.items()], key=lambda x: x["name"])
def finalize_fields(prefix, job):
currently_recording = current is not None and current.prefix == prefix
currently_rendering = current_render_job is not None and current_render_job["prefix"] == prefix
job["size"] = util.get_formatted_size(job["bytes"])
job["date"] = util.get_formatted_datetime(datetime.datetime.fromtimestamp(job["timestamp"]))
job["recording"] = currently_recording
job["rendering"] = currently_rendering
job["processing"] = currently_recording or currently_rendering
del job["timestamp"]
return job
return sorted([util.dict_merge(dict(name=key), finalize_fields(key, value)) for key, value in jobs.items()], key=lambda x: x["name"])
def delete_unrendered_timelapse(name):
global _cleanup_lock
basedir = settings().getBaseFolder("timelapse_tmp")
for filename in os.listdir(basedir):
try:
if fnmatch.fnmatch(filename, "{}*.jpg".format(name)):
os.remove(os.path.join(basedir, filename))
except:
logging.getLogger(__name__).exception("Error while processing file {} during cleanup".format(filename))
with _cleanup_lock:
for filename in os.listdir(basedir):
try:
if fnmatch.fnmatch(filename, "{}*.jpg".format(name)):
os.remove(os.path.join(basedir, filename))
except:
if logging.getLogger(__name__).isEnabledFor(logging.DEBUG):
logging.getLogger(__name__).exception("Error while processing file {} during cleanup".format(filename))
def render_unrendered_timelapse(name, gcode=None, postfix=None, fps=25):
@ -131,53 +155,65 @@ def render_unrendered_timelapse(name, gcode=None, postfix=None, fps=25):
def delete_old_unrendered_timelapses():
global _cleanup_lock
basedir = settings().getBaseFolder("timelapse_tmp")
clean_after_days = settings().getInt(["webcam", "cleanTmpAfterDays"])
cutoff = time.time() - clean_after_days * 24 * 60 * 60
prefixes_to_clean = []
for filename in os.listdir(basedir):
try:
path = os.path.join(basedir, filename)
prefix = _extract_prefix(filename)
if prefix is None:
# might be an old tmp_00000.jpg kinda frame. we can't
# render those easily anymore, so delete that stuff
if _old_capture_format_re.match(filename):
os.remove(path)
continue
with _cleanup_lock:
for filename in os.listdir(basedir):
try:
path = os.path.join(basedir, filename)
if prefix in prefixes_to_clean:
continue
prefix = _extract_prefix(filename)
if prefix is None:
# might be an old tmp_00000.jpg kinda frame. we can't
# render those easily anymore, so delete that stuff
if _old_capture_format_re.match(filename):
os.remove(path)
continue
if os.path.getmtime(path) < cutoff:
prefixes_to_clean.append(prefix)
except:
logging.getLogger(__name__).exception("Error while processing file {} during cleanup".format(filename))
if prefix in prefixes_to_clean:
continue
for prefix in prefixes_to_clean:
delete_unrendered_timelapse(prefix)
if os.path.getmtime(path) < cutoff:
prefixes_to_clean.append(prefix)
except:
if logging.getLogger(__name__).isEnabledFor(logging.DEBUG):
logging.getLogger(__name__).exception("Error while processing file {} during cleanup".format(filename))
for prefix in prefixes_to_clean:
delete_unrendered_timelapse(prefix)
logging.getLogger(__name__).info("Deleted old unrendered timelapse {}".format(prefix))
def _create_render_start_handler(name, gcode=None):
def f(movie):
global current_render_job
event_payload = {"gcode": gcode if gcode is not None else "unknown",
"movie": movie,
"movie_basename": os.path.basename(movie)}
current_render_job = event_payload
global _job_lock
with _job_lock:
global current_render_job
event_payload = {"gcode": gcode if gcode is not None else "unknown",
"movie": movie,
"movie_basename": os.path.basename(movie),
"movie_prefix": name}
current_render_job = dict(prefix=name)
current_render_job.update(event_payload)
eventManager().fire(Events.MOVIE_RENDERING, event_payload)
return f
def _create_render_success_handler(name, gcode=None):
def f(movie):
delete_unrendered_timelapse(name)
event_payload = {"gcode": gcode if gcode is not None else "unknown",
"movie": movie,
"movie_basename": os.path.basename(movie)}
"movie_basename": os.path.basename(movie),
"movie_prefix": name}
eventManager().fire(Events.MOVIE_DONE, event_payload)
delete_unrendered_timelapse(name)
return f
@ -185,7 +221,8 @@ def _create_render_fail_handler(name, gcode=None):
def f(movie, returncode=255, stdout="Unknown error", stderr="Unknown error"):
event_payload = {"gcode": gcode if gcode is not None else "unknown",
"movie": movie,
"movie_basename": os.path.basename(movie)}
"movie_basename": os.path.basename(movie),
"movie_prefix": name}
payload = dict(event_payload)
payload.update(dict(returncode=returncode, error=stderr))
eventManager().fire(Events.MOVIE_FAILED, payload)
@ -195,7 +232,9 @@ def _create_render_fail_handler(name, gcode=None):
def _create_render_always_handler(name, gcode=None):
def f(movie):
global current_render_job
current_render_job = None
global _job_lock
with _job_lock:
current_render_job = None
return f
@ -267,6 +306,7 @@ class Timelapse(object):
self._image_number = None
self._in_timelapse = False
self._gcode_file = None
self._file_prefix = None
self._post_roll = post_roll
self._on_post_roll_done = None
@ -293,6 +333,10 @@ class Timelapse(object):
for (event, callback) in self.event_subscriptions():
eventManager().subscribe(event, callback)
@property
def prefix(self):
return self._file_prefix
@property
def post_roll(self):
return self._post_roll