fix(example-images): stop permanently blacklisting models on transient failures

The example-images download loop marked a model as failed in
.download_progress.json whenever its metadata had no civitai images or
processing raised any exception. Since failed_models is a permanent skip
list for non-force runs (and excluded from the pending pre-check), models
whose metadata simply had not been fetched yet — or that hit a transient
error — were never retried. Users had to delete the progress file to
unstick them, and the blacklist re-grew afterwards.

- Distinguish 'metadata not fetched yet' (no civitai payload) from
  confirmed absences (civitai entry present but image-less, or the model
  is known to be gone from CivitAI via civitai_deleted/from_civitai).
  Only confirmed absences are marked failed; the rest stay pending.
  Retrying pending models only re-reads local metadata, so no extra
  provider API calls are introduced.
- Transient processing errors no longer add the model to failed_models.
- Add unblock_failed_example_image_models() to remove hashes from the
  persisted failed list (library-scoped and legacy locations) and from
  the live download manager's in-memory state.
- Hook it into MetadataSyncService.update_model_metadata so a successful
  metadata fetch/relink immediately re-enables example image downloads
  for that model — no TTL polling needed.
This commit is contained in:
Will Miao
2026-10-10 08:09:08 +08:00
parent af90fe873d
commit b31e831c57
4 changed files with 420 additions and 6 deletions
@@ -300,6 +300,57 @@ async def test_update_model_metadata_raises_when_sidecar_write_fails():
)
@pytest.mark.asyncio
async def test_update_model_metadata_unblocks_example_image_failures(monkeypatch):
"""Persisting fresh provider metadata re-enables example image downloads."""
helpers = build_service()
from py.utils import example_images_download_manager as download_module
calls: list[list[str]] = []
def fake_unblock(hashes, library_name=None):
calls.append(list(hashes))
return len(hashes)
monkeypatch.setattr(
download_module, "unblock_failed_example_image_models", fake_unblock
)
result = await helpers.service.update_model_metadata(
"path/to/model.metadata.json",
{"sha256": "A" * 64, "model_name": "Local"},
{"source": "api", "model": {"name": "Remote"}, "images": []},
helpers.default_provider,
)
assert result["model_name"] == "Remote"
assert calls == [["a" * 64]]
@pytest.mark.asyncio
async def test_update_model_metadata_without_hash_skips_unblock(monkeypatch):
helpers = build_service()
from py.utils import example_images_download_manager as download_module
def fail_unblock(hashes, library_name=None): # pragma: no cover - must not run
raise AssertionError("unblock must not run without a sha256")
monkeypatch.setattr(
download_module, "unblock_failed_example_image_models", fail_unblock
)
result = await helpers.service.update_model_metadata(
"path/to/model.metadata.json",
{"model_name": "Local"},
{"source": "api", "model": {"name": "Remote"}, "images": []},
helpers.default_provider,
)
assert result["model_name"] == "Remote"
@pytest.mark.asyncio
async def test_fetch_and_update_model_reports_sidecar_write_failure(tmp_path):
"""Fetch fails the item (and leaves the cache untouched) when the sidecar