feat: improve video URL detection to handle query parameters

Enhanced the `isVideoUrl` function to more accurately detect video URLs by:
- Adding support for URL query parameters and fragments
- Creating helper function `extractExtension` to handle URI decoding
- Checking multiple candidate values from different parts of the URL
- Maintaining backward compatibility with existing video detection

This improves reliability when detecting video URLs that contain query parameters or encoded characters.
This commit is contained in:
Will Miao
2025-10-27 12:21:51 +08:00
parent 136d3153fa
commit 8508763831
2 changed files with 212 additions and 5 deletions

View File

@@ -19,19 +19,77 @@ function escapeHtml(value) {
.replace(/'/g, ''');
}
function extractExtension(value) {
if (value == null) {
return '';
}
const targets = [];
const stringValue = String(value);
if (stringValue) {
targets.push(stringValue);
stringValue.split(/[?&=]/).forEach(fragment => {
if (fragment) {
targets.push(fragment);
}
});
}
for (const target of targets) {
let candidate = target;
try {
candidate = decodeURIComponent(candidate);
} catch (error) {
// ignoring malformed sequences, fallback to raw value
}
const lastDot = candidate.lastIndexOf('.');
if (lastDot === -1) {
continue;
}
const extension = candidate.slice(lastDot).toLowerCase();
if (extension.includes('/') || extension.includes('\\')) {
continue;
}
return extension;
}
return '';
}
function isVideoUrl(url) {
if (!url || typeof url !== 'string') {
return false;
}
const candidates = new Set();
const addCandidate = value => {
if (value == null) {
return;
}
const stringValue = String(value);
if (!stringValue) {
return;
}
candidates.add(stringValue);
};
addCandidate(url);
try {
const parsed = new URL(url, window.location.origin);
const pathname = parsed.pathname || '';
const extension = pathname.slice(pathname.lastIndexOf('.')).toLowerCase();
return VIDEO_EXTENSIONS.includes(extension);
addCandidate(parsed.pathname);
parsed.searchParams.forEach(value => addCandidate(value));
} catch (error) {
const normalized = url.split('?')[0].toLowerCase();
return VIDEO_EXTENSIONS.some(ext => normalized.endsWith(ext));
// ignore parse errors and rely on fallbacks below
}
for (const candidate of candidates) {
const extension = extractExtension(candidate);
if (extension && VIDEO_EXTENSIONS.includes(extension)) {
return true;
}
}
return false;
}
function formatDateLabel(value) {

View File

@@ -0,0 +1,149 @@
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
const {
MODEL_VERSIONS_MODULE,
API_FACTORY_MODULE,
DOWNLOAD_MANAGER_MODULE,
UI_HELPERS_MODULE,
STATE_MODULE,
I18N_HELPERS_MODULE,
UTILS_MODULE,
} = vi.hoisted(() => ({
MODEL_VERSIONS_MODULE: new URL('../../../static/js/components/shared/ModelVersionsTab.js', import.meta.url).pathname,
API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname,
DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname,
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
UTILS_MODULE: new URL('../../../static/js/components/shared/utils.js', import.meta.url).pathname,
}));
vi.mock(DOWNLOAD_MANAGER_MODULE, () => ({
downloadManager: {
downloadVersionWithDefaults: vi.fn(),
},
}));
vi.mock(UI_HELPERS_MODULE, () => ({
showToast: vi.fn(),
}));
const stateMock = { global: { settings: { autoplay_on_hover: false } } };
vi.mock(STATE_MODULE, () => ({
state: stateMock,
}));
vi.mock(I18N_HELPERS_MODULE, () => ({
translate: vi.fn((_, __, fallback) => fallback ?? ''),
}));
vi.mock(UTILS_MODULE, () => ({
formatFileSize: vi.fn(() => '1 MB'),
}));
vi.mock(API_FACTORY_MODULE, () => ({
getModelApiClient: vi.fn(),
}));
describe('ModelVersionsTab media rendering', () => {
let getModelApiClient;
let fetchModelUpdateVersions;
beforeEach(async () => {
vi.resetModules();
document.body.innerHTML = `
<div id="model-versions-modal">
<div id="versions-tab">
<div class="model-versions-tab"></div>
</div>
</div>
`;
stateMock.global.settings.autoplay_on_hover = false;
({ getModelApiClient } = await import(API_FACTORY_MODULE));
fetchModelUpdateVersions = vi.fn();
getModelApiClient.mockReturnValue({
fetchModelUpdateVersions,
setModelUpdateIgnore: vi.fn(),
setVersionUpdateIgnore: vi.fn(),
deleteModel: vi.fn(),
});
});
afterEach(() => {
document.body.innerHTML = '';
});
it('renders video preview when preview URL references a video file in a query parameter', async () => {
const previewUrl = '/api/lm/previews?path=%2Fhome%2Fexample%2Fvideo-preview.mp4';
fetchModelUpdateVersions.mockResolvedValue({
success: true,
record: {
shouldIgnore: false,
inLibraryVersionIds: [2],
versions: [
{
versionId: 2,
name: 'v1.0',
previewUrl,
baseModel: 'SDXL',
sizeBytes: 1024,
isInLibrary: true,
shouldIgnore: false,
},
],
},
});
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
const controller = initVersionsTab({
modalId: 'model-versions-modal',
modelType: 'loras',
modelId: 123,
currentVersionId: null,
});
await controller.load();
const videoElement = document.querySelector('.version-media video');
expect(videoElement).toBeTruthy();
expect(videoElement?.getAttribute('src')).toBe(previewUrl);
expect(document.querySelector('.version-media img')).toBeFalsy();
});
it('renders image preview when preview URL does not reference a video', async () => {
const previewUrl = '/api/lm/previews?path=%2Fhome%2Fexample%2Fpreview-image.png';
fetchModelUpdateVersions.mockResolvedValue({
success: true,
record: {
shouldIgnore: false,
inLibraryVersionIds: [3],
versions: [
{
versionId: 3,
name: 'v1.1',
previewUrl,
baseModel: 'SDXL',
sizeBytes: 2048,
isInLibrary: true,
shouldIgnore: false,
},
],
},
});
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
const controller = initVersionsTab({
modalId: 'model-versions-modal',
modelType: 'loras',
modelId: 456,
currentVersionId: null,
});
await controller.load();
const imageElement = document.querySelector('.version-media img');
expect(imageElement).toBeTruthy();
expect(imageElement?.getAttribute('src')).toBe(previewUrl);
expect(document.querySelector('.version-media video')).toBeFalsy();
});
});