From ae7bfdb5175e94d0b606883f98fd3324b5511170 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Thu, 16 Apr 2026 18:25:16 +0800 Subject: [PATCH] fix(download): normalize civitai.red download URLs (#898) --- py/services/download_manager.py | 11 +++++++--- py/utils/civitai_utils.py | 18 +++++++++++++++ .../test_download_manager_concurrent.py | 4 ++-- tests/utils/test_civitai_utils.py | 22 +++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/py/services/download_manager.py b/py/services/download_manager.py index 6ec5edd0..2fe07cff 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -16,7 +16,7 @@ from ..utils.constants import ( SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS, VALID_LORA_TYPES, ) -from ..utils.civitai_utils import rewrite_preview_url +from ..utils.civitai_utils import normalize_civitai_download_url, rewrite_preview_url from ..utils.preview_selection import resolve_mature_threshold, select_preview_media from ..utils.utils import sanitize_folder_name from ..utils.exif_utils import ExifUtils @@ -644,7 +644,9 @@ class DownloadManager: if mirrors: for mirror in mirrors: if mirror.get("deletedAt") is None and mirror.get("url"): - download_urls.append(mirror["url"]) + download_urls.append( + normalize_civitai_download_url(mirror["url"]) + ) # When source is 'civarchive', prioritize non-Civitai URLs # This avoids failed downloads from deleted Civitai models @@ -663,7 +665,9 @@ class DownloadManager: else: download_url = file_info.get("downloadUrl") if download_url: - download_urls.append(download_url) + download_urls.append( + normalize_civitai_download_url(download_url) + ) if not download_urls: return {"success": False, "error": "No mirror URL found"} @@ -1138,6 +1142,7 @@ class DownloadManager: pause_control.update_stall_timeout(downloader.stall_timeout) last_error = None for download_url in download_urls: + download_url = normalize_civitai_download_url(download_url) use_auth = download_url.startswith(CIVITAI_DOWNLOAD_URL_PREFIXES) download_kwargs = { "progress_callback": lambda progress, snapshot=None: ( diff --git a/py/utils/civitai_utils.py b/py/utils/civitai_utils.py index 07fc0057..7c2c9d11 100644 --- a/py/utils/civitai_utils.py +++ b/py/utils/civitai_utils.py @@ -119,6 +119,24 @@ def extract_civitai_image_id(url: str | None) -> str | None: return path_match.group(1) +def normalize_civitai_download_url(url: str | None) -> str | None: + """Rewrite Civitai download URLs to the canonical authenticated host.""" + + if not url: + return url + + try: + parsed = urlparse(url) + except ValueError: + return url + + hostname = parsed.hostname.lower() if parsed.hostname else None + if hostname != "civitai.red" or not parsed.path.startswith("/api/download/"): + return url + + return urlunparse(parsed._replace(netloc="civitai.com")) + + def extract_civitai_page_host(url: str | None) -> str | None: """Extract the supported Civitai page host from a URL.""" diff --git a/tests/services/test_download_manager_concurrent.py b/tests/services/test_download_manager_concurrent.py index e6219663..2c520ccc 100644 --- a/tests/services/test_download_manager_concurrent.py +++ b/tests/services/test_download_manager_concurrent.py @@ -369,8 +369,8 @@ async def test_execute_download_uses_auth_for_red_civitai_downloads(monkeypatch, ) assert result == {"success": True} - assert recorded_use_auth == [("https://civitai.red/api/download/models/119514", True)] - assert "https://civitai.red/api/download/".startswith(CIVITAI_DOWNLOAD_URL_PREFIXES) + assert recorded_use_auth == [("https://civitai.com/api/download/models/119514", True)] + assert "https://civitai.com/api/download/".startswith(CIVITAI_DOWNLOAD_URL_PREFIXES) @pytest.mark.asyncio diff --git a/tests/utils/test_civitai_utils.py b/tests/utils/test_civitai_utils.py index 7d2836fd..7f5a74e9 100644 --- a/tests/utils/test_civitai_utils.py +++ b/tests/utils/test_civitai_utils.py @@ -3,6 +3,7 @@ from py.utils.civitai_utils import ( extract_civitai_image_id, extract_civitai_model_url_parts, is_supported_civitai_page_host, + normalize_civitai_download_url, resolve_license_info, resolve_license_payload, ) @@ -122,3 +123,24 @@ def test_extract_civitai_image_id_supports_red(): def test_extract_civitai_image_id_rejects_non_civitai_host(): assert extract_civitai_image_id("https://example.com/images/126920345") is None + + +def test_normalize_civitai_download_url_rewrites_red_to_com(): + url = "https://civitai.red/api/download/models/2786889?type=Model&format=SafeTensor" + + assert ( + normalize_civitai_download_url(url) + == "https://civitai.com/api/download/models/2786889?type=Model&format=SafeTensor" + ) + + +def test_normalize_civitai_download_url_keeps_non_download_red_urls(): + url = "https://civitai.red/models/65423/nijimecha-artstyle?modelVersionId=777" + + assert normalize_civitai_download_url(url) == url + + +def test_normalize_civitai_download_url_keeps_existing_com_urls(): + url = "https://civitai.com/api/download/models/2786889?type=Model&format=SafeTensor" + + assert normalize_civitai_download_url(url) == url