mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-09 11:02:12 -03:00
fix(civitai): anchor model-page URL parsing and improve missing-model diagnostics (#1140)
This commit is contained in:
@@ -329,8 +329,19 @@ class CivitaiClient:
|
|||||||
"name": result.get("name", ""),
|
"name": result.get("name", ""),
|
||||||
}
|
}
|
||||||
message = self._extract_error_message(result)
|
message = self._extract_error_message(result)
|
||||||
if message and "not found" in message.lower():
|
if message and (
|
||||||
raise ResourceNotFoundError(f"Resource not found for model {model_id}")
|
"not found" in message.lower()
|
||||||
|
or message.lower().startswith("no model with id ")
|
||||||
|
):
|
||||||
|
raise ResourceNotFoundError(
|
||||||
|
f"Civitai model {model_id} was not found. Check the original "
|
||||||
|
"Civitai link: use the model ID from /models/<id>, not a "
|
||||||
|
"version ID from ?modelVersionId=<id> or "
|
||||||
|
"/api/download/models/<id>. If the model ID is correct, "
|
||||||
|
"the model may have been removed or made inaccessible. "
|
||||||
|
"If the page opens only when logged in, check the Civitai "
|
||||||
|
"API key in LoRA Manager settings."
|
||||||
|
)
|
||||||
if is_expected_offline_error(message):
|
if is_expected_offline_error(message):
|
||||||
logger.info("Civitai request skipped: %s", OFFLINE_FRIENDLY_MESSAGE)
|
logger.info("Civitai request skipped: %s", OFFLINE_FRIENDLY_MESSAGE)
|
||||||
return None
|
return None
|
||||||
@@ -347,7 +358,7 @@ class CivitaiClient:
|
|||||||
except RateLimitError:
|
except RateLimitError:
|
||||||
raise
|
raise
|
||||||
except ResourceNotFoundError as exc:
|
except ResourceNotFoundError as exc:
|
||||||
logger.info("Model %s is no longer available on Civitai: %s", model_id, exc)
|
logger.info("Civitai model lookup failed: %s", exc)
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error fetching model versions: %s", e, exc_info=True)
|
logger.error("Error fetching model versions: %s", e, exc_info=True)
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ def extract_civitai_model_url_parts(
|
|||||||
if parsed is None:
|
if parsed is None:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
path_match = re.search(r"/models/(\d+)", parsed.path)
|
# Download URLs contain version IDs, not model IDs.
|
||||||
|
path_match = re.match(r"^/models/(\d+)(?:/|$)", parsed.path)
|
||||||
if not path_match:
|
if not path_match:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|||||||
@@ -228,12 +228,19 @@ export function extractCivitaiModelUrlParts(url) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||||
|
return { modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
if (!isSupportedCivitaiPageHost(parsedUrl.hostname)) {
|
if (!isSupportedCivitaiPageHost(parsedUrl.hostname)) {
|
||||||
return { modelId: null, modelVersionId: null };
|
return { modelId: null, modelVersionId: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathMatch = parsedUrl.pathname.match(/\/models\/(\d+)/);
|
// Download URLs contain version IDs, not model IDs.
|
||||||
const modelId = pathMatch ? pathMatch[1] : null;
|
const pathMatch = parsedUrl.pathname.match(/^\/models\/(\d+)(?:\/|$)/);
|
||||||
|
if (!pathMatch) {
|
||||||
|
return { modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
|
const modelId = pathMatch[1];
|
||||||
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
||||||
|
|
||||||
return { modelId, modelVersionId };
|
return { modelId, modelVersionId };
|
||||||
@@ -269,8 +276,11 @@ export function classifyModelRelinkUrl(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hostname = parsedUrl.hostname.toLowerCase().replace(/^www\./, '');
|
const hostname = parsedUrl.hostname.toLowerCase().replace(/^www\./, '');
|
||||||
const pathMatch = parsedUrl.pathname.match(/\/models\/(\d+)/);
|
const pathMatch = parsedUrl.pathname.match(/^\/models\/(\d+)(?:\/|$)/);
|
||||||
const modelId = pathMatch ? pathMatch[1] : null;
|
if (!pathMatch) {
|
||||||
|
return { source: null, modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
|
const modelId = pathMatch[1];
|
||||||
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
||||||
|
|
||||||
if (SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname)) {
|
if (SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname)) {
|
||||||
|
|||||||
@@ -352,6 +352,12 @@ describe('civitaiUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('extractCivitaiModelUrlParts', () => {
|
describe('extractCivitaiModelUrlParts', () => {
|
||||||
|
it.each(['civitai.com', 'civitai.red'])('rejects download and API URLs on %s', (host) => {
|
||||||
|
for (const path of ['/api/download/models/3390681', '/api/v1/models/3390681', '/models/3390681invalid']) {
|
||||||
|
expect(extractCivitaiModelUrlParts(`https://${host}${path}?modelVersionId=3390681`))
|
||||||
|
.toEqual({ modelId: null, modelVersionId: null });
|
||||||
|
}
|
||||||
|
});
|
||||||
it('extracts model and version ids from civitai.red model URLs', () => {
|
it('extracts model and version ids from civitai.red model URLs', () => {
|
||||||
expect(
|
expect(
|
||||||
extractCivitaiModelUrlParts('https://civitai.red/models/65423/name?modelVersionId=98765')
|
extractCivitaiModelUrlParts('https://civitai.red/models/65423/name?modelVersionId=98765')
|
||||||
@@ -376,6 +382,12 @@ describe('civitaiUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('classifyModelRelinkUrl', () => {
|
describe('classifyModelRelinkUrl', () => {
|
||||||
|
it.each(['civitai.com', 'civitai.red', 'civarchive.com'])('rejects non-model pages on %s', (host) => {
|
||||||
|
for (const path of ['/api/download/models/3390681', '/api/v1/models/3390681', '/models/3390681invalid']) {
|
||||||
|
expect(classifyModelRelinkUrl(`https://${host}${path}`))
|
||||||
|
.toEqual({ source: null, modelId: null, modelVersionId: null });
|
||||||
|
}
|
||||||
|
});
|
||||||
it('classifies civitai.com model URLs', () => {
|
it('classifies civitai.com model URLs', () => {
|
||||||
expect(
|
expect(
|
||||||
classifyModelRelinkUrl('https://civitai.com/models/649516/name?modelVersionId=726676')
|
classifyModelRelinkUrl('https://civitai.com/models/649516/name?modelVersionId=726676')
|
||||||
|
|||||||
@@ -199,6 +199,26 @@ async def test_get_model_versions_raises_on_not_found(monkeypatch, downloader):
|
|||||||
await client.get_model_versions("missing")
|
await client.get_model_versions("missing")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("payload", ["Resource not found", {"error": "No model with id 3390681"}])
|
||||||
|
async def test_get_model_versions_not_found_explains_id_and_access(payload, downloader, caplog):
|
||||||
|
downloader.make_request = AsyncMock(return_value=(False, payload))
|
||||||
|
client = await CivitaiClient.get_instance()
|
||||||
|
|
||||||
|
with caplog.at_level("INFO"), pytest.raises(ResourceNotFoundError) as exc_info:
|
||||||
|
await client.get_model_versions("3390681")
|
||||||
|
|
||||||
|
message = str(exc_info.value)
|
||||||
|
assert "3390681" in message
|
||||||
|
assert "/models/<id>" in message
|
||||||
|
assert "modelVersionId" in message
|
||||||
|
assert "/api/download/models/<id>" in message
|
||||||
|
assert "API key" in message
|
||||||
|
assert "no longer available" not in caplog.text
|
||||||
|
downloader.make_request.assert_awaited_once_with(
|
||||||
|
"GET", f"{client.base_url}/models/3390681", use_auth=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_model_versions_raises_on_nested_not_found(monkeypatch, downloader):
|
async def test_get_model_versions_raises_on_nested_not_found(monkeypatch, downloader):
|
||||||
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
||||||
return False, {"error": {"message": "Resource not found"}}
|
return False, {"error": {"message": "Resource not found"}}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
from py.utils.civitai_utils import (
|
from py.utils.civitai_utils import (
|
||||||
build_license_flags,
|
build_license_flags,
|
||||||
extract_civitai_image_id,
|
extract_civitai_image_id,
|
||||||
@@ -8,6 +10,13 @@ from py.utils.civitai_utils import (
|
|||||||
resolve_license_payload,
|
resolve_license_payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("host", ["civitai.com", "civitai.red"])
|
||||||
|
@pytest.mark.parametrize("path", ["/api/download/models/3390681", "/api/v1/models/3390681", "/models/3390681invalid"])
|
||||||
|
def test_extract_civitai_model_url_parts_rejects_non_model_pages(host, path):
|
||||||
|
assert extract_civitai_model_url_parts(
|
||||||
|
f"https://{host}{path}?modelVersionId=3390681"
|
||||||
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_license_payload_defaults():
|
def test_resolve_license_payload_defaults():
|
||||||
payload, flags = resolve_license_info({})
|
payload, flags = resolve_license_info({})
|
||||||
|
|||||||
Reference in New Issue
Block a user