mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-10 12:59:24 -03:00
feat(recipe): add reimport endpoint to re-import recipe from source URL
Adds POST /api/lm/recipe/{recipe_id}/reimport that atomically:
1. Reads the existing recipe to extract source_url and user edits
2. Deletes the old recipe files and cache entries
3. Re-downloads the image from CivitAI, re-parses EXIF metadata
4. Carries over user edits (title, tags, favorite) and timestamps
This commit is contained in:
@@ -102,6 +102,7 @@ class RecipeHandlerSet:
|
||||
"check_image_exists": self.management.check_image_exists,
|
||||
"import_from_url": self.management.import_from_url,
|
||||
"create_from_example": self.management.create_from_example,
|
||||
"reimport_recipe": self.management.reimport_recipe,
|
||||
}
|
||||
|
||||
|
||||
@@ -799,6 +800,116 @@ class RecipeManagementHandler:
|
||||
self._logger.error("Error repairing single recipe: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def reimport_recipe(self, request: web.Request) -> web.Response:
|
||||
"""Delete a recipe and re-import it from its source URL.
|
||||
|
||||
This gives the recipe a fresh start — re-downloads the image from
|
||||
CivitAI, re-parses EXIF metadata with the current parser, and
|
||||
re-resolves LoRAs / checkpoint. User edits (title, tags, favorite)
|
||||
are carried over from the old recipe.
|
||||
"""
|
||||
try:
|
||||
await self._ensure_dependencies_ready()
|
||||
recipe_scanner = self._recipe_scanner_getter()
|
||||
if recipe_scanner is None:
|
||||
raise RuntimeError("Recipe scanner unavailable")
|
||||
|
||||
recipe_id = request.match_info["recipe_id"]
|
||||
old_recipe = await recipe_scanner.get_recipe_by_id(recipe_id)
|
||||
if not old_recipe:
|
||||
raise RecipeNotFoundError(f"Recipe {recipe_id} not found")
|
||||
|
||||
source_path = old_recipe.get("source_path")
|
||||
if not source_path:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": (
|
||||
"Recipe has no source URL — cannot re-import. "
|
||||
"Use repair or manual import instead."
|
||||
),
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
user_edits: dict[str, Any] = {}
|
||||
for key in ("title", "tags", "favorite"):
|
||||
if key in old_recipe and old_recipe[key]:
|
||||
user_edits[key] = old_recipe[key]
|
||||
if "tags" in user_edits and not isinstance(user_edits["tags"], list):
|
||||
del user_edits["tags"]
|
||||
|
||||
old_created = old_recipe.get("created_date")
|
||||
old_modified = old_recipe.get("modified")
|
||||
|
||||
await self._persistence_service.delete_recipe(
|
||||
recipe_scanner=recipe_scanner, recipe_id=recipe_id
|
||||
)
|
||||
|
||||
async with self._import_semaphore:
|
||||
import_response = await self._do_import_from_url(
|
||||
source_path, recipe_scanner
|
||||
)
|
||||
|
||||
body_bytes = import_response.body
|
||||
if not body_bytes:
|
||||
raise RuntimeError("Re-import returned an empty response")
|
||||
import_body = json.loads(body_bytes.decode())
|
||||
new_recipe_id = import_body.get("recipe_id")
|
||||
|
||||
if new_recipe_id and user_edits:
|
||||
try:
|
||||
await self._persistence_service.update_recipe(
|
||||
recipe_scanner=recipe_scanner,
|
||||
recipe_id=new_recipe_id,
|
||||
updates=user_edits,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._logger.warning(
|
||||
"Re-import succeeded but failed to carry over "
|
||||
"user edits for new recipe %s: %s",
|
||||
new_recipe_id,
|
||||
exc,
|
||||
)
|
||||
|
||||
timestamp_updates: dict[str, Any] = {}
|
||||
if old_created is not None:
|
||||
timestamp_updates["created_date"] = old_created
|
||||
if old_modified is not None:
|
||||
timestamp_updates["modified"] = old_modified
|
||||
if new_recipe_id and timestamp_updates:
|
||||
try:
|
||||
await recipe_scanner.update_recipe_metadata(
|
||||
new_recipe_id, timestamp_updates
|
||||
)
|
||||
except Exception as exc:
|
||||
self._logger.warning(
|
||||
"Re-import succeeded but failed to preserve "
|
||||
"timestamps for new recipe %s: %s",
|
||||
new_recipe_id,
|
||||
exc,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"old_recipe_id": recipe_id,
|
||||
"recipe_id": new_recipe_id,
|
||||
"source_path": source_path,
|
||||
}
|
||||
)
|
||||
except RecipeNotFoundError as exc:
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=404)
|
||||
except RecipeValidationError as exc:
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=400)
|
||||
except RecipeDownloadError as exc:
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=400)
|
||||
except Exception as exc:
|
||||
self._logger.error(
|
||||
"Error reimporting recipe: %s", exc, exc_info=True
|
||||
)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_repair_progress(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
progress = self._ws_manager.get_recipe_repair_progress()
|
||||
|
||||
@@ -78,6 +78,9 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/recipes/create-from-example", "create_from_example"
|
||||
),
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/recipe/{recipe_id}/reimport", "reimport_recipe"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user