mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Add sharing functionality for recipes
- Introduced new endpoints for sharing recipes and downloading shared images in RecipeRoutes. - Implemented logic to process recipe images and append metadata to EXIF data. - Updated RecipeCard component to handle sharing via API calls, providing user feedback during the process. - Enhanced error handling for missing recipe IDs and failed API responses.
This commit is contained in:
@@ -44,6 +44,10 @@ class RecipeRoutes:
|
|||||||
app.router.add_get('/api/recipes/top-tags', routes.get_top_tags)
|
app.router.add_get('/api/recipes/top-tags', routes.get_top_tags)
|
||||||
app.router.add_get('/api/recipes/base-models', routes.get_base_models)
|
app.router.add_get('/api/recipes/base-models', routes.get_base_models)
|
||||||
|
|
||||||
|
# Add new sharing endpoints
|
||||||
|
app.router.add_get('/api/recipe/{recipe_id}/share', routes.share_recipe)
|
||||||
|
app.router.add_get('/api/recipe/{recipe_id}/share/download', routes.download_shared_recipe)
|
||||||
|
|
||||||
# Start cache initialization
|
# Start cache initialization
|
||||||
app.on_startup.append(routes._init_cache)
|
app.on_startup.append(routes._init_cache)
|
||||||
|
|
||||||
@@ -598,3 +602,122 @@ class RecipeRoutes:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
async def share_recipe(self, request: web.Request) -> web.Response:
|
||||||
|
"""Process a recipe image for sharing by adding metadata to EXIF"""
|
||||||
|
try:
|
||||||
|
recipe_id = request.match_info['recipe_id']
|
||||||
|
|
||||||
|
# Get all recipes from cache
|
||||||
|
cache = await self.recipe_scanner.get_cached_data()
|
||||||
|
|
||||||
|
# Find the specific recipe
|
||||||
|
recipe = next((r for r in cache.raw_data if str(r.get('id', '')) == recipe_id), None)
|
||||||
|
|
||||||
|
if not recipe:
|
||||||
|
return web.json_response({"error": "Recipe not found"}, status=404)
|
||||||
|
|
||||||
|
# Get the image path
|
||||||
|
image_path = recipe.get('file_path')
|
||||||
|
if not image_path or not os.path.exists(image_path):
|
||||||
|
return web.json_response({"error": "Recipe image not found"}, status=404)
|
||||||
|
|
||||||
|
# Create a temporary copy of the image to modify
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Create temp file with same extension
|
||||||
|
ext = os.path.splitext(image_path)[1]
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as temp_file:
|
||||||
|
temp_path = temp_file.name
|
||||||
|
|
||||||
|
# Copy the original image to temp file
|
||||||
|
shutil.copy2(image_path, temp_path)
|
||||||
|
|
||||||
|
# Add recipe metadata to the image
|
||||||
|
from ..utils.exif_utils import ExifUtils
|
||||||
|
processed_path = ExifUtils.append_recipe_metadata(temp_path, recipe)
|
||||||
|
|
||||||
|
# Create a URL for the processed image
|
||||||
|
# Use a timestamp to prevent caching
|
||||||
|
timestamp = int(time.time())
|
||||||
|
filename = os.path.basename(processed_path)
|
||||||
|
url_path = f"/api/recipe/{recipe_id}/share/download?t={timestamp}"
|
||||||
|
|
||||||
|
# Store the temp path in a dictionary to serve later
|
||||||
|
if not hasattr(self, '_shared_recipes'):
|
||||||
|
self._shared_recipes = {}
|
||||||
|
|
||||||
|
self._shared_recipes[recipe_id] = {
|
||||||
|
'path': processed_path,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'expires': time.time() + 300 # Expire after 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up old entries
|
||||||
|
self._cleanup_shared_recipes()
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'download_url': url_path,
|
||||||
|
'filename': f"recipe_{recipe.get('title', '').replace(' ', '_').lower()}{ext}"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sharing recipe: {e}", exc_info=True)
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
async def download_shared_recipe(self, request: web.Request) -> web.Response:
|
||||||
|
"""Serve a processed recipe image for download"""
|
||||||
|
try:
|
||||||
|
recipe_id = request.match_info['recipe_id']
|
||||||
|
|
||||||
|
# Check if we have this shared recipe
|
||||||
|
if not hasattr(self, '_shared_recipes') or recipe_id not in self._shared_recipes:
|
||||||
|
return web.json_response({"error": "Shared recipe not found or expired"}, status=404)
|
||||||
|
|
||||||
|
shared_info = self._shared_recipes[recipe_id]
|
||||||
|
file_path = shared_info['path']
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return web.json_response({"error": "Shared recipe file not found"}, status=404)
|
||||||
|
|
||||||
|
# Get recipe to determine filename
|
||||||
|
cache = await self.recipe_scanner.get_cached_data()
|
||||||
|
recipe = next((r for r in cache.raw_data if str(r.get('id', '')) == recipe_id), None)
|
||||||
|
|
||||||
|
# Set filename for download
|
||||||
|
filename = f"recipe_{recipe.get('title', '').replace(' ', '_').lower() if recipe else recipe_id}"
|
||||||
|
ext = os.path.splitext(file_path)[1]
|
||||||
|
download_filename = f"{filename}{ext}"
|
||||||
|
|
||||||
|
# Serve the file
|
||||||
|
return web.FileResponse(
|
||||||
|
file_path,
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': f'attachment; filename="{download_filename}"'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error downloading shared recipe: {e}", exc_info=True)
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
def _cleanup_shared_recipes(self):
|
||||||
|
"""Clean up expired shared recipes"""
|
||||||
|
if not hasattr(self, '_shared_recipes'):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
expired_ids = [rid for rid, info in self._shared_recipes.items()
|
||||||
|
if current_time > info.get('expires', 0)]
|
||||||
|
|
||||||
|
for rid in expired_ids:
|
||||||
|
try:
|
||||||
|
# Delete the temporary file
|
||||||
|
file_path = self._shared_recipes[rid]['path']
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.unlink(file_path)
|
||||||
|
|
||||||
|
# Remove from dictionary
|
||||||
|
del self._shared_recipes[rid]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error cleaning up shared recipe {rid}: {e}")
|
||||||
@@ -42,11 +42,9 @@ class ExifUtils:
|
|||||||
if 'Exif' not in exif_dict:
|
if 'Exif' not in exif_dict:
|
||||||
exif_dict['Exif'] = {}
|
exif_dict['Exif'] = {}
|
||||||
|
|
||||||
# Update the UserComment field
|
# Update the UserComment field - use UNICODE format
|
||||||
if isinstance(user_comment, str):
|
unicode_bytes = user_comment.encode('utf-16be')
|
||||||
user_comment_bytes = user_comment.encode('utf-8')
|
user_comment_bytes = b'UNICODE\0' + unicode_bytes
|
||||||
else:
|
|
||||||
user_comment_bytes = user_comment
|
|
||||||
|
|
||||||
exif_dict['Exif'][piexif.ExifIFD.UserComment] = user_comment_bytes
|
exif_dict['Exif'][piexif.ExifIFD.UserComment] = user_comment_bytes
|
||||||
|
|
||||||
@@ -122,7 +120,7 @@ class ExifUtils:
|
|||||||
"""Extract recipe metadata section from UserComment if it exists"""
|
"""Extract recipe metadata section from UserComment if it exists"""
|
||||||
try:
|
try:
|
||||||
# Look for recipe metadata section
|
# Look for recipe metadata section
|
||||||
recipe_match = re.search(r'recipe metadata: (\{.*\})', user_comment, re.IGNORECASE | re.DOTALL)
|
recipe_match = re.search(r'Recipe metadata: (\{.*\})', user_comment, re.IGNORECASE | re.DOTALL)
|
||||||
if not recipe_match:
|
if not recipe_match:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -131,3 +129,36 @@ class ExifUtils:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error extracting recipe metadata: {e}")
|
logger.error(f"Error extracting recipe metadata: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def append_recipe_metadata(image_path: str, recipe_data: Dict) -> str:
|
||||||
|
"""Append recipe metadata to image EXIF data and return the path to the modified image"""
|
||||||
|
try:
|
||||||
|
# Extract existing user comment
|
||||||
|
existing_comment = ExifUtils.extract_user_comment(image_path) or ""
|
||||||
|
|
||||||
|
# Prepare recipe metadata to append
|
||||||
|
recipe_metadata = {
|
||||||
|
"title": recipe_data.get("title", ""),
|
||||||
|
"base_model": recipe_data.get("base_model", ""),
|
||||||
|
"loras": recipe_data.get("loras", []),
|
||||||
|
"gen_params": recipe_data.get("gen_params", {}),
|
||||||
|
"tags": recipe_data.get("tags", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert to JSON string
|
||||||
|
recipe_json = json.dumps(recipe_metadata, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Append to existing comment
|
||||||
|
if existing_comment and not existing_comment.endswith("\n"):
|
||||||
|
existing_comment += "\n"
|
||||||
|
|
||||||
|
new_comment = existing_comment + "Recipe metadata: " + recipe_json
|
||||||
|
|
||||||
|
# Update the image with new comment
|
||||||
|
ExifUtils.update_user_comment(image_path, new_comment)
|
||||||
|
|
||||||
|
return image_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error appending recipe metadata: {e}")
|
||||||
|
return image_path # Return original path on error
|
||||||
@@ -234,29 +234,48 @@ class RecipeCard {
|
|||||||
|
|
||||||
shareRecipe() {
|
shareRecipe() {
|
||||||
try {
|
try {
|
||||||
// Get the image URL
|
// Get recipe ID
|
||||||
const imageUrl = this.recipe.file_url ||
|
const recipeId = this.recipe.id;
|
||||||
(this.recipe.file_path ? `/loras_static/root1/preview/${this.recipe.file_path.split('/').pop()}` :
|
if (!recipeId) {
|
||||||
'/loras_static/images/no-preview.png');
|
showToast('Cannot share recipe: Missing recipe ID', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a temporary anchor element
|
// Show loading toast
|
||||||
|
showToast('Preparing recipe for sharing...', 'info');
|
||||||
|
|
||||||
|
// Call the API to process the image with metadata
|
||||||
|
fetch(`/api/recipe/${recipeId}/share`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to prepare recipe for sharing');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary anchor element for download
|
||||||
const downloadLink = document.createElement('a');
|
const downloadLink = document.createElement('a');
|
||||||
downloadLink.href = imageUrl;
|
downloadLink.href = data.download_url;
|
||||||
|
downloadLink.download = data.filename;
|
||||||
// Set the download attribute with the recipe title as filename
|
|
||||||
const fileExtension = imageUrl.split('.').pop();
|
|
||||||
const safeFileName = this.recipe.title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
||||||
downloadLink.download = `recipe_${safeFileName}.${fileExtension}`;
|
|
||||||
|
|
||||||
// Append to body, click and remove
|
// Append to body, click and remove
|
||||||
document.body.appendChild(downloadLink);
|
document.body.appendChild(downloadLink);
|
||||||
downloadLink.click();
|
downloadLink.click();
|
||||||
document.body.removeChild(downloadLink);
|
document.body.removeChild(downloadLink);
|
||||||
|
|
||||||
showToast('Recipe image download started', 'success');
|
showToast('Recipe download started', 'success');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error sharing recipe:', error);
|
||||||
|
showToast('Error sharing recipe: ' + error.message, 'error');
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sharing recipe:', error);
|
console.error('Error sharing recipe:', error);
|
||||||
showToast('Error downloading recipe image', 'error');
|
showToast('Error preparing recipe for sharing', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user