소스 검색

Fetch timelapse of failed and cancelled prints (#3481)

- The automatic timelapse scan ran only for completed prints, so a
  failed or cancelled print's video stayed on the printer. The printer
  saves it up to where the print stopped (measured on an H2C), so the
  scan now runs whenever the print was recording.
- Tests: a failed or aborted print schedules the scan for its run; no
  scan without a recording.
- CHANGELOG entry.
maziggy 3 일 전
부모
커밋
0ef31fb43e
3개의 변경된 파일과 27개의 추가작업 그리고 2개의 파일을 삭제
  1. 1 0
      CHANGELOG.md
  2. 6 2
      backend/app/main.py
  3. 20 0
      backend/tests/integration/test_print_complete_end_to_end.py

+ 1 - 0
CHANGELOG.md

@@ -12,6 +12,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 
 ### Fixed
+- **Failed and cancelled prints get their timelapse too (#3481)** — The printer saves the timelapse of a print that fails or is cancelled, up to where it stopped, and that video is the best way to see what went wrong. Bambuddy fetched timelapses only for prints that completed, so a stopped print's video stayed on the printer until it was attached by hand with **Scan for Timelapse**. It is now fetched and attached to that print's run like any other. Wiki updated.
 - **A printer named after its model no longer hides the model in the support package (#3442, reported by @daltonclift)** — The support package replaces printer names with [PRINTER]. A printer named "H3C" turned every "H3C" in the package into [PRINTER], including the model itself. A name that is only a model name or model code is not private, so it now stays; any other name is still replaced.
 - **Auto-drying no longer waits on a drying timer the printer never started (#3478, reported by @superflyer11)** — An AMS HT can accept a drying command while it feeds a print and never heat: its timer stays at the full time (12h 0m) and its status reads Off, for days. Since 1.3.1b1 the printer card shows such a timer as not running, but auto-drying still took it for a running cycle and never dried that unit again, while the humidity alarms kept firing. A timer that has not moved for a few minutes with the status Off now counts as not drying, so auto-drying tries again after its usual 30-minute pause. If that happens twice in a row, auto-drying stops on that unit and sends the **Auto-Drying Suspended** notification, saying the timer stood still (new template **Auto-Drying Not Heating**). It resumes once the humidity is back at or below the threshold. Wiki updated.
 - **The log says which auto-archive switch skipped a print (#3477, reported by @Fuzzylog2x)** — Since 1.3.0 the global **Auto-archive prints** switch (Settings > General > Archive Settings) takes effect for prints started outside Bambuddy. Before, it was saved and never read, so an install upgraded with it off stopped archiving, and the statistics stopped updating. The log only said "auto_archive: False", the same as for a printer's own switch. It now names the global switch or the printer, whichever turned archiving off. The switch's description now says what off means: those prints are missing from the archive, the print history and the statistics, while spool inventory still counts their filament. The wiki explains how to keep statistics without keeping every 3MF (auto-purge removes the files and keeps the statistics), and no longer says auto-purge removes prints from the statistics.

+ 6 - 2
backend/app/main.py

@@ -10795,8 +10795,12 @@ async def _close_out_print(printer_id: int, data: dict):
 
     log_timing("All background tasks scheduled")
 
-    # Auto-scan for timelapse if recording was active during the print
-    if archive_id and data.get("timelapse_was_active") and data.get("status") == "completed":
+    # Auto-scan for timelapse if recording was active during the print. A
+    # failed or cancelled print gets one too: the printer writes the video of
+    # the run so far when it stops (measured on an H2C, #3481), and that video
+    # is how the user sees what went wrong. A model that writes none costs one
+    # poll that finds nothing new.
+    if archive_id and data.get("timelapse_was_active"):
         logger.info("[TIMELAPSE] Timelapse was active during print, scheduling auto-scan for archive %s", archive_id)
         # Schedule timelapse scan as background task with retries
         # The printer needs time to encode the video after print completion

+ 20 - 0
backend/tests/integration/test_print_complete_end_to_end.py

@@ -399,6 +399,26 @@ async def test_the_timelapse_scan_is_for_the_run_that_ended(test_engine, db_sess
     assert schedule.call_args.kwargs["run_id"] == logs[0].id
 
 
+@pytest.mark.parametrize("status", ["failed", "aborted"])
+async def test_a_stopped_prints_timelapse_is_fetched_too(test_engine, db_session, queued_print, status):
+    """#3481: the printer writes the video of a cancelled or failed run, and
+    it is the one video that shows what went wrong."""
+    with patch("backend.app.main._schedule_timelapse_scan") as schedule:
+        await _complete(test_engine, queued_print["printer"], status, extra={"timelapse_was_active": True})
+
+    _archive, _item, logs, _charges = await _rows(db_session, queued_print["archive"], queued_print["item"])
+    schedule.assert_called_once()
+    assert schedule.call_args.args[0] == queued_print["archive"]
+    assert schedule.call_args.kwargs["run_id"] == logs[0].id
+
+
+async def test_no_timelapse_scan_without_a_recording(test_engine, queued_print):
+    with patch("backend.app.main._schedule_timelapse_scan") as schedule:
+        await _complete(test_engine, queued_print["printer"], "aborted")
+
+    schedule.assert_not_called()
+
+
 async def test_the_finish_photo_is_the_ended_runs_too(test_engine, db_session, queued_print):
     from backend.app.core.tasks import spawn_background_task