|
@@ -1,4 +1,5 @@
|
|
|
import io
|
|
import io
|
|
|
|
|
+import json
|
|
|
import logging
|
|
import logging
|
|
|
import zipfile
|
|
import zipfile
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
@@ -180,7 +181,7 @@ async def search_archives(
|
|
|
result = await db.execute(fts_query, {"search_term": search_term, "limit": limit + 100, "offset": 0})
|
|
result = await db.execute(fts_query, {"search_term": search_term, "limit": limit + 100, "offset": 0})
|
|
|
matched_ids = [row[0] for row in result.fetchall()]
|
|
matched_ids = [row[0] for row in result.fetchall()]
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.warning(f"FTS search failed, falling back to LIKE search: {e}")
|
|
|
|
|
|
|
+ logger.warning("FTS search failed, falling back to LIKE search: %s", e)
|
|
|
# Fallback to LIKE search if FTS fails
|
|
# Fallback to LIKE search if FTS fails
|
|
|
like_pattern = f"%{q}%"
|
|
like_pattern = f"%{q}%"
|
|
|
query = (
|
|
query = (
|
|
@@ -265,7 +266,7 @@ async def rebuild_search_index(
|
|
|
|
|
|
|
|
return {"message": f"Search index rebuilt with {count} entries"}
|
|
return {"message": f"Search index rebuilt with {count} entries"}
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.error(f"Failed to rebuild search index: {e}")
|
|
|
|
|
|
|
+ logger.error("Failed to rebuild search index: %s", e)
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to rebuild index: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to rebuild index: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
@@ -941,7 +942,7 @@ async def rescan_all_archives(
|
|
|
|
|
|
|
|
updated += 1
|
|
updated += 1
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.exception(f"Failed to rescan archive {archive.id}: {e}")
|
|
|
|
|
|
|
+ logger.exception("Failed to rescan archive %s: %s", archive.id, e)
|
|
|
errors.append({"id": archive.id, "error": "Failed to parse 3MF file"})
|
|
errors.append({"id": archive.id, "error": "Failed to parse 3MF file"})
|
|
|
|
|
|
|
|
await db.commit()
|
|
await db.commit()
|
|
@@ -992,7 +993,7 @@ async def backfill_content_hashes(
|
|
|
archive.content_hash = ArchiveService.compute_file_hash(file_path)
|
|
archive.content_hash = ArchiveService.compute_file_hash(file_path)
|
|
|
updated += 1
|
|
updated += 1
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.exception(f"Failed to compute hash for archive {archive.id}: {e}")
|
|
|
|
|
|
|
+ logger.exception("Failed to compute hash for archive %s: %s", archive.id, e)
|
|
|
errors.append({"id": archive.id, "error": "Failed to compute hash"})
|
|
errors.append({"id": archive.id, "error": "Failed to compute hash"})
|
|
|
|
|
|
|
|
await db.commit()
|
|
await db.commit()
|
|
@@ -1262,14 +1263,14 @@ async def scan_timelapse(
|
|
|
# Accept match within 4 hours (more lenient for timezone issues)
|
|
# Accept match within 4 hours (more lenient for timezone issues)
|
|
|
if best_match and best_diff < timedelta(hours=4):
|
|
if best_match and best_diff < timedelta(hours=4):
|
|
|
matching_file = best_match
|
|
matching_file = best_match
|
|
|
- logger.info(f"Matched timelapse by timestamp: {best_match.get('name')} (diff: {best_diff})")
|
|
|
|
|
|
|
+ logger.info("Matched timelapse by timestamp: %s (diff: %s)", best_match.get("name"), best_diff)
|
|
|
|
|
|
|
|
# Strategy 3: Use file modification time from FTP listing
|
|
# Strategy 3: Use file modification time from FTP listing
|
|
|
# This handles cases where printer's filename timestamp is wrong but file mtime is correct
|
|
# This handles cases where printer's filename timestamp is wrong but file mtime is correct
|
|
|
if not matching_file and (archive.started_at or archive.completed_at or archive.created_at):
|
|
if not matching_file and (archive.started_at or archive.completed_at or archive.created_at):
|
|
|
from datetime import datetime, timedelta
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
- archive_start = archive.started_at
|
|
|
|
|
|
|
+ _archive_start = archive.started_at
|
|
|
archive_end = archive.completed_at or archive.created_at
|
|
archive_end = archive.completed_at or archive.created_at
|
|
|
best_match = None
|
|
best_match = None
|
|
|
best_diff = timedelta(hours=24)
|
|
best_diff = timedelta(hours=24)
|
|
@@ -1290,7 +1291,7 @@ async def scan_timelapse(
|
|
|
|
|
|
|
|
if best_match and best_diff < timedelta(hours=2):
|
|
if best_match and best_diff < timedelta(hours=2):
|
|
|
matching_file = best_match
|
|
matching_file = best_match
|
|
|
- logger.info(f"Matched timelapse by file mtime: {best_match.get('name')} (diff: {best_diff})")
|
|
|
|
|
|
|
+ logger.info("Matched timelapse by file mtime: %s (diff: %s)", best_match.get("name"), best_diff)
|
|
|
|
|
|
|
|
# Strategy 4: If only one timelapse exists and archive was recently completed, use it
|
|
# Strategy 4: If only one timelapse exists and archive was recently completed, use it
|
|
|
# This handles cases where printer clock is wrong or timezone issues exist
|
|
# This handles cases where printer clock is wrong or timezone issues exist
|
|
@@ -1303,7 +1304,7 @@ async def scan_timelapse(
|
|
|
# If archive was completed within the last hour, assume the single timelapse is for it
|
|
# If archive was completed within the last hour, assume the single timelapse is for it
|
|
|
if time_since_completion < timedelta(hours=1):
|
|
if time_since_completion < timedelta(hours=1):
|
|
|
matching_file = mp4_files[0]
|
|
matching_file = mp4_files[0]
|
|
|
- logger.info(f"Using single timelapse file as fallback: {mp4_files[0].get('name')}")
|
|
|
|
|
|
|
+ logger.info("Using single timelapse file as fallback: %s", mp4_files[0].get("name"))
|
|
|
|
|
|
|
|
# Note: We intentionally don't use a "most recent file" fallback because
|
|
# Note: We intentionally don't use a "most recent file" fallback because
|
|
|
# we can't verify if timelapse was actually enabled for this print.
|
|
# we can't verify if timelapse was actually enabled for this print.
|
|
@@ -1505,7 +1506,7 @@ async def get_timelapse_info(
|
|
|
info = await processor.get_info()
|
|
info = await processor.get_info()
|
|
|
return TimelapseInfoResponse(**info)
|
|
return TimelapseInfoResponse(**info)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.error(f"Failed to get timelapse info: {e}")
|
|
|
|
|
|
|
+ logger.error("Failed to get timelapse info: %s", e)
|
|
|
raise HTTPException(500, f"Failed to get video info: {str(e)}")
|
|
raise HTTPException(500, f"Failed to get video info: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1541,7 +1542,7 @@ async def get_timelapse_thumbnails(
|
|
|
timestamps=[ts for ts, _ in thumbnails],
|
|
timestamps=[ts for ts, _ in thumbnails],
|
|
|
)
|
|
)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.error(f"Failed to generate thumbnails: {e}")
|
|
|
|
|
|
|
+ logger.error("Failed to generate thumbnails: %s", e)
|
|
|
raise HTTPException(500, f"Failed to generate thumbnails: {str(e)}")
|
|
raise HTTPException(500, f"Failed to generate thumbnails: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1647,7 +1648,7 @@ async def process_timelapse(
|
|
|
except HTTPException:
|
|
except HTTPException:
|
|
|
raise
|
|
raise
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.error(f"Timelapse processing failed: {e}")
|
|
|
|
|
|
|
+ logger.error("Timelapse processing failed: %s", e)
|
|
|
raise HTTPException(500, f"Processing failed: {str(e)}")
|
|
raise HTTPException(500, f"Processing failed: {str(e)}")
|
|
|
finally:
|
|
finally:
|
|
|
# Cleanup temp audio file
|
|
# Cleanup temp audio file
|
|
@@ -1838,8 +1839,6 @@ async def get_archive_capabilities(
|
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ),
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ),
|
|
|
):
|
|
):
|
|
|
"""Check what viewing capabilities are available for this 3MF file."""
|
|
"""Check what viewing capabilities are available for this 3MF file."""
|
|
|
- import json
|
|
|
|
|
-
|
|
|
|
|
import defusedxml.ElementTree as ET
|
|
import defusedxml.ElementTree as ET
|
|
|
|
|
|
|
|
service = ArchiveService(db)
|
|
service = ArchiveService(db)
|
|
@@ -1883,8 +1882,8 @@ async def get_archive_capabilities(
|
|
|
if "<vertex" in content or "<mesh" in content:
|
|
if "<vertex" in content or "<mesh" in content:
|
|
|
found_mesh = True
|
|
found_mesh = True
|
|
|
break
|
|
break
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (KeyError, UnicodeDecodeError):
|
|
|
|
|
+ pass # Skip unreadable .model entries in archive
|
|
|
|
|
|
|
|
# Extract filament colors from project_settings.config
|
|
# Extract filament colors from project_settings.config
|
|
|
if "Metadata/project_settings.config" in names:
|
|
if "Metadata/project_settings.config" in names:
|
|
@@ -1906,7 +1905,7 @@ async def get_archive_capabilities(
|
|
|
max_x = max(max_x, x)
|
|
max_x = max(max_x, x)
|
|
|
max_y = max(max_y, y)
|
|
max_y = max(max_y, y)
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip non-numeric printable_area coordinate
|
|
|
if max_x > 0 and max_y > 0:
|
|
if max_x > 0 and max_y > 0:
|
|
|
volume["x"] = max_x
|
|
volume["x"] = max_x
|
|
|
volume["y"] = max_y
|
|
volume["y"] = max_y
|
|
@@ -1917,7 +1916,7 @@ async def get_archive_capabilities(
|
|
|
try:
|
|
try:
|
|
|
volume["z"] = int(printable_height)
|
|
volume["z"] = int(printable_height)
|
|
|
except (ValueError, TypeError):
|
|
except (ValueError, TypeError):
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip unparseable printable_height value
|
|
|
|
|
|
|
|
# Extract filament colors
|
|
# Extract filament colors
|
|
|
raw_colors = config_data.get("filament_colour", [])
|
|
raw_colors = config_data.get("filament_colour", [])
|
|
@@ -1925,10 +1924,10 @@ async def get_archive_capabilities(
|
|
|
for color in raw_colors:
|
|
for color in raw_colors:
|
|
|
if color and isinstance(color, str):
|
|
if color and isinstance(color, str):
|
|
|
colors.append(color)
|
|
colors.append(color)
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (json.JSONDecodeError, KeyError, ValueError, TypeError):
|
|
|
|
|
+ pass # Skip malformed project_settings.config
|
|
|
except zipfile.BadZipFile:
|
|
except zipfile.BadZipFile:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # File is not a valid zip/3MF archive
|
|
|
|
|
|
|
|
return found_mesh, colors, volume
|
|
return found_mesh, colors, volume
|
|
|
|
|
|
|
@@ -1958,8 +1957,8 @@ async def get_archive_capabilities(
|
|
|
if "<vertex" in content or "<mesh" in content:
|
|
if "<vertex" in content or "<mesh" in content:
|
|
|
has_model = True
|
|
has_model = True
|
|
|
break
|
|
break
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (KeyError, UnicodeDecodeError):
|
|
|
|
|
+ pass # Skip unreadable .model entries in archive
|
|
|
|
|
|
|
|
# Extract filament colors from slice_info.config (for gcode preview)
|
|
# Extract filament colors from slice_info.config (for gcode preview)
|
|
|
# These are the actual filaments used in the print, indexed by tool/extruder
|
|
# These are the actual filaments used in the print, indexed by tool/extruder
|
|
@@ -1986,14 +1985,14 @@ async def get_archive_capabilities(
|
|
|
if tool_id >= 0 and used_amount > 0:
|
|
if tool_id >= 0 and used_amount > 0:
|
|
|
filament_map[tool_id] = fcolor
|
|
filament_map[tool_id] = fcolor
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip filament entry with non-numeric ID
|
|
|
|
|
|
|
|
if filament_map:
|
|
if filament_map:
|
|
|
max_tool = max(filament_map.keys())
|
|
max_tool = max(filament_map.keys())
|
|
|
for i in range(max_tool + 1):
|
|
for i in range(max_tool + 1):
|
|
|
slice_colors.append(filament_map.get(i, "#00AE42"))
|
|
slice_colors.append(filament_map.get(i, "#00AE42"))
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (KeyError, ValueError, ET.ParseError, UnicodeDecodeError):
|
|
|
|
|
+ pass # Skip malformed slice_info.config XML
|
|
|
|
|
|
|
|
# Use slice_info colors if we don't have colors from source yet
|
|
# Use slice_info colors if we don't have colors from source yet
|
|
|
if not filament_colors and slice_colors:
|
|
if not filament_colors and slice_colors:
|
|
@@ -2019,7 +2018,7 @@ async def get_archive_capabilities(
|
|
|
max_x = max(max_x, x)
|
|
max_x = max(max_x, x)
|
|
|
max_y = max(max_y, y)
|
|
max_y = max(max_y, y)
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip non-numeric printable_area coordinate
|
|
|
if max_x > 0 and max_y > 0:
|
|
if max_x > 0 and max_y > 0:
|
|
|
build_volume["x"] = max_x
|
|
build_volume["x"] = max_x
|
|
|
build_volume["y"] = max_y
|
|
build_volume["y"] = max_y
|
|
@@ -2029,7 +2028,7 @@ async def get_archive_capabilities(
|
|
|
try:
|
|
try:
|
|
|
build_volume["z"] = int(printable_height)
|
|
build_volume["z"] = int(printable_height)
|
|
|
except (ValueError, TypeError):
|
|
except (ValueError, TypeError):
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip unparseable printable_height value
|
|
|
|
|
|
|
|
# Fallback colors from project_settings if still empty
|
|
# Fallback colors from project_settings if still empty
|
|
|
if not filament_colors:
|
|
if not filament_colors:
|
|
@@ -2038,8 +2037,8 @@ async def get_archive_capabilities(
|
|
|
for color in raw_colors:
|
|
for color in raw_colors:
|
|
|
if color and isinstance(color, str):
|
|
if color and isinstance(color, str):
|
|
|
filament_colors.append(color)
|
|
filament_colors.append(color)
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (json.JSONDecodeError, KeyError, ValueError, TypeError):
|
|
|
|
|
+ pass # Skip malformed project_settings.config
|
|
|
|
|
|
|
|
except zipfile.BadZipFile:
|
|
except zipfile.BadZipFile:
|
|
|
raise HTTPException(400, "Invalid 3MF file")
|
|
raise HTTPException(400, "Invalid 3MF file")
|
|
@@ -2127,8 +2126,8 @@ async def get_plate_preview(
|
|
|
plate_elem = root.find(".//plate/metadata[@key='index']")
|
|
plate_elem = root.find(".//plate/metadata[@key='index']")
|
|
|
if plate_elem is not None:
|
|
if plate_elem is not None:
|
|
|
plate_num = int(plate_elem.get("value", "1"))
|
|
plate_num = int(plate_elem.get("value", "1"))
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (KeyError, ValueError, ET.ParseError, UnicodeDecodeError):
|
|
|
|
|
+ pass # Default plate_num=1 if slice_info is missing or malformed
|
|
|
|
|
|
|
|
# Try plate-specific image first, then fall back to plate_1
|
|
# Try plate-specific image first, then fall back to plate_1
|
|
|
preview_paths = [
|
|
preview_paths = [
|
|
@@ -2234,7 +2233,7 @@ async def upload_archives_bulk(
|
|
|
else:
|
|
else:
|
|
|
errors.append({"filename": file.filename, "error": "Failed to process"})
|
|
errors.append({"filename": file.filename, "error": "Failed to process"})
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.exception(f"Failed to upload archive {file.filename}: {e}")
|
|
|
|
|
|
|
+ logger.exception("Failed to upload archive %s: %s", file.filename, e)
|
|
|
errors.append({"filename": file.filename, "error": "Failed to process file"})
|
|
errors.append({"filename": file.filename, "error": "Failed to process file"})
|
|
|
finally:
|
|
finally:
|
|
|
if temp_path.exists():
|
|
if temp_path.exists():
|
|
@@ -2259,7 +2258,6 @@ async def get_archive_plates(
|
|
|
Returns a list of plates with their index, name, thumbnail availability,
|
|
Returns a list of plates with their index, name, thumbnail availability,
|
|
|
and filament requirements. For single-plate exports, returns a single plate.
|
|
and filament requirements. For single-plate exports, returns a single plate.
|
|
|
"""
|
|
"""
|
|
|
- import json
|
|
|
|
|
import re
|
|
import re
|
|
|
|
|
|
|
|
import defusedxml.ElementTree as ET
|
|
import defusedxml.ElementTree as ET
|
|
@@ -2292,7 +2290,7 @@ async def get_archive_plates(
|
|
|
plate_str = gf[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
|
plate_str = gf[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
|
|
plate_indices.append(int(plate_str))
|
|
plate_indices.append(int(plate_str))
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip gcode file with non-numeric plate index
|
|
|
else:
|
|
else:
|
|
|
plate_json_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".json")]
|
|
plate_json_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".json")]
|
|
|
plate_png_files = [
|
|
plate_png_files = [
|
|
@@ -2358,7 +2356,7 @@ async def get_archive_plates(
|
|
|
try:
|
|
try:
|
|
|
plater_id = int(value)
|
|
plater_id = int(value)
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip plate with non-numeric plater_id
|
|
|
elif key == "plater_name" and value:
|
|
elif key == "plater_name" and value:
|
|
|
plater_name = value.strip()
|
|
plater_name = value.strip()
|
|
|
if plater_id is not None and plater_name:
|
|
if plater_id is not None and plater_name:
|
|
@@ -2374,7 +2372,7 @@ async def get_archive_plates(
|
|
|
plate_object_ids.setdefault(plater_id, [])
|
|
plate_object_ids.setdefault(plater_id, [])
|
|
|
if obj_id not in plate_object_ids[plater_id]:
|
|
if obj_id not in plate_object_ids[plater_id]:
|
|
|
plate_object_ids[plater_id].append(obj_id)
|
|
plate_object_ids[plater_id].append(obj_id)
|
|
|
- except Exception:
|
|
|
|
|
|
|
+ except (KeyError, ValueError, ET.ParseError, UnicodeDecodeError):
|
|
|
pass # model_settings.config parsing is optional
|
|
pass # model_settings.config parsing is optional
|
|
|
|
|
|
|
|
# Parse slice_info.config for plate metadata
|
|
# Parse slice_info.config for plate metadata
|
|
@@ -2395,17 +2393,17 @@ async def get_archive_plates(
|
|
|
try:
|
|
try:
|
|
|
plate_index = int(value)
|
|
plate_index = int(value)
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip plate with non-numeric index
|
|
|
elif key == "prediction" and value:
|
|
elif key == "prediction" and value:
|
|
|
try:
|
|
try:
|
|
|
plate_info["prediction"] = int(value)
|
|
plate_info["prediction"] = int(value)
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip non-numeric print time prediction
|
|
|
elif key == "weight" and value:
|
|
elif key == "weight" and value:
|
|
|
try:
|
|
try:
|
|
|
plate_info["weight"] = float(value)
|
|
plate_info["weight"] = float(value)
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip non-numeric filament weight
|
|
|
|
|
|
|
|
# Get filaments used in this plate
|
|
# Get filaments used in this plate
|
|
|
for filament_elem in plate_elem.findall("filament"):
|
|
for filament_elem in plate_elem.findall("filament"):
|
|
@@ -2473,7 +2471,7 @@ async def get_archive_plates(
|
|
|
names.append(obj_name)
|
|
names.append(obj_name)
|
|
|
if names:
|
|
if names:
|
|
|
plate_json_objects[plate_index] = names
|
|
plate_json_objects[plate_index] = names
|
|
|
- except Exception:
|
|
|
|
|
|
|
+ except (json.JSONDecodeError, KeyError, ValueError, UnicodeDecodeError):
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
# Build plate list
|
|
# Build plate list
|
|
@@ -2510,8 +2508,8 @@ async def get_archive_plates(
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.warning(f"Failed to parse plates from archive {archive_id}: {e}")
|
|
|
|
|
|
|
+ except (KeyError, ValueError, zipfile.BadZipFile, ET.ParseError, UnicodeDecodeError) as e:
|
|
|
|
|
+ logger.warning("Failed to parse plates from archive %s: %s", archive_id, e)
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
"archive_id": archive_id,
|
|
"archive_id": archive_id,
|
|
@@ -2546,8 +2544,8 @@ async def get_plate_thumbnail(
|
|
|
if thumb_path in zf.namelist():
|
|
if thumb_path in zf.namelist():
|
|
|
data = zf.read(thumb_path)
|
|
data = zf.read(thumb_path)
|
|
|
return Response(content=data, media_type="image/png")
|
|
return Response(content=data, media_type="image/png")
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ except (zipfile.BadZipFile, KeyError, OSError):
|
|
|
|
|
+ pass # Fall through to 404 if archive is unreadable or thumbnail missing
|
|
|
|
|
|
|
|
raise HTTPException(404, f"Thumbnail for plate {plate_index} not found")
|
|
raise HTTPException(404, f"Thumbnail for plate {plate_index} not found")
|
|
|
|
|
|
|
@@ -2598,7 +2596,7 @@ async def get_filament_requirements(
|
|
|
try:
|
|
try:
|
|
|
plate_index = int(meta.get("value", "0"))
|
|
plate_index = int(meta.get("value", "0"))
|
|
|
except ValueError:
|
|
except ValueError:
|
|
|
- pass
|
|
|
|
|
|
|
+ pass # Skip plate with non-numeric index metadata
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
if plate_index == plate_id:
|
|
if plate_index == plate_id:
|
|
@@ -2662,8 +2660,8 @@ async def get_filament_requirements(
|
|
|
# Sort by slot ID
|
|
# Sort by slot ID
|
|
|
filaments.sort(key=lambda x: x["slot_id"])
|
|
filaments.sort(key=lambda x: x["slot_id"])
|
|
|
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.warning(f"Failed to parse filament requirements from archive {archive_id}: {e}")
|
|
|
|
|
|
|
+ except (KeyError, ValueError, zipfile.BadZipFile, ET.ParseError, UnicodeDecodeError) as e:
|
|
|
|
|
+ logger.warning("Failed to parse filament requirements from archive %s: %s", archive_id, e)
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
"archive_id": archive_id,
|
|
"archive_id": archive_id,
|
|
@@ -2751,7 +2749,7 @@ async def reprint_archive(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
# Delete existing file if present (avoids 553 error)
|
|
# Delete existing file if present (avoids 553 error)
|
|
|
- logger.debug(f"Deleting existing file {remote_path} if present...")
|
|
|
|
|
|
|
+ logger.debug("Deleting existing file %s if present...", remote_path)
|
|
|
delete_result = await delete_file_async(
|
|
delete_result = await delete_file_async(
|
|
|
printer.ip_address,
|
|
printer.ip_address,
|
|
|
printer.access_code,
|
|
printer.access_code,
|
|
@@ -2759,7 +2757,7 @@ async def reprint_archive(
|
|
|
socket_timeout=ftp_timeout,
|
|
socket_timeout=ftp_timeout,
|
|
|
printer_model=printer.model,
|
|
printer_model=printer.model,
|
|
|
)
|
|
)
|
|
|
- logger.debug(f"Delete result: {delete_result}")
|
|
|
|
|
|
|
+ logger.debug("Delete result: %s", delete_result)
|
|
|
|
|
|
|
|
if ftp_retry_enabled:
|
|
if ftp_retry_enabled:
|
|
|
uploaded = await with_ftp_retry(
|
|
uploaded = await with_ftp_retry(
|
|
@@ -2813,7 +2811,7 @@ async def reprint_archive(
|
|
|
plate_str = name[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
|
plate_str = name[15:-6] # Remove "Metadata/plate_" and ".gcode"
|
|
|
plate_id = int(plate_str)
|
|
plate_id = int(plate_str)
|
|
|
break
|
|
break
|
|
|
- except Exception:
|
|
|
|
|
|
|
+ except (ValueError, zipfile.BadZipFile, OSError):
|
|
|
pass # Default to plate 1 if detection fails
|
|
pass # Default to plate 1 if detection fails
|
|
|
|
|
|
|
|
logger.info(
|
|
logger.info(
|
|
@@ -2843,7 +2841,7 @@ async def reprint_archive(
|
|
|
# Track who started this print (Issue #206)
|
|
# Track who started this print (Issue #206)
|
|
|
if user:
|
|
if user:
|
|
|
printer_manager.set_current_print_user(printer_id, user.id, user.username)
|
|
printer_manager.set_current_print_user(printer_id, user.id, user.username)
|
|
|
- logger.info(f"Reprint started by user: {user.username}")
|
|
|
|
|
|
|
+ logger.info("Reprint started by user: %s", user.username)
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
"status": "printing",
|
|
"status": "printing",
|