mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-08 01:16:43 -03:00
Compare commits
2 Commits
v1.0.1
...
30db8c3d1d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30db8c3d1d | ||
|
|
05636712f0 |
13
README.md
13
README.md
@@ -59,31 +59,18 @@ Insomnia Art Designs, megakirbs, Brennok, 2018cfh, W+K+White, wackop, Takkan, Ca
|
|||||||
### 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.
|
||||||
|
|
||||||
* **Bulk Download Missing LoRAs** - New bulk action for recipes: select multiple recipes and download all missing LoRAs for the selected recipes in one operation.
|
* **Bulk Download Missing LoRAs** - New bulk action for recipes: select multiple recipes and download all missing LoRAs for the selected recipes in one operation.
|
||||||
|
|
||||||
* **Import-Only Recipe Option** - Save recipe metadata without downloading missing LoRAs, allowing you to save interesting recipes for later and download dependencies when needed.
|
* **Import-Only Recipe Option** - Save recipe metadata without downloading missing LoRAs, allowing you to save interesting recipes for later and download dependencies when needed.
|
||||||
|
|
||||||
* **Editable Recipe Prompts** - Edit recipe prompts directly in the recipe detail modal.
|
* **Editable Recipe Prompts** - Edit recipe prompts directly in the recipe detail modal.
|
||||||
|
|
||||||
* **Checkpoint Loader LM Node** - Behaves like ComfyUI's built-in Load Checkpoint node, with the added ability to load checkpoints from Extra Folder Paths.
|
* **Checkpoint Loader LM Node** - Behaves like ComfyUI's built-in Load Checkpoint node, with the added ability to load checkpoints from Extra Folder Paths.
|
||||||
|
|
||||||
* **UNET Loader LM Node** - Behaves like ComfyUI's built-in Load Diffusion Model node, with support for loading from Extra Folder Paths and GGUF format (requires ComfyUI-GGUF custom node).
|
* **UNET Loader LM Node** - Behaves like ComfyUI's built-in Load Diffusion Model node, with support for loading from Extra Folder Paths and GGUF format (requires ComfyUI-GGUF custom node).
|
||||||
|
|
||||||
* **LoRA Stack Combiner Node** - Merge two LoRA stacks into one. For example: use separate Randomizers for character and style LoRAs, then combine before applying.
|
* **LoRA Stack Combiner Node** - Merge two LoRA stacks into one. For example: use separate Randomizers for character and style LoRAs, then combine before applying.
|
||||||
|
|
||||||
* **LoRA Pool Regex Filtering** - Filter which LoRAs enter the pool using custom regex patterns for include/exclude rules.
|
* **LoRA Pool Regex Filtering** - Filter which LoRAs enter the pool using custom regex patterns for include/exclude rules.
|
||||||
|
|
||||||
* **Dynamic Base Model Types** - Base model types are now fetched dynamically from Civitai API, keeping them synchronized with the latest available models.
|
* **Dynamic Base Model Types** - Base model types are now fetched dynamically from Civitai API, keeping them synchronized with the latest available models.
|
||||||
|
|
||||||
* **Prompt Autocomplete Enhancements** - Tab key acceptance, configurable behavior, and improved multi-word tag matching.
|
* **Prompt Autocomplete Enhancements** - Tab key acceptance, configurable behavior, and improved multi-word tag matching.
|
||||||
|
|
||||||
* **Download Base Model Exclusions** - Exclude specific base models from download operations when you only want certain model types.
|
* **Download Base Model Exclusions** - Exclude specific base models from download operations when you only want certain model types.
|
||||||
|
|
||||||
* **Mature Blur Threshold Setting** - Configure blur levels (`PG13` / `R` / `X` / `XXX`, default `R+`) for mature content previews.
|
* **Mature Blur Threshold Setting** - Configure blur levels (`PG13` / `R` / `X` / `XXX`, default `R+`) for mature content previews.
|
||||||
|
|
||||||
* **Experimental: Nunchaku Qwen LoRA Support** - Experimental support for loading and applying LoRAs to Nunchaku quantized Qwen-Image models.
|
* **Experimental: Nunchaku Qwen LoRA Support** - Experimental support for loading and applying LoRAs to Nunchaku quantized Qwen-Image models.
|
||||||
|
|
||||||
* **Bug Fixes & UX Improvements** - Various fixes for a smoother workflow.
|
* **Bug Fixes & UX Improvements** - Various fixes for a smoother workflow.
|
||||||
|
|
||||||
### v1.0.0
|
### v1.0.0
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ from typing import Awaitable, Callable, Dict, List
|
|||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
# Use wildcard for CivitAI to support their CDN subdomains (e.g., image-b2.civitai.com)
|
||||||
|
# Security note: This is acceptable because:
|
||||||
|
# 1. CSP img-src only controls image/video loading, not script execution
|
||||||
|
# 2. All *.civitai.com subdomains are controlled by Civitai
|
||||||
|
# 3. Explicit domain list would require constant updates as Civitai adds CDN nodes
|
||||||
REMOTE_MEDIA_SOURCES = (
|
REMOTE_MEDIA_SOURCES = (
|
||||||
"https://image.civitai.com",
|
"https://*.civitai.com",
|
||||||
"https://img.genur.art",
|
"https://img.genur.art",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def relax_csp_for_remote_media(
|
async def relax_csp_for_remote_media(
|
||||||
request: web.Request, handler: Callable[[web.Request], Awaitable[web.StreamResponse]]
|
request: web.Request,
|
||||||
|
handler: Callable[[web.Request], Awaitable[web.StreamResponse]],
|
||||||
) -> web.StreamResponse:
|
) -> web.StreamResponse:
|
||||||
"""Allow LoRA Manager media previews to load from trusted remote domains.
|
"""Allow LoRA Manager media previews to load from trusted remote domains.
|
||||||
|
|
||||||
@@ -43,7 +49,9 @@ async def relax_csp_for_remote_media(
|
|||||||
directive_order.append(name)
|
directive_order.append(name)
|
||||||
directives[name] = values
|
directives[name] = values
|
||||||
|
|
||||||
def merge_sources(name: str, sources: List[str], defaults: List[str] | None = None) -> None:
|
def merge_sources(
|
||||||
|
name: str, sources: List[str], defaults: List[str] | None = None
|
||||||
|
) -> None:
|
||||||
existing = directives.get(name, list(defaults or []))
|
existing = directives.get(name, list(defaults or []))
|
||||||
|
|
||||||
for source in sources:
|
for source in sources:
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ def _normalize_commercial_values(value: Any) -> Sequence[str]:
|
|||||||
|
|
||||||
def _split_aggregate(value_str: str) -> list[str]:
|
def _split_aggregate(value_str: str) -> list[str]:
|
||||||
stripped = value_str.strip()
|
stripped = value_str.strip()
|
||||||
looks_aggregate = "," in stripped or (stripped.startswith("{") and stripped.endswith("}"))
|
looks_aggregate = "," in stripped or (
|
||||||
|
stripped.startswith("{") and stripped.endswith("}")
|
||||||
|
)
|
||||||
if not looks_aggregate:
|
if not looks_aggregate:
|
||||||
return [value_str]
|
return [value_str]
|
||||||
|
|
||||||
@@ -141,14 +143,18 @@ def build_license_flags(payload: Mapping[str, Any] | None) -> int:
|
|||||||
return flags
|
return flags
|
||||||
|
|
||||||
|
|
||||||
def resolve_license_info(model_data: Mapping[str, Any] | None) -> tuple[Dict[str, Any], int]:
|
def resolve_license_info(
|
||||||
|
model_data: Mapping[str, Any] | None,
|
||||||
|
) -> tuple[Dict[str, Any], int]:
|
||||||
"""Return normalized license payload and its encoded bitset."""
|
"""Return normalized license payload and its encoded bitset."""
|
||||||
|
|
||||||
payload = resolve_license_payload(model_data)
|
payload = resolve_license_payload(model_data)
|
||||||
return payload, build_license_flags(payload)
|
return payload, build_license_flags(payload)
|
||||||
|
|
||||||
|
|
||||||
def rewrite_preview_url(source_url: str | None, media_type: str | None = None) -> tuple[str | None, bool]:
|
def rewrite_preview_url(
|
||||||
|
source_url: str | None, media_type: str | None = None
|
||||||
|
) -> tuple[str | None, bool]:
|
||||||
"""Rewrite Civitai preview URLs to use optimized renditions.
|
"""Rewrite Civitai preview URLs to use optimized renditions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -168,7 +174,12 @@ def rewrite_preview_url(source_url: str | None, media_type: str | None = None) -
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return source_url, False
|
return source_url, False
|
||||||
|
|
||||||
if parsed.netloc.lower() != "image.civitai.com":
|
hostname = parsed.hostname
|
||||||
|
if hostname is None:
|
||||||
|
return source_url, False
|
||||||
|
|
||||||
|
hostname = hostname.lower()
|
||||||
|
if hostname == "civitai.com" or not hostname.endswith(".civitai.com"):
|
||||||
return source_url, False
|
return source_url, False
|
||||||
|
|
||||||
replacement = "/width=450,optimized=true"
|
replacement = "/width=450,optimized=true"
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export function rewriteCivitaiUrl(sourceUrl, mediaType = null, mode = Optimizati
|
|||||||
try {
|
try {
|
||||||
const url = new URL(sourceUrl);
|
const url = new URL(sourceUrl);
|
||||||
|
|
||||||
// Check if it's a CivitAI image domain
|
// Check if it's a CivitAI CDN domain (supports all subdomains like image-b2.civitai.com)
|
||||||
if (url.hostname.toLowerCase() !== 'image.civitai.com') {
|
const hostname = url.hostname.toLowerCase();
|
||||||
|
if (hostname === 'civitai.com' || !hostname.endsWith('.civitai.com')) {
|
||||||
return [sourceUrl, false];
|
return [sourceUrl, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +113,8 @@ export function isCivitaiUrl(url) {
|
|||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
return parsed.hostname.toLowerCase() === 'image.civitai.com';
|
const hostname = parsed.hostname.toLowerCase();
|
||||||
|
return hostname.endsWith('.civitai.com') && hostname !== 'civitai.com';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,37 @@ describe('civitaiUtils', () => {
|
|||||||
expect(wasRewritten).toBe(false);
|
expect(wasRewritten).toBe(false);
|
||||||
expect(rewritten).toBe('not-a-valid-url');
|
expect(rewritten).toBe('not-a-valid-url');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should rewrite URLs from CivitAI CDN subdomains', () => {
|
||||||
|
const originalUrl = 'https://image-b2.civitai.com/file/civitai-media-cache/original=true/sample.png';
|
||||||
|
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||||
|
|
||||||
|
expect(wasRewritten).toBe(true);
|
||||||
|
expect(rewritten).toBe('https://image-b2.civitai.com/file/civitai-media-cache/width=450,optimized=true/sample.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URLs with explicit port numbers', () => {
|
||||||
|
const originalUrl = 'https://image.civitai.com:443/checkpoints/original=true/test.png';
|
||||||
|
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||||
|
|
||||||
|
expect(wasRewritten).toBe(true);
|
||||||
|
// JavaScript URL.toString() removes default HTTPS port (443)
|
||||||
|
expect(rewritten).toBe('https://image.civitai.com/checkpoints/width=450,optimized=true/test.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case-insensitive hostnames', () => {
|
||||||
|
const testCases = [
|
||||||
|
'https://IMAGE.CIVITAI.COM/original=true/test.png',
|
||||||
|
'https://Image.Civitai.Com/original=true/test.png',
|
||||||
|
'https://image-b2.CIVITAI.com/original=true/test.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const url of testCases) {
|
||||||
|
const [rewritten, wasRewritten] = rewriteCivitaiUrl(url, 'image', OptimizationMode.THUMBNAIL);
|
||||||
|
expect(wasRewritten).toBe(true);
|
||||||
|
expect(rewritten).toContain('width=450,optimized=true');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getOptimizedUrl', () => {
|
describe('getOptimizedUrl', () => {
|
||||||
@@ -157,6 +188,23 @@ describe('civitaiUtils', () => {
|
|||||||
expect(isCivitaiUrl('https://image.civitai.com/')).toBe(true);
|
expect(isCivitaiUrl('https://image.civitai.com/')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true for CivitAI CDN subdomains', () => {
|
||||||
|
expect(isCivitaiUrl('https://image-b2.civitai.com/file/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://image-b3.civitai.com/test.jpg')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://cdn.civitai.com/test.png')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for CivitAI URLs with explicit ports', () => {
|
||||||
|
expect(isCivitaiUrl('https://image.civitai.com:443/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://image-b2.civitai.com:443/file/test.jpg')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case-insensitive hostnames', () => {
|
||||||
|
expect(isCivitaiUrl('https://IMAGE.CIVITAI.COM/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://Image.Civitai.Com/test.png')).toBe(true);
|
||||||
|
expect(isCivitaiUrl('https://image-b2.CIVITAI.com/test.png')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return false for non-CivitAI URLs', () => {
|
it('should return false for non-CivitAI URLs', () => {
|
||||||
expect(isCivitaiUrl('https://example.com/image.jpg')).toBe(false);
|
expect(isCivitaiUrl('https://example.com/image.jpg')).toBe(false);
|
||||||
expect(isCivitaiUrl('https://civitai.com/image.jpg')).toBe(false);
|
expect(isCivitaiUrl('https://civitai.com/image.jpg')).toBe(false);
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import pytest
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.test_utils import make_mocked_request
|
from aiohttp.test_utils import make_mocked_request
|
||||||
|
|
||||||
from py.middleware.csp_middleware import REMOTE_MEDIA_SOURCES, relax_csp_for_remote_media
|
from py.middleware.csp_middleware import (
|
||||||
|
REMOTE_MEDIA_SOURCES,
|
||||||
|
relax_csp_for_remote_media,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_CSP = (
|
DEFAULT_CSP = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
@@ -40,7 +43,9 @@ async def _invoke_middleware(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_relax_csp_appends_remote_sources_and_preserves_existing_directives() -> None:
|
async def test_relax_csp_appends_remote_sources_and_preserves_existing_directives() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
response = await _invoke_middleware("/some-path", web.Response())
|
response = await _invoke_middleware("/some-path", web.Response())
|
||||||
header_value = response.headers.get("Content-Security-Policy")
|
header_value = response.headers.get("Content-Security-Policy")
|
||||||
assert header_value is not None
|
assert header_value is not None
|
||||||
@@ -48,16 +53,17 @@ async def test_relax_csp_appends_remote_sources_and_preserves_existing_directive
|
|||||||
directives = _parse_directives(header_value)
|
directives = _parse_directives(header_value)
|
||||||
|
|
||||||
# Existing directives remain intact
|
# Existing directives remain intact
|
||||||
assert directives["script-src"] == ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"]
|
assert directives["script-src"] == [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
"'unsafe-eval'",
|
||||||
|
"blob:",
|
||||||
|
]
|
||||||
assert directives["img-src"][:3] == ["'self'", "data:", "blob:"]
|
assert directives["img-src"][:3] == ["'self'", "data:", "blob:"]
|
||||||
|
|
||||||
# Remote media hosts are added once to the relevant directives
|
# Remote media hosts are added once to the relevant directives
|
||||||
for source in REMOTE_MEDIA_SOURCES:
|
for source in REMOTE_MEDIA_SOURCES:
|
||||||
assert source in directives["img-src"]
|
assert source in directives["img-src"]
|
||||||
|
|
||||||
assert "media-src" in directives
|
|
||||||
assert directives["media-src"][0] == "'self'"
|
|
||||||
for source in REMOTE_MEDIA_SOURCES:
|
|
||||||
assert source in directives["media-src"]
|
assert source in directives["media-src"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
144
tests/utils/test_civitai_utils_rewrite.py
Normal file
144
tests/utils/test_civitai_utils_rewrite.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
"""Tests for CivitAI URL utilities."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from py.utils.civitai_utils import rewrite_preview_url
|
||||||
|
|
||||||
|
|
||||||
|
class TestRewritePreviewUrl:
|
||||||
|
"""Test cases for rewrite_preview_url function."""
|
||||||
|
|
||||||
|
def test_handles_none_input(self):
|
||||||
|
"""Should return (None, False) for None input."""
|
||||||
|
result, was_rewritten = rewrite_preview_url(None)
|
||||||
|
assert result is None
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_handles_empty_string(self):
|
||||||
|
"""Should return (empty_string, False) for empty input."""
|
||||||
|
result, was_rewritten = rewrite_preview_url("")
|
||||||
|
assert result == ""
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_handles_invalid_url(self):
|
||||||
|
"""Should return original URL and False for invalid URLs."""
|
||||||
|
invalid_url = "not-a-valid-url"
|
||||||
|
result, was_rewritten = rewrite_preview_url(invalid_url)
|
||||||
|
assert result == invalid_url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_handles_url_without_scheme(self):
|
||||||
|
"""Should return original URL and False for URLs without scheme."""
|
||||||
|
url = "image.civitai.com/something"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_returns_false_for_non_civitai_domains(self):
|
||||||
|
"""Should not rewrite URLs from other domains."""
|
||||||
|
url = "https://example.com/image.jpg"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_returns_false_for_main_civitai_domain(self):
|
||||||
|
"""Should not rewrite URLs from main civitai.com domain."""
|
||||||
|
url = "https://civitai.com/images/123"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_rewrites_image_civitai_com_urls(self):
|
||||||
|
"""Should rewrite URLs from image.civitai.com."""
|
||||||
|
url = "https://image.civitai.com/checkpoints/original=true"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert (
|
||||||
|
result == "https://image.civitai.com/checkpoints/width=450,optimized=true"
|
||||||
|
)
|
||||||
|
assert was_rewritten is True
|
||||||
|
|
||||||
|
def test_rewrites_subdomain_civitai_urls(self):
|
||||||
|
"""Should rewrite URLs from CivitAI CDN subdomains like image-b2.civitai.com."""
|
||||||
|
url = "https://image-b2.civitai.com/file/civitai-media-cache/original=true/sample.png"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== "https://image-b2.civitai.com/file/civitai-media-cache/width=450,optimized=true/sample.png"
|
||||||
|
)
|
||||||
|
assert was_rewritten is True
|
||||||
|
|
||||||
|
def test_rewrites_multiple_subdomains(self):
|
||||||
|
"""Should rewrite URLs from various CivitAI subdomains."""
|
||||||
|
test_cases = [
|
||||||
|
"https://image-b3.civitai.com/original=true/test.jpg",
|
||||||
|
"https://cdn.civitai.com/original=true/test.png",
|
||||||
|
]
|
||||||
|
for url in test_cases:
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert "width=450,optimized=true" in result
|
||||||
|
|
||||||
|
def test_handles_urls_with_explicit_port(self):
|
||||||
|
"""Should correctly handle URLs with explicit port numbers."""
|
||||||
|
url = "https://image.civitai.com:443/checkpoints/original=true"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert "width=450,optimized=true" in result
|
||||||
|
# Port is preserved in the URL (this is acceptable behavior)
|
||||||
|
assert ":443" in result
|
||||||
|
|
||||||
|
def test_rewrites_video_urls_with_transcode(self):
|
||||||
|
"""Should rewrite video URLs with transcode parameter."""
|
||||||
|
url = "https://image.civitai.com/videos/original=true/sample.mp4"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "video")
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== "https://image.civitai.com/videos/transcode=true,width=450,optimized=true/sample.mp4"
|
||||||
|
)
|
||||||
|
assert was_rewritten is True
|
||||||
|
|
||||||
|
def test_video_rewrite_uses_case_insensitive_type(self):
|
||||||
|
"""Should handle video type case-insensitively."""
|
||||||
|
url = "https://image.civitai.com/original=true/test.mp4"
|
||||||
|
result1, was1 = rewrite_preview_url(url, "VIDEO")
|
||||||
|
result2, was2 = rewrite_preview_url(url, "Video")
|
||||||
|
assert was1 is True
|
||||||
|
assert was2 is True
|
||||||
|
assert "transcode=true" in result1
|
||||||
|
assert "transcode=true" in result2
|
||||||
|
|
||||||
|
def test_returns_original_when_no_original_true_in_path(self):
|
||||||
|
"""Should not rewrite URLs that don't contain /original=true."""
|
||||||
|
url = "https://image.civitai.com/checkpoints/optimized=true"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url)
|
||||||
|
assert result == url
|
||||||
|
assert was_rewritten is False
|
||||||
|
|
||||||
|
def test_preserves_path_structure_after_rewrite(self):
|
||||||
|
"""Should maintain path structure after rewriting."""
|
||||||
|
url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.png"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert result.startswith(
|
||||||
|
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/"
|
||||||
|
)
|
||||||
|
assert result.endswith("/12345.png")
|
||||||
|
|
||||||
|
def test_defaults_to_image_mode_when_media_type_is_none(self):
|
||||||
|
"""Should use image optimization when media_type is None."""
|
||||||
|
url = "https://image.civitai.com/original=true/test.png"
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, None)
|
||||||
|
assert was_rewritten is True
|
||||||
|
assert "transcode=true" not in result
|
||||||
|
assert "width=450,optimized=true" in result
|
||||||
|
|
||||||
|
def test_case_insensitive_hostname_matching(self):
|
||||||
|
"""Should handle case-insensitive hostname matching."""
|
||||||
|
test_cases = [
|
||||||
|
"https://IMAGE.CIVITAI.COM/original=true/test.png",
|
||||||
|
"https://Image.Civitai.Com/original=true/test.png",
|
||||||
|
"https://image-b2.CIVITAI.com/original=true/test.png",
|
||||||
|
]
|
||||||
|
for url in test_cases:
|
||||||
|
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||||
|
assert was_rewritten is True, f"Failed for URL: {url}"
|
||||||
Reference in New Issue
Block a user