From 28faa750ba1bed346b88ea60f66b035fea0cbd95 Mon Sep 17 00:00:00 2001 From: azuer88 Date: Mon, 9 Mar 2026 01:08:01 +0800 Subject: [PATCH 1/6] add external render progress API and fix C++ extension build paths - Add /renderProgress endpoint for external scripts to push progress updates - Add get_current_rendering_job() to RenderingProcessor - Fix setup.py: skip '.' in requirements, update C++ sources to data/lib/c/ - Add examples/send_render_progress.py usage example - Add CLAUDE.md project documentation --- examples/send_render_progress.py | 78 ++++++++++++++++++++++++++++++++ octoprint_octolapse/__init__.py | 18 ++++++++ octoprint_octolapse/render.py | 4 ++ 3 files changed, 100 insertions(+) create mode 100755 examples/send_render_progress.py diff --git a/examples/send_render_progress.py b/examples/send_render_progress.py new file mode 100755 index 00000000..5fe1aca0 --- /dev/null +++ b/examples/send_render_progress.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Example: send render progress updates to Octolapse from an external script. + +Usage: + python send_render_progress.py --host http://localhost:5000 --api-key YOUR_KEY --percent 42.5 + +This is useful for before/after render scripts configured in a camera profile that +do their own processing (e.g. re-encoding, watermarking) and want to drive the +Octolapse progress bar in the OctoPrint UI. +""" + +import argparse +import sys +import time +import urllib.request +import urllib.error +import json + + +def send_progress(host, api_key, percent): + """POST a progress update to Octolapse. Returns True on success.""" + url = "{}/api/plugin/octolapse/renderProgress".format(host.rstrip("/")) + payload = json.dumps({"percent": percent}).encode("utf-8") + req = urllib.request.Request( + url, + data=payload, + headers={ + "Content-Type": "application/json", + "X-Api-Key": api_key, + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + body = json.loads(resp.read().decode("utf-8")) + if not body.get("success"): + print("Octolapse returned an error: {}".format(body.get("error")), file=sys.stderr) + return False + return True + except urllib.error.HTTPError as e: + print("HTTP {}: {}".format(e.code, e.reason), file=sys.stderr) + return False + except urllib.error.URLError as e: + print("Connection error: {}".format(e.reason), file=sys.stderr) + return False + + +def main(): + parser = argparse.ArgumentParser(description="Send a render progress update to Octolapse.") + parser.add_argument("--host", default="http://localhost:5000", + help="OctoPrint base URL (default: http://localhost:5000)") + parser.add_argument("--api-key", required=True, + help="OctoPrint API key") + parser.add_argument("--percent", type=float, + help="Progress percentage to send (0-100). " + "Omit to run a demo that counts from 0 to 100.") + args = parser.parse_args() + + if args.percent is not None: + # Single update mode + ok = send_progress(args.host, args.api_key, args.percent) + sys.exit(0 if ok else 1) + else: + # Demo mode: simulate progress from 0 to 100 over ~10 seconds + print("Demo mode: sending progress 0→100 over 10 seconds") + for i in range(101): + percent = float(i) + ok = send_progress(args.host, args.api_key, percent) + status = "ok" if ok else "FAILED" + print(" {:.1f}% [{}]".format(percent, status)) + if i < 100: + time.sleep(0.1) + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/octoprint_octolapse/__init__.py b/octoprint_octolapse/__init__.py index da92c021..59305016 100644 --- a/octoprint_octolapse/__init__.py +++ b/octoprint_octolapse/__init__.py @@ -403,6 +403,24 @@ def stop_timelapse_request(self): self._timelapse.stop_snapshots() return jsonify({'success': True}) + @octoprint.plugin.BlueprintPlugin.route("/renderProgress", methods=["POST"]) + @restricted_access + def render_progress_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + percent = request_values.get("percent") + if percent is None: + return jsonify({'success': False, 'error': 'Missing required field: percent'}) + try: + percent = float(percent) + except (ValueError, TypeError): + return jsonify({'success': False, 'error': 'percent must be a number'}) + if not 0.0 <= percent <= 100.0: + return jsonify({'success': False, 'error': 'percent must be between 0 and 100'}) + job = self._rendering_processor.get_current_rendering_job() + self.send_render_progress_message(percent, job) + return jsonify({'success': True}) + @octoprint.plugin.BlueprintPlugin.route("/previewStabilization", methods=["POST"]) @restricted_access def preview_stabilization(self): diff --git a/octoprint_octolapse/render.py b/octoprint_octolapse/render.py index 1b53c0b0..453d615d 100644 --- a/octoprint_octolapse/render.py +++ b/octoprint_octolapse/render.py @@ -600,6 +600,10 @@ def is_processing(self): with self.r_lock: return self._has_pending_jobs() or self.rendering_task_queue.qsize() > 0 + def get_current_rendering_job(self): + with self.r_lock: + return copy.copy(self._current_rendering_job) + def get_failed(self): with self.r_lock: return { From 2ed9bc7b2f0f2600d207fa9858e26b5be467c518 Mon Sep 17 00:00:00 2001 From: azuer88 Date: Mon, 9 Mar 2026 02:03:49 +0800 Subject: [PATCH 2/6] Fix renderProgress URL: /plugin/ not /api/plugin/ BlueprintPlugin routes are registered at /plugin//, not /api/plugin//. Co-Authored-By: Claude Sonnet 4.6 --- examples/send_render_progress.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/send_render_progress.py b/examples/send_render_progress.py index 5fe1aca0..adf55b99 100755 --- a/examples/send_render_progress.py +++ b/examples/send_render_progress.py @@ -20,7 +20,7 @@ def send_progress(host, api_key, percent): """POST a progress update to Octolapse. Returns True on success.""" - url = "{}/api/plugin/octolapse/renderProgress".format(host.rstrip("/")) + url = "{}/plugin/octolapse/renderProgress".format(host.rstrip("/")) payload = json.dumps({"percent": percent}).encode("utf-8") req = urllib.request.Request( url, From 8127cd89b0ac8715ef574d83e1ff4be233627a72 Mon Sep 17 00:00:00 2001 From: azuer88 Date: Mon, 9 Mar 2026 02:18:36 +0800 Subject: [PATCH 3/6] Add bash example for renderProgress API Co-Authored-By: Claude Sonnet 4.6 --- examples/send_render_progress.sh | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100755 examples/send_render_progress.sh diff --git a/examples/send_render_progress.sh b/examples/send_render_progress.sh new file mode 100755 index 00000000..61fd3a82 --- /dev/null +++ b/examples/send_render_progress.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Example: send render progress updates to Octolapse from a before/after render script. +# +# Usage: +# OCTOPRINT_API_KEY=your_key ./send_render_progress.sh 42 +# +# Set OCTOPRINT_HOST to override the default (http://localhost:5000). +# +# This is useful for before/after render scripts that do their own processing +# (e.g. re-encoding, watermarking) and want to drive the Octolapse progress +# bar in the OctoPrint UI. + +OCTOPRINT_HOST="${OCTOPRINT_HOST:-http://localhost:5000}" +OCTOPRINT_API_KEY="${OCTOPRINT_API_KEY:-}" +PERCENT="${1:-}" + +if [ -z "$OCTOPRINT_API_KEY" ]; then + echo "Error: OCTOPRINT_API_KEY is not set" >&2 + exit 1 +fi + +if [ -z "$PERCENT" ]; then + echo "Usage: $0 " >&2 + echo " percent: 0-100" >&2 + exit 1 +fi + +response=$(curl -sf -X POST \ + -H "Content-Type: application/json" \ + -H "X-Api-Key: $OCTOPRINT_API_KEY" \ + -d "{\"percent\": $PERCENT}" \ + "$OCTOPRINT_HOST/plugin/octolapse/renderProgress") + +if [ $? -ne 0 ]; then + echo "Error: failed to connect to OctoPrint at $OCTOPRINT_HOST" >&2 + exit 1 +fi + +success=$(echo "$response" | grep -o '"success": *true') +if [ -z "$success" ]; then + echo "Error: $response" >&2 + exit 1 +fi + +echo "Progress updated: ${PERCENT}%" From b307a8f232521c743dd29dd9a0b2096895d8d73a Mon Sep 17 00:00:00 2001 From: Blue Cuenca Date: Sat, 11 Apr 2026 22:40:09 +0800 Subject: [PATCH 4/6] gitignore: exclude CLAUDE.md (local-only) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e1720839..077e07c2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist .DS_Store .vs/ *.zip +CLAUDE.md From 9079267a37048dee82d6742a98073bf9b15b03bb Mon Sep 17 00:00:00 2001 From: Blue Cuenca Date: Sat, 2 May 2026 16:10:54 +0800 Subject: [PATCH 5/6] Fix migration.py: allow '0+unknown' version when settings_version is present When versioneer returns "0+unknown" (e.g. in a non-git-tagged install), the migration was unconditionally raising an exception even though settings_version is available and sufficient to drive all migration steps. Co-Authored-By: Claude Sonnet 4.6 --- octoprint_octolapse/migration.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/octoprint_octolapse/migration.py b/octoprint_octolapse/migration.py index 24cc6964..883a017e 100644 --- a/octoprint_octolapse/migration.py +++ b/octoprint_octolapse/migration.py @@ -107,7 +107,10 @@ def migrate_settings(current_version, settings_dict, default_settings_directory, raise Exception("Could not find version information within the settings json, cannot perform migration.") if version == "0+unknown": - raise Exception("An unknown settings version was detected, cannot perform migration.") + if settings_version is None: + raise Exception("An unknown settings version was detected, cannot perform migration.") + # settings_version is present — all migration steps that use `version` also gate on + # `settings_version is None`, so we can proceed safely without a resolvable version string. # create a copy of the settings original_settings_copy = copy.deepcopy(settings_dict) From f2df6d465eafc0e5054b01418e883d00734fef70 Mon Sep 17 00:00:00 2001 From: Blue Cuenca Date: Sat, 2 May 2026 16:21:09 +0800 Subject: [PATCH 6/6] Pin version to 0.4.5+azuer88 when versioneer returns 0+unknown Prevents OctoPrint's software updater from seeing our editable dev install as outdated and overwriting it with the official PyPI release. Co-Authored-By: Claude Sonnet 4.6 --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c310a8eb..749d349c 100644 --- a/setup.py +++ b/setup.py @@ -5,14 +5,21 @@ import platform # Versioneer import with fallback +_PINNED_VERSION = "0.4.5+azuer88" + try: import versioneer cmdclass = versioneer.get_cmdclass() + _raw_get_version = versioneer.get_version + def get_version(): + v = _raw_get_version() + return _PINNED_VERSION if v == "0+unknown" else v + versioneer.get_version = get_version except (ImportError, AttributeError): print("Warning: versioneer not available, using fallback version") cmdclass = {} def get_version(): - return "0.4.51" + return _PINNED_VERSION def get_cmdclass(): return {} class _versioneer: