mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-07-13 20:21:16 -03:00
fix(preview): gracefully handle deleted preview files - image fallback, cache cleanup, quieter logs
- Add onerror handler on <img> previews to fallback to no-preview.png - Fire async cache cleanup when preview file returns 404 - Add ModelCache.clear_preview_by_path() for safe stale-url removal - Downgrade /api/lm/previews 404 log from warning to debug
This commit is contained in:
@@ -41,7 +41,12 @@ async def api_json_error(
|
||||
if exc.status < 400:
|
||||
raise
|
||||
|
||||
logger.warning(
|
||||
# Preview 404 is routine (file deleted from disk) — not worth a warning.
|
||||
logger_method = logger.warning
|
||||
if request.path.startswith("/api/lm/previews") and exc.status == 404:
|
||||
logger_method = logger.debug
|
||||
|
||||
logger_method(
|
||||
"API %s %s returned HTTP %d: %s",
|
||||
request.method,
|
||||
request.path,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
@@ -53,6 +54,7 @@ class PreviewHandler:
|
||||
|
||||
if not resolved.is_file():
|
||||
logger.debug("Preview file not found at %s", str(resolved))
|
||||
asyncio.create_task(self._cleanup_stale_preview_url(normalized))
|
||||
raise web.HTTPNotFound(text="Preview file not found")
|
||||
|
||||
# aiohttp's FileResponse handles range requests, content headers, and
|
||||
@@ -69,6 +71,35 @@ class PreviewHandler:
|
||||
resp.headers["Cache-Control"] = "public, max-age=86400"
|
||||
return resp
|
||||
|
||||
async def _cleanup_stale_preview_url(self, normalized_preview_path: str) -> None:
|
||||
"""Fire-and-forget: clear stale preview_url from all model caches.
|
||||
|
||||
When a preview file is no longer on disk, remove its reference from
|
||||
every cached entry so subsequent list API responses return an empty
|
||||
``preview_url``, letting the frontend show the no-preview placeholder.
|
||||
"""
|
||||
try:
|
||||
from ...services.service_registry import ServiceRegistry
|
||||
|
||||
for service_name in ("lora_scanner", "checkpoint_scanner", "embedding_scanner"):
|
||||
scanner = ServiceRegistry.get_service_sync(service_name)
|
||||
if scanner is None or not hasattr(scanner, "_cache"):
|
||||
continue
|
||||
cache = getattr(scanner, "_cache", None)
|
||||
if cache is None or not hasattr(cache, "clear_preview_by_path"):
|
||||
continue
|
||||
cleared = await cache.clear_preview_by_path(normalized_preview_path)
|
||||
if cleared and hasattr(scanner, "_persist_current_cache"):
|
||||
await scanner._persist_current_cache()
|
||||
logger.info(
|
||||
"Cleared stale preview_url for %d %s entries (%s)",
|
||||
cleared,
|
||||
service_name,
|
||||
normalized_preview_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug("Failed to clean up stale preview_url: %s", exc)
|
||||
|
||||
async def _stream_file(
|
||||
self, request: web.Request, path: Path
|
||||
) -> web.StreamResponse:
|
||||
|
||||
@@ -337,4 +337,25 @@ class ModelCache:
|
||||
else:
|
||||
return False # Model not found
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
async def clear_preview_by_path(self, preview_file_path: str) -> int:
|
||||
"""Clear ``preview_url`` for every cached entry referencing a file path.
|
||||
|
||||
When a preview file has been deleted from disk, this removes its
|
||||
reference from all matching cache entries so the next list-API
|
||||
response returns an empty ``preview_url`` instead of a stale URL
|
||||
that produces 404s.
|
||||
|
||||
Returns the number of entries that were updated.
|
||||
"""
|
||||
normalized = preview_file_path.replace("\\", "/")
|
||||
cleared = 0
|
||||
async with self._lock:
|
||||
for item in self.raw_data:
|
||||
cached_url = item.get("preview_url", "")
|
||||
if cached_url.replace("\\", "/") == normalized:
|
||||
item["preview_url"] = ""
|
||||
item["preview_nsfw_level"] = 0
|
||||
cleared += 1
|
||||
return cleared
|
||||
@@ -358,7 +358,7 @@ class RecipeCard {
|
||||
<div class="delete-preview">
|
||||
${isVideo ?
|
||||
`<video src="${previewUrl}" controls muted loop playsinline style="max-width: 100%;"></video>` :
|
||||
`<img src="${previewUrl}" alt="${this.recipe.title}">`
|
||||
`<img src="${previewUrl}" alt="${this.recipe.title}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`
|
||||
}
|
||||
</div>
|
||||
<div class="delete-info">
|
||||
|
||||
@@ -757,7 +757,7 @@ class RecipeModal {
|
||||
`<video class="thumbnail-video" autoplay loop muted playsinline>
|
||||
<source src="${lora.preview_url}" type="video/mp4">
|
||||
</video>` :
|
||||
`<img src="${lora.preview_url || '/loras_static/images/no-preview.png'}" alt="LoRA preview">`;
|
||||
`<img src="${lora.preview_url || '/loras_static/images/no-preview.png'}" alt="LoRA preview" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`;
|
||||
|
||||
let loraItemClass = 'recipe-lora-item';
|
||||
if (existsLocally) {
|
||||
@@ -1606,7 +1606,7 @@ class RecipeModal {
|
||||
<video class="thumbnail-video" autoplay loop muted playsinline>
|
||||
<source src="${previewUrl}" type="video/mp4">
|
||||
</video>
|
||||
` : `<img src="${previewUrl}" alt="Checkpoint preview">`;
|
||||
` : `<img src="${previewUrl}" alt="Checkpoint preview" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`;
|
||||
|
||||
const badge = existsLocally ? `
|
||||
<div class="local-badge">
|
||||
|
||||
@@ -643,7 +643,7 @@ export function createModelCard(model, modelType) {
|
||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||
${isVideo ?
|
||||
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`
|
||||
}
|
||||
<div class="card-header">
|
||||
${shouldBlur ?
|
||||
|
||||
@@ -432,7 +432,7 @@ function renderMediaMarkup(version) {
|
||||
|
||||
return `
|
||||
<div class="version-media">
|
||||
<img src="${escapeHtml(version.previewUrl)}" alt="${escapeHtml(version.name || 'preview')}">
|
||||
<img src="${escapeHtml(version.previewUrl)}" alt="${escapeHtml(version.name || 'preview')}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user