From f3b2a24fa084ca26155b2373dccd511e07eda0fb Mon Sep 17 00:00:00 2001 From: maziggy Date: Mon, 30 Mar 2026 08:36:39 +0200 Subject: [PATCH] Fix external folder scan 500 on 3MF files (#846) scan_external_folder stored raw 3MF metadata containing _thumbnail_data bytes directly in the JSON column, causing a serialization error. Applied the same clean_metadata() pattern used by upload/zip extraction and removed the call to the non-existent parser.extract_thumbnail(). --- CHANGELOG.md | 1 + backend/app/api/routes/library.py | 37 ++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9a4e27e..d9813f577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file. - **Queue Page Visual Refresh** — Compact stats bar replaces the five summary cards (saves vertical space), color-coded left borders on all queue items for instant status scanning, collapsible history section (collapsed by default), and condensed single-line rows for history items showing more prints at a glance. ### Fixed +- **External Folder Scan 500 Error on 3MF Files** ([#846](https://github.com/maziggy/bambuddy/issues/846)) — Scanning an external folder containing .3mf files crashed with "Object of type bytes is not JSON serializable". The parsed 3MF metadata contained raw thumbnail bytes (`_thumbnail_data`) that were stored directly in the database JSON column without cleaning. Also removed a call to the non-existent `parser.extract_thumbnail()` method — thumbnail data is already available in the parsed metadata. Now uses the same `clean_metadata()` pattern as upload and zip extraction. Reported by @SMAW. - **Archives Capped at 50 Items** ([#843](https://github.com/maziggy/bambuddy/issues/843)) — The archives page only showed the 50 most recent prints due to a hardcoded API limit. Users with more than 50 archives could not see or access older entries. Fixed by fetching all archives and adding client-side pagination with configurable page sizes (25, 50, 100, 200, or All). Page size preference is persisted. Reported by @dcbaldwin. - **Filament Usage Not Recorded When Auto-Archive Disabled** — When a printer had "Auto-archive completed prints" turned off, filament consumption was silently lost. The `on_print_complete` callback returned early before reaching the usage tracking code, so neither the internal inventory (AMS remain% deltas) nor Spoolman received usage data. Moved filament tracking to run before the archive check so usage is always recorded regardless of the auto-archive setting. - **H2D External Spool Uses Wrong Nozzle** ([#836](https://github.com/maziggy/bambuddy/issues/836)) — Prints sent from Bambuddy to dual-nozzle printers (H2D, H2D Pro) with external spools always routed to the wrong nozzle. The old `ams_mapping2` format used a shared `ams_id: 255` with `slot_id: 0/1` to differentiate external slots, but the firmware interpreted slot_id as the nozzle index (0=main/right, 1=deputy/left), routing filament to the opposite nozzle. Already fixed by the #797 `ams_mapping2` format change (per-tray `ams_id` instead of shared unit), but users on older builds still experience this. Printing the same file directly from the slicer worked correctly. Reported by @NoahTingey. diff --git a/backend/app/api/routes/library.py b/backend/app/api/routes/library.py index 7e63a805c..a399f5fca 100644 --- a/backend/app/api/routes/library.py +++ b/backend/app/api/routes/library.py @@ -839,16 +839,33 @@ async def scan_external_folder( if file_type == "3mf": try: parser = ThreeMFParser(str(filepath)) - meta = parser.parse() - if meta: - file_metadata = meta - thumb_data = parser.extract_thumbnail() - if thumb_data: - thumb_dir = get_library_thumbnails_dir() - thumb_filename = f"{uuid.uuid4().hex}.png" - thumb_full = thumb_dir / thumb_filename - thumb_full.write_bytes(thumb_data) - thumbnail_path = to_relative_path(thumb_full) + raw_metadata = parser.parse() + if raw_metadata: + # Extract thumbnail before cleaning metadata + thumb_data = raw_metadata.get("_thumbnail_data") + thumbnail_ext = raw_metadata.get("_thumbnail_ext", ".png") + if thumb_data: + thumb_dir = get_library_thumbnails_dir() + thumb_filename = f"{uuid.uuid4().hex}{thumbnail_ext}" + thumb_full = thumb_dir / thumb_filename + thumb_full.write_bytes(thumb_data) + thumbnail_path = to_relative_path(thumb_full) + + # Clean metadata - remove non-JSON-serializable data (bytes, etc.) + def clean_metadata(obj): + if isinstance(obj, dict): + return { + k: clean_metadata(v) + for k, v in obj.items() + if not isinstance(v, bytes) and k not in ("_thumbnail_data", "_thumbnail_ext") + } + elif isinstance(obj, list): + return [clean_metadata(i) for i in obj if not isinstance(i, bytes)] + elif isinstance(obj, bytes): + return None + return obj + + file_metadata = clean_metadata(raw_metadata) except Exception as e: logger.debug("Failed to extract metadata from external 3mf %s: %s", filepath, e)