Compare commits

..

3 Commits

Author SHA1 Message Date
Will Miao
b6dd6938b0 docs: add v1.0.2 release notes, bump version to 1.0.2 2026-04-06 20:14:26 +08:00
Will Miao
727d0ef043 feat(misc): add model download status aggregation 2026-04-03 22:17:09 +08:00
Will Miao
9344d86332 test(misc): cover model existence download status 2026-04-03 22:16:09 +08:00
6 changed files with 105 additions and 4 deletions

View File

@@ -56,6 +56,13 @@ Insomnia Art Designs, megakirbs, Brennok, 2018cfh, W+K+White, wackop, Takkan, Ca
## Release Notes ## Release Notes
### v1.0.2
* **Model Download History Tracking** - LoRA Manager now keeps a history of downloaded model versions, allowing it to recognize whether a version has been downloaded before, even if it is no longer currently present in your library.
* **Skip Previously Downloaded Model Versions** - Added a new setting, `Skip previously downloaded model versions`, to help avoid downloading model versions you have already downloaded in the past.
* **LoRA Stack Combiner Trigger Words Fix** - Fixed an issue where trigger word updates from `LORA_STACK` inputs were not propagated correctly through the LoRA Stack Combiner node.
* **CivitAI Example Image Compatibility** - Improved support for CivitAI CDN subdomains so example images load more reliably.
### v1.0.1 ### v1.0.1
* **Batch Recipe Import** - Import recipes from multiple URLs or directories simultaneously with optimized concurrency. * **Batch Recipe Import** - Import recipes from multiple URLs or directories simultaneously with optimized concurrency.

View File

@@ -896,18 +896,49 @@ class ModelLibraryHandler:
model_type = None model_type = None
versions = [] versions = []
downloaded_version_ids = []
history_service = await self._get_download_history_service()
if lora_versions: if lora_versions:
model_type = "lora" model_type = "lora"
versions = self._with_downloaded_flag(lora_versions) versions = self._with_downloaded_flag(lora_versions)
downloaded_version_ids = await history_service.get_downloaded_version_ids(
model_type,
model_id,
)
elif checkpoint_versions: elif checkpoint_versions:
model_type = "checkpoint" model_type = "checkpoint"
versions = self._with_downloaded_flag(checkpoint_versions) versions = self._with_downloaded_flag(checkpoint_versions)
downloaded_version_ids = await history_service.get_downloaded_version_ids(
model_type,
model_id,
)
elif embedding_versions: elif embedding_versions:
model_type = "embedding" model_type = "embedding"
versions = self._with_downloaded_flag(embedding_versions) versions = self._with_downloaded_flag(embedding_versions)
downloaded_version_ids = await history_service.get_downloaded_version_ids(
model_type,
model_id,
)
else:
for candidate_type in ("lora", "checkpoint", "embedding"):
candidate_downloaded_version_ids = (
await history_service.get_downloaded_version_ids(
candidate_type,
model_id,
)
)
if candidate_downloaded_version_ids:
model_type = candidate_type
downloaded_version_ids = candidate_downloaded_version_ids
break
return web.json_response( return web.json_response(
{"success": True, "modelType": model_type, "versions": versions} {
"success": True,
"modelType": model_type,
"versions": versions,
"downloadedVersionIds": downloaded_version_ids,
}
) )
except Exception as exc: # pragma: no cover - defensive logging except Exception as exc: # pragma: no cover - defensive logging
logger.error("Failed to check model existence: %s", exc, exc_info=True) logger.error("Failed to check model existence: %s", exc, exc_info=True)
@@ -962,7 +993,10 @@ class ModelLibraryHandler:
self, request: web.Request self, request: web.Request
) -> web.Response: ) -> web.Response:
try: try:
data = await request.json() if request.method == "GET":
data = request.query
else:
data = await request.json()
model_type, _ = await self._get_scanner_for_type(data.get("modelType")) model_type, _ = await self._get_scanner_for_type(data.get("modelType"))
if not model_type: if not model_type:
return web.json_response( return web.json_response(
@@ -979,6 +1013,13 @@ class ModelLibraryHandler:
) )
downloaded = data.get("downloaded") downloaded = data.get("downloaded")
if isinstance(downloaded, str):
normalized_downloaded = downloaded.strip().lower()
if normalized_downloaded in {"true", "1"}:
downloaded = True
elif normalized_downloaded in {"false", "0"}:
downloaded = False
if not isinstance(downloaded, bool): if not isinstance(downloaded, bool):
return web.json_response( return web.json_response(
{"success": False, "error": "Parameter downloaded must be a boolean"}, {"success": False, "error": "Parameter downloaded must be a boolean"},

View File

@@ -47,6 +47,11 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
"/api/lm/model-version-download-status", "/api/lm/model-version-download-status",
"set_model_version_download_status", "set_model_version_download_status",
), ),
RouteDefinition(
"GET",
"/api/lm/set-model-version-download-status",
"set_model_version_download_status",
),
RouteDefinition("GET", "/api/lm/civitai/user-models", "get_civitai_user_models"), RouteDefinition("GET", "/api/lm/civitai/user-models", "get_civitai_user_models"),
RouteDefinition( RouteDefinition(
"POST", "/api/lm/download-metadata-archive", "download_metadata_archive" "POST", "/api/lm/download-metadata-archive", "download_metadata_archive"

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-lora-manager" name = "comfyui-lora-manager"
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!" description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
version = "1.0.1" version = "1.0.2"
license = {file = "LICENSE"} license = {file = "LICENSE"}
dependencies = [ dependencies = [
"aiohttp", "aiohttp",

View File

@@ -1,6 +1,8 @@
# serializer version: 1 # serializer version: 1
# name: TestModelLibraryHandlerSnapshots.test_check_model_exists_empty_response # name: TestModelLibraryHandlerSnapshots.test_check_model_exists_empty_response
dict({ dict({
'downloadedVersionIds': list([
]),
'modelType': None, 'modelType': None,
'success': True, 'success': True,
'versions': list([ 'versions': list([

View File

@@ -23,9 +23,10 @@ from py.routes.misc_routes import MiscRoutes
class FakeRequest: class FakeRequest:
def __init__(self, *, json_data=None, query=None): def __init__(self, *, json_data=None, query=None, method="POST"):
self._json_data = json_data or {} self._json_data = json_data or {}
self.query = query or {} self.query = query or {}
self.method = method
async def json(self): async def json(self):
return self._json_data return self._json_data
@@ -869,6 +870,32 @@ async def test_check_model_exists_returns_local_versions():
assert lora_scanner.version_calls == [5] assert lora_scanner.version_calls == [5]
@pytest.mark.asyncio
async def test_check_model_exists_model_id_only_does_not_call_metadata_provider():
async def metadata_provider_factory():
raise AssertionError("metadata provider should not be called for modelId-only checks")
handler = ModelLibraryHandler(
ServiceRegistryAdapter(
get_lora_scanner=fake_scanner_factory,
get_checkpoint_scanner=fake_scanner_factory,
get_embedding_scanner=fake_scanner_factory,
get_downloaded_version_history_service=fake_download_history_service_factory,
),
metadata_provider_factory=metadata_provider_factory,
)
response = await handler.check_model_exists(FakeRequest(query={"modelId": "5"}))
payload = json.loads(response.text)
assert payload == {
"success": True,
"modelType": None,
"versions": [],
"downloadedVersionIds": [],
}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_check_model_exists_returns_download_history_when_file_missing(): async def test_check_model_exists_returns_download_history_when_file_missing():
history_service = FakeDownloadHistoryService({"checkpoint": {999}}) history_service = FakeDownloadHistoryService({"checkpoint": {999}})
@@ -949,6 +976,25 @@ async def test_model_version_download_status_endpoints():
("checkpoint", 456, 78, "manual", "/tmp/model.safetensors") ("checkpoint", 456, 78, "manual", "/tmp/model.safetensors")
] ]
set_get_response = await handler.set_model_version_download_status(
FakeRequest(
method="GET",
query={
"modelType": "embedding",
"modelVersionId": "789",
"modelId": "12",
"downloaded": "false",
},
)
)
set_get_payload = json.loads(set_get_response.text)
assert set_get_payload == {
"success": True,
"modelType": "embedding",
"modelVersionId": 789,
"hasBeenDownloaded": False,
}
def test_create_handler_set_uses_provided_dependencies(): def test_create_handler_set_uses_provided_dependencies():
recorded_handlers: list[dict] = [] recorded_handlers: list[dict] = []