mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
f7b247f9e8
- New OptimizationMode.DISPLAY (width=2400 for images, full quality for videos) and getDisplayUrl(); the in-modal main viewer renders at most ~1200 CSS px wide, so full-size originals wasted 50-70% bandwidth - Main viewer and adjacent prefetch use display URLs; the full-size media viewer keeps using getShowcaseUrl for original quality
303 lines
9.9 KiB
JavaScript
303 lines
9.9 KiB
JavaScript
/**
|
|
* CivitAI URL utilities
|
|
* Functions for working with CivitAI media URLs
|
|
*/
|
|
|
|
/**
|
|
* Optimization strategies for CivitAI URLs
|
|
*/
|
|
export const OptimizationMode = {
|
|
/** Full quality for showcase/display - uses /optimized=true only */
|
|
SHOWCASE: 'showcase',
|
|
/** In-modal display - caps image width at 2400 (covers the ~1200 CSS px
|
|
* main viewer at DPR 2); videos stay full quality */
|
|
DISPLAY: 'display',
|
|
/** Thumbnail size for cards - uses /width=450,optimized=true */
|
|
THUMBNAIL: 'thumbnail',
|
|
/** Small thumbnails for the showcase gallery strip (72px display) - uses /width=160,optimized=true */
|
|
GALLERY_THUMBNAIL: 'gallery-thumbnail',
|
|
};
|
|
|
|
export const DEFAULT_CIVITAI_PAGE_HOST = 'civitai.com';
|
|
|
|
const SUPPORTED_CIVITAI_PAGE_HOSTS = new Set([
|
|
'civitai.com',
|
|
'civitai.red',
|
|
]);
|
|
|
|
export function normalizeCivitaiPageHost(hostname) {
|
|
if (!hostname || typeof hostname !== 'string') {
|
|
return DEFAULT_CIVITAI_PAGE_HOST;
|
|
}
|
|
|
|
const normalized = hostname.trim().toLowerCase();
|
|
if (SUPPORTED_CIVITAI_PAGE_HOSTS.has(normalized)) {
|
|
return normalized;
|
|
}
|
|
|
|
return DEFAULT_CIVITAI_PAGE_HOST;
|
|
}
|
|
|
|
export function buildCivitaiModelUrl(modelId, versionId = null, host = DEFAULT_CIVITAI_PAGE_HOST) {
|
|
const normalizedHost = normalizeCivitaiPageHost(host);
|
|
const normalizedModelId = modelId == null ? '' : String(modelId).trim();
|
|
const normalizedVersionId = versionId == null ? '' : String(versionId).trim();
|
|
|
|
if (normalizedModelId) {
|
|
const encodedModelId = encodeURIComponent(normalizedModelId);
|
|
let url = `https://${normalizedHost}/models/${encodedModelId}`;
|
|
if (normalizedVersionId) {
|
|
url += `?modelVersionId=${encodeURIComponent(normalizedVersionId)}`;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
if (normalizedVersionId) {
|
|
return `https://${normalizedHost}/model-versions/${encodeURIComponent(normalizedVersionId)}`;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function buildCivitaiSearchUrl(query, host = DEFAULT_CIVITAI_PAGE_HOST) {
|
|
const normalizedQuery = query == null ? '' : String(query).trim();
|
|
if (!normalizedQuery) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedHost = normalizeCivitaiPageHost(host);
|
|
return `https://${normalizedHost}/models?query=${encodeURIComponent(normalizedQuery)}`;
|
|
}
|
|
|
|
export function buildCivitaiUrl({ modelId = null, versionId = null, modelName = null, host = DEFAULT_CIVITAI_PAGE_HOST } = {}) {
|
|
return (
|
|
buildCivitaiModelUrl(modelId, versionId, host)
|
|
|| buildCivitaiSearchUrl(modelName, host)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rewrite Civitai preview URLs to use optimized renditions.
|
|
* Mirrors the backend's rewrite_preview_url() function from py/utils/civitai_utils.py
|
|
*
|
|
* @param {string|null} sourceUrl - Original preview URL from the Civitai API
|
|
* @param {string|null} mediaType - Optional media type hint ("image" or "video")
|
|
* @param {string} mode - Optimization mode ('showcase' or 'thumbnail')
|
|
* @returns {[string|null, boolean]} - Tuple of [rewritten URL or original, wasRewritten flag]
|
|
*/
|
|
export function rewriteCivitaiUrl(sourceUrl, mediaType = null, mode = OptimizationMode.THUMBNAIL) {
|
|
if (!sourceUrl) {
|
|
return [sourceUrl, false];
|
|
}
|
|
|
|
try {
|
|
const url = new URL(sourceUrl);
|
|
|
|
// Check if it's a CivitAI CDN domain (supports all subdomains like image-b2.civitai.com)
|
|
const hostname = url.hostname.toLowerCase();
|
|
if (hostname === 'civitai.com' || !hostname.endsWith('.civitai.com')) {
|
|
return [sourceUrl, false];
|
|
}
|
|
|
|
// Determine replacement based on mode and media type
|
|
const isVideo = Boolean(mediaType && mediaType.toLowerCase() === 'video');
|
|
let replacement;
|
|
if (mode === OptimizationMode.SHOWCASE) {
|
|
// Full quality for showcase - no width restriction
|
|
replacement = '/optimized=true';
|
|
} else if (mode === OptimizationMode.DISPLAY) {
|
|
// Display mode caps image width for in-modal viewing; videos stay
|
|
// full quality (CDN transcoding costs more than it saves here)
|
|
replacement = isVideo ? '/optimized=true' : '/width=2400,optimized=true';
|
|
} else {
|
|
// Thumbnail modes with width restriction
|
|
const width = mode === OptimizationMode.GALLERY_THUMBNAIL ? 160 : 450;
|
|
replacement = `/width=${width},optimized=true`;
|
|
if (isVideo) {
|
|
replacement = `/transcode=true,width=${width},optimized=true`;
|
|
}
|
|
}
|
|
|
|
// Replace /original=true with optimized version
|
|
if (!url.pathname.includes('/original=true')) {
|
|
return [sourceUrl, false];
|
|
}
|
|
|
|
const updatedPath = url.pathname.replace('/original=true', replacement, 1);
|
|
|
|
if (updatedPath === url.pathname) {
|
|
return [sourceUrl, false];
|
|
}
|
|
|
|
url.pathname = updatedPath;
|
|
return [url.toString(), true];
|
|
} catch (e) {
|
|
// Invalid URL
|
|
return [sourceUrl, false];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the optimized URL for a media item, falling back to original if not a CivitAI URL
|
|
*
|
|
* @param {string} url - Original URL
|
|
* @param {string} type - Media type ("image" or "video")
|
|
* @param {string} mode - Optimization mode ('showcase' or 'thumbnail')
|
|
* @returns {string} - Optimized URL or original URL
|
|
*/
|
|
export function getOptimizedUrl(url, type = 'image', mode = OptimizationMode.THUMBNAIL) {
|
|
const [optimizedUrl] = rewriteCivitaiUrl(url, type, mode);
|
|
return optimizedUrl || url;
|
|
}
|
|
|
|
/**
|
|
* Get showcase-optimized URL (full quality)
|
|
*
|
|
* @param {string} url - Original URL
|
|
* @param {string} type - Media type ("image" or "video")
|
|
* @returns {string} - Optimized URL for showcase display
|
|
*/
|
|
export function getShowcaseUrl(url, type = 'image') {
|
|
return getOptimizedUrl(url, type, OptimizationMode.SHOWCASE);
|
|
}
|
|
|
|
/**
|
|
* Get display-optimized URL for the in-modal main viewer (images capped at
|
|
* width=2400; videos full quality). Use getShowcaseUrl for full-size viewing
|
|
* (e.g. the media viewer overlay)
|
|
*
|
|
* @param {string} url - Original URL
|
|
* @param {string} type - Media type ("image" or "video")
|
|
* @returns {string} - Optimized URL for in-modal display
|
|
*/
|
|
export function getDisplayUrl(url, type = 'image') {
|
|
return getOptimizedUrl(url, type, OptimizationMode.DISPLAY);
|
|
}
|
|
|
|
/**
|
|
* Get thumbnail-optimized URL (width=450)
|
|
*
|
|
* @param {string} url - Original URL
|
|
* @param {string} type - Media type ("image" or "video")
|
|
* @returns {string} - Optimized URL for thumbnail display
|
|
*/
|
|
export function getThumbnailUrl(url, type = 'image') {
|
|
return getOptimizedUrl(url, type, OptimizationMode.THUMBNAIL);
|
|
}
|
|
|
|
/**
|
|
* Get gallery-strip-thumbnail-optimized URL (width=160, for the 72px strip)
|
|
*
|
|
* @param {string} url - Original URL
|
|
* @param {string} type - Media type ("image" or "video")
|
|
* @returns {string} - Optimized URL for gallery strip thumbnail display
|
|
*/
|
|
export function getGalleryThumbnailUrl(url, type = 'image') {
|
|
return getOptimizedUrl(url, type, OptimizationMode.GALLERY_THUMBNAIL);
|
|
}
|
|
|
|
/**
|
|
* Check if a URL is from CivitAI
|
|
*
|
|
* @param {string} url - URL to check
|
|
* @returns {boolean} - True if it's a CivitAI URL
|
|
*/
|
|
export function isCivitaiUrl(url) {
|
|
if (!url) return false;
|
|
try {
|
|
const parsed = new URL(url);
|
|
const hostname = parsed.hostname.toLowerCase();
|
|
return hostname.endsWith('.civitai.com') && hostname !== 'civitai.com';
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function isSupportedCivitaiPageHost(hostname) {
|
|
if (!hostname) {
|
|
return false;
|
|
}
|
|
|
|
return SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname.toLowerCase());
|
|
}
|
|
|
|
export function extractCivitaiModelUrlParts(url) {
|
|
if (!url) {
|
|
return { modelId: null, modelVersionId: null };
|
|
}
|
|
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (!isSupportedCivitaiPageHost(parsedUrl.hostname)) {
|
|
return { modelId: null, modelVersionId: null };
|
|
}
|
|
|
|
const pathMatch = parsedUrl.pathname.match(/\/models\/(\d+)/);
|
|
const modelId = pathMatch ? pathMatch[1] : null;
|
|
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
|
|
|
return { modelId, modelVersionId };
|
|
} catch (e) {
|
|
return { modelId: null, modelVersionId: null };
|
|
}
|
|
}
|
|
|
|
const CIVITARCHIVE_PAGE_HOSTS = new Set([
|
|
'civitaiarchive.com',
|
|
'civarchive.com',
|
|
]);
|
|
|
|
/**
|
|
* Classify a relink URL by its hosting source and extract ids.
|
|
* CivitArchive mirrors the Civitai id namespace, so both sources resolve to
|
|
* the same {modelId, modelVersionId} shape; only `source` differs.
|
|
*/
|
|
export function classifyModelRelinkUrl(url) {
|
|
if (!url || typeof url !== 'string') {
|
|
return { source: null, modelId: null, modelVersionId: null };
|
|
}
|
|
|
|
let parsedUrl;
|
|
try {
|
|
parsedUrl = new URL(url.trim());
|
|
} catch (e) {
|
|
return { source: null, modelId: null, modelVersionId: null };
|
|
}
|
|
|
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
return { source: null, modelId: null, modelVersionId: null };
|
|
}
|
|
|
|
const hostname = parsedUrl.hostname.toLowerCase().replace(/^www\./, '');
|
|
const pathMatch = parsedUrl.pathname.match(/\/models\/(\d+)/);
|
|
const modelId = pathMatch ? pathMatch[1] : null;
|
|
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
|
|
|
if (SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname)) {
|
|
return { source: 'civitai', modelId, modelVersionId };
|
|
}
|
|
if (CIVITARCHIVE_PAGE_HOSTS.has(hostname) && modelId) {
|
|
return { source: 'civarchive', modelId, modelVersionId };
|
|
}
|
|
|
|
return { source: null, modelId: null, modelVersionId: null };
|
|
}
|
|
|
|
export function extractCivitaiImageId(url) {
|
|
if (!url) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (!isSupportedCivitaiPageHost(parsedUrl.hostname)) {
|
|
return null;
|
|
}
|
|
|
|
const pathMatch = parsedUrl.pathname.match(/\/images\/(\d+)/);
|
|
return pathMatch ? pathMatch[1] : null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|