mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(showcase): optimize CivitAI media URLs for better performance
- Add CivitAI URL utility with optimization strategies for showcase and thumbnail modes - Replace /original=true with /optimized=true for showcase videos to reduce bandwidth - Remove redundant crossorigin and referrerpolicy attributes from video elements - Use media type detection to apply appropriate optimization (image vs video) - Integrate URL optimization into showcase rendering for improved loading times
This commit is contained in:
@@ -26,8 +26,7 @@ export function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText,
|
||||
</button>
|
||||
` : ''}
|
||||
${mediaControlsHtml}
|
||||
<video controls autoplay muted loop crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
<video controls autoplay muted loop
|
||||
data-local-src="${localUrl || ''}"
|
||||
data-remote-src="${remoteUrl}"
|
||||
data-nsfw-level="${nsfwLevel}"
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from './MediaUtils.js';
|
||||
import { generateMetadataPanel } from './MetadataPanel.js';
|
||||
import { generateImageWrapper, generateVideoWrapper } from './MediaRenderers.js';
|
||||
import { getShowcaseUrl } from '../../../utils/civitaiUtils.js';
|
||||
|
||||
export const showcaseListenerMetrics = {
|
||||
wheelListeners: 0,
|
||||
@@ -157,11 +158,19 @@ export function renderShowcaseContent(images, exampleFiles = [], startExpanded =
|
||||
function renderMediaItem(img, index, exampleFiles) {
|
||||
// Find matching file in our list of actual files
|
||||
let localFile = findLocalFile(img, index, exampleFiles);
|
||||
|
||||
const remoteUrl = img.url || '';
|
||||
|
||||
// Get original remote URL
|
||||
const originalRemoteUrl = img.url || '';
|
||||
|
||||
// Determine media type for optimization
|
||||
const isVideo = localFile ? localFile.is_video :
|
||||
originalRemoteUrl.endsWith('.mp4') || originalRemoteUrl.endsWith('.webm');
|
||||
const mediaType = isVideo ? 'video' : 'image';
|
||||
|
||||
// Optimize CivitAI URLs for showcase display (full quality)
|
||||
const remoteUrl = getShowcaseUrl(originalRemoteUrl, mediaType);
|
||||
|
||||
const localUrl = localFile ? localFile.path : '';
|
||||
const isVideo = localFile ? localFile.is_video :
|
||||
remoteUrl.endsWith('.mp4') || remoteUrl.endsWith('.webm');
|
||||
|
||||
// Calculate appropriate aspect ratio
|
||||
const aspectRatio = (img.height / img.width) * 100;
|
||||
|
||||
119
static/js/utils/civitaiUtils.js
Normal file
119
static/js/utils/civitaiUtils.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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',
|
||||
/** Thumbnail size for cards - uses /width=450,optimized=true */
|
||||
THUMBNAIL: 'thumbnail',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 image domain
|
||||
if (url.hostname.toLowerCase() !== 'image.civitai.com') {
|
||||
return [sourceUrl, false];
|
||||
}
|
||||
|
||||
// Determine replacement based on mode and media type
|
||||
let replacement;
|
||||
if (mode === OptimizationMode.SHOWCASE) {
|
||||
// Full quality for showcase - no width restriction
|
||||
replacement = '/optimized=true';
|
||||
} else {
|
||||
// Thumbnail mode with width restriction
|
||||
replacement = '/width=450,optimized=true';
|
||||
if (mediaType && mediaType.toLowerCase() === 'video') {
|
||||
replacement = '/transcode=true,width=450,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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return parsed.hostname.toLowerCase() === 'image.civitai.com';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
172
tests/frontend/utils/civitaiUtils.test.js
Normal file
172
tests/frontend/utils/civitaiUtils.test.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
rewriteCivitaiUrl,
|
||||
getOptimizedUrl,
|
||||
getShowcaseUrl,
|
||||
getThumbnailUrl,
|
||||
isCivitaiUrl,
|
||||
OptimizationMode
|
||||
} from '../../../static/js/utils/civitaiUtils.js';
|
||||
|
||||
describe('civitaiUtils', () => {
|
||||
describe('OptimizationMode', () => {
|
||||
it('should have correct mode values', () => {
|
||||
expect(OptimizationMode.SHOWCASE).toBe('showcase');
|
||||
expect(OptimizationMode.THUMBNAIL).toBe('thumbnail');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rewriteCivitaiUrl', () => {
|
||||
it('should rewrite image URLs with /original=true for thumbnail mode', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450,optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should rewrite image URLs with /original=true for showcase mode (no width)', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.SHOWCASE);
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should rewrite video URLs with /original=true for thumbnail mode', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'video', OptimizationMode.THUMBNAIL);
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=450,optimized=true/12345.mp4');
|
||||
});
|
||||
|
||||
it('should rewrite video URLs with /original=true for showcase mode (no width/transcode)', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'video', OptimizationMode.SHOWCASE);
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.mp4');
|
||||
});
|
||||
|
||||
it('should default to thumbnail mode when mode is not specified', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image');
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450,optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should not rewrite URLs without /original=true', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450/12345.jpeg';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||
|
||||
expect(wasRewritten).toBe(false);
|
||||
expect(rewritten).toBe(originalUrl);
|
||||
});
|
||||
|
||||
it('should not rewrite non-CivitAI URLs', () => {
|
||||
const originalUrl = 'https://example.com/image.jpg';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.SHOWCASE);
|
||||
|
||||
expect(wasRewritten).toBe(false);
|
||||
expect(rewritten).toBe(originalUrl);
|
||||
});
|
||||
|
||||
it('should handle null/undefined URLs', () => {
|
||||
const [rewritten1, wasRewritten1] = rewriteCivitaiUrl(null, 'image');
|
||||
expect(wasRewritten1).toBe(false);
|
||||
expect(rewritten1).toBe(null);
|
||||
|
||||
const [rewritten2, wasRewritten2] = rewriteCivitaiUrl(undefined, 'image');
|
||||
expect(wasRewritten2).toBe(false);
|
||||
expect(rewritten2).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl('', 'image');
|
||||
expect(wasRewritten).toBe(false);
|
||||
expect(rewritten).toBe('');
|
||||
});
|
||||
|
||||
it('should handle invalid URLs gracefully', () => {
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl('not-a-valid-url', 'image');
|
||||
expect(wasRewritten).toBe(false);
|
||||
expect(rewritten).toBe('not-a-valid-url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptimizedUrl', () => {
|
||||
it('should return optimized URL for CivitAI images in thumbnail mode', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const optimized = getOptimizedUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||
|
||||
expect(optimized).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450,optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should return optimized URL for CivitAI images in showcase mode', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const optimized = getOptimizedUrl(originalUrl, 'image', OptimizationMode.SHOWCASE);
|
||||
|
||||
expect(optimized).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should return original URL for non-CivitAI URLs', () => {
|
||||
const originalUrl = 'https://example.com/image.jpg';
|
||||
const optimized = getOptimizedUrl(originalUrl, 'image');
|
||||
|
||||
expect(optimized).toBe(originalUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShowcaseUrl', () => {
|
||||
it('should return showcase-optimized URL (full quality)', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const showcaseUrl = getShowcaseUrl(originalUrl, 'image');
|
||||
|
||||
expect(showcaseUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should handle videos for showcase', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
|
||||
const showcaseUrl = getShowcaseUrl(originalUrl, 'video');
|
||||
|
||||
expect(showcaseUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.mp4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThumbnailUrl', () => {
|
||||
it('should return thumbnail-optimized URL (width=450)', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||
const thumbnailUrl = getThumbnailUrl(originalUrl, 'image');
|
||||
|
||||
expect(thumbnailUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450,optimized=true/12345.jpeg');
|
||||
});
|
||||
|
||||
it('should handle videos for thumbnails', () => {
|
||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
|
||||
const thumbnailUrl = getThumbnailUrl(originalUrl, 'video');
|
||||
|
||||
expect(thumbnailUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=450,optimized=true/12345.mp4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCivitaiUrl', () => {
|
||||
it('should return true for CivitAI URLs', () => {
|
||||
expect(isCivitaiUrl('https://image.civitai.com/something')).toBe(true);
|
||||
expect(isCivitaiUrl('https://image.civitai.com/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-CivitAI URLs', () => {
|
||||
expect(isCivitaiUrl('https://example.com/image.jpg')).toBe(false);
|
||||
expect(isCivitaiUrl('https://civitai.com/image.jpg')).toBe(false);
|
||||
expect(isCivitaiUrl('')).toBe(false);
|
||||
expect(isCivitaiUrl(null)).toBe(false);
|
||||
expect(isCivitaiUrl(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle invalid URLs gracefully', () => {
|
||||
expect(isCivitaiUrl('not-a-url')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user