refactor shared source URL detection
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
normalizePathIdentity,
|
||||
safeStorage,
|
||||
} from '../web/resolver/utils/html_utils.js';
|
||||
import { getModelCardUrl } from '../web/resolver/utils/url_utils.js';
|
||||
import { getModelCardUrl, getSourceKeyFromUrl } from '../web/resolver/utils/url_utils.js';
|
||||
import { joinPathPreservingStyle } from '../web/resolver/utils/path_utils.js';
|
||||
import {
|
||||
classifyLocalMatches,
|
||||
@@ -4758,11 +4758,6 @@ test('source link prefers the selected provider page over another provider mirro
|
||||
civarchivePage
|
||||
];
|
||||
},
|
||||
getContextMenuSourceKeyFromUrl(value = '') {
|
||||
if (value.includes('civarchive.com')) return 'civarchive';
|
||||
if (value.includes('huggingface.co')) return 'huggingface';
|
||||
return '';
|
||||
},
|
||||
normalizeContextMenuSourceUrl(value = '') {
|
||||
return value;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { getModelCardUrl, parseHuggingFaceFileUrl } from '../web/resolver/utils/url_utils.js';
|
||||
import {
|
||||
getModelCardUrl,
|
||||
getSourceKeyFromUrl,
|
||||
parseHuggingFaceFileUrl,
|
||||
} from '../web/resolver/utils/url_utils.js';
|
||||
import { getSourceKeyFromText } from '../web/resolver/utils/source_labels.js';
|
||||
|
||||
test('source URL parsing normalizes supported provider hosts', () => {
|
||||
const cases = [
|
||||
['https://huggingface.co/owner/repo', 'huggingface'],
|
||||
['https://www.huggingface.co/owner/repo', 'huggingface'],
|
||||
['https://cdn.civarchive.com/models/1', 'civarchive'],
|
||||
['https://civitai.com/models/1', 'civitai'],
|
||||
['https://www.civitai.red/models/1', 'civitai'],
|
||||
['https://unknown.example/models/1', ''],
|
||||
['not a URL', ''],
|
||||
];
|
||||
|
||||
for (const [value, expected] of cases) {
|
||||
assert.equal(getSourceKeyFromUrl(value), expected, value);
|
||||
}
|
||||
});
|
||||
|
||||
test('source URL parsing can preserve text fallback semantics', () => {
|
||||
assert.equal(getSourceKeyFromUrl('CivArchive mirror', { fallbackToText: true }), 'civarchive');
|
||||
assert.equal(getSourceKeyFromUrl('LoRA Archive result', { fallbackToText: true }), 'lora_manager_archive');
|
||||
assert.equal(getSourceKeyFromUrl('unknown.example/model', { fallbackToText: true }), '');
|
||||
});
|
||||
|
||||
test('source text parsing normalizes provider aliases', () => {
|
||||
assert.equal(getSourceKeyFromText('Hugging Face result'), 'huggingface');
|
||||
assert.equal(getSourceKeyFromText('Civ Archive result'), 'civarchive');
|
||||
assert.equal(getSourceKeyFromText('CivitAI result'), 'civitai');
|
||||
assert.equal(getSourceKeyFromText('LoRA Archive result'), 'lora_manager_archive');
|
||||
assert.equal(getSourceKeyFromText('unknown'), '');
|
||||
});
|
||||
|
||||
test('Hugging Face file URLs expose repository, revision, path, and filename', () => {
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -28,6 +28,16 @@ export function normalizeSourceKey(value, { trim = true } = {}) {
|
||||
return (trim ? text.trim() : text).toLowerCase().replace(/-/g, '_');
|
||||
}
|
||||
|
||||
export function getSourceKeyFromText(value = '') {
|
||||
const text = String(value || '').toLowerCase();
|
||||
if (!text) return '';
|
||||
if (text.includes('huggingface') || text.includes('hugging face')) return 'huggingface';
|
||||
if (text.includes('civarchive') || text.includes('civ archive')) return 'civarchive';
|
||||
if (text.includes('civitai') || text.includes('civitai.red')) return 'civitai';
|
||||
if (text.includes('lora_manager_archive') || text.includes('lora archive')) return 'lora_manager_archive';
|
||||
return '';
|
||||
}
|
||||
|
||||
export function normalizeSourceList(sources = []) {
|
||||
return new Set(
|
||||
(Array.isArray(sources) ? sources : [sources])
|
||||
|
||||
@@ -2,6 +2,32 @@
|
||||
* URL Utilities for Model Resolver
|
||||
*/
|
||||
|
||||
import { getSourceKeyFromText } from './source_labels.js';
|
||||
|
||||
/**
|
||||
* Resolve a supported provider key from a URL host.
|
||||
* @param {string} value
|
||||
* @param {{fallbackToText?: boolean}} options
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getSourceKeyFromUrl(value, { fallbackToText = false } = {}) {
|
||||
const url = String(value || '').trim();
|
||||
if (!url) return '';
|
||||
|
||||
try {
|
||||
const baseUrl = globalThis.location?.origin || 'http://localhost';
|
||||
const parsed = new URL(url, baseUrl);
|
||||
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
||||
if (host === 'huggingface.co' || host.endsWith('.huggingface.co')) return 'huggingface';
|
||||
if (host === 'civarchive.com' || host.endsWith('.civarchive.com')) return 'civarchive';
|
||||
if (host === 'civitai.com' || host.endsWith('.civitai.com') || host === 'civitai.red' || host.endsWith('.civitai.red')) return 'civitai';
|
||||
} catch {
|
||||
// Preserve the caller's optional text fallback for malformed URLs.
|
||||
}
|
||||
|
||||
return fallbackToText ? getSourceKeyFromText(url) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Hugging Face blob/resolve file URL into repository coordinates.
|
||||
* @param {string} value
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { escapeHtml, escapeJsString, getFilenameFromPath, sanitizeDescriptionHtml, pollBackgroundTask, safeStorage, copyTextWithFeedback } from "../utils/html_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getModelCardUrl, getSourceKeyFromUrl } from "../utils/url_utils.js";
|
||||
import { extractComfyWorkflow } from "../utils/workflow_metadata.js";
|
||||
import { getSha256Field, normalizeSha256 } from "../utils/hash_utils.js";
|
||||
import { parseFiniteNumber } from "../utils/size_utils.js";
|
||||
import { getSourceDisplayLabel } from "../utils/source_labels.js";
|
||||
import {
|
||||
getSourceDisplayLabel,
|
||||
getSourceKeyFromText,
|
||||
normalizeSourceKey,
|
||||
} from "../utils/source_labels.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
export const modelInfoMethods = {
|
||||
escapeHtml,
|
||||
@@ -189,50 +193,27 @@ export const modelInfoMethods = {
|
||||
},
|
||||
|
||||
getContextMenuSourceConfig(key = '') {
|
||||
const normalized = String(key || '').toLowerCase().replace(/[-\s]+/g, '_');
|
||||
const normalized = normalizeSourceKey(key).replace(/\s+/g, '_');
|
||||
const configs = {
|
||||
civitai: { key: 'civitai', label: 'CivitAI', icon: 'civitai' },
|
||||
civarchive: { key: 'civarchive', label: 'CivArchive', icon: 'civarchive' },
|
||||
huggingface: { key: 'huggingface', label: 'HuggingFace', icon: 'huggingface' },
|
||||
lora_manager_archive: { key: 'lora_manager_archive', label: 'LoRA Archive', icon: 'loraManager' }
|
||||
civitai: { key: 'civitai', icon: 'civitai' },
|
||||
civarchive: { key: 'civarchive', icon: 'civarchive' },
|
||||
huggingface: { key: 'huggingface', icon: 'huggingface' },
|
||||
lora_manager_archive: { key: 'lora_manager_archive', icon: 'loraManager' }
|
||||
};
|
||||
return configs[normalized] || { key: normalized || 'source', label: 'Source', icon: 'externalLink' };
|
||||
},
|
||||
|
||||
getContextMenuSourceKeyFromText(value = '') {
|
||||
const text = String(value || '').toLowerCase();
|
||||
if (!text) return '';
|
||||
if (text.includes('huggingface') || text.includes('hugging face')) return 'huggingface';
|
||||
if (text.includes('civarchive') || text.includes('civ archive')) return 'civarchive';
|
||||
if (text.includes('civitai') || text.includes('civitai.red')) return 'civitai';
|
||||
if (text.includes('lora_manager_archive') || text.includes('lora archive')) return 'lora_manager_archive';
|
||||
return '';
|
||||
},
|
||||
|
||||
getContextMenuSourceKeyFromUrl(value = '') {
|
||||
const url = String(value || '').trim();
|
||||
if (!url) return '';
|
||||
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
||||
if (host === 'huggingface.co' || host.endsWith('.huggingface.co')) return 'huggingface';
|
||||
if (host === 'civarchive.com' || host.endsWith('.civarchive.com')) return 'civarchive';
|
||||
if (host === 'civitai.com' || host.endsWith('.civitai.com') || host === 'civitai.red' || host.endsWith('.civitai.red')) return 'civitai';
|
||||
} catch (_error) {
|
||||
return this.getContextMenuSourceKeyFromText(url);
|
||||
}
|
||||
|
||||
return this.getContextMenuSourceKeyFromText(url);
|
||||
const config = configs[normalized];
|
||||
const label = getSourceDisplayLabel(normalized, { fallback: 'Source' });
|
||||
return config
|
||||
? { ...config, label }
|
||||
: { key: normalized || 'source', label, icon: 'externalLink' };
|
||||
},
|
||||
|
||||
getContextMenuSourceKey(model = {}) {
|
||||
return this.getContextMenuSourceKeyFromText(model?.details_source)
|
||||
|| this.getContextMenuSourceKeyFromText(model?.source)
|
||||
|| this.getContextMenuSourceKeyFromText(model?.source_name)
|
||||
|| this.getContextMenuSourceKeyFromText(model?.sourceName)
|
||||
|| this.getContextMenuSourceKeyFromText(model?.metadata_source)
|
||||
|| this.getContextMenuSourceKeyFromText(model?.metadataSource)
|
||||
return getSourceKeyFromText(model?.details_source)
|
||||
|| getSourceKeyFromText(model?.source)
|
||||
|| getSourceKeyFromText(model?.source_name)
|
||||
|| getSourceKeyFromText(model?.sourceName)
|
||||
|| getSourceKeyFromText(model?.metadata_source)
|
||||
|| getSourceKeyFromText(model?.metadataSource)
|
||||
|| '';
|
||||
},
|
||||
|
||||
@@ -328,13 +309,13 @@ export const modelInfoMethods = {
|
||||
const fallbackKey = this.getContextMenuSourceKey(model);
|
||||
const candidates = this.getContextMenuSourceUrlCandidates(model);
|
||||
const resolveCandidate = (candidate) => {
|
||||
const candidateKey = this.getContextMenuSourceKeyFromUrl(candidate) || fallbackKey;
|
||||
const candidateKey = getSourceKeyFromUrl(candidate, { fallbackToText: true }) || fallbackKey;
|
||||
const normalizedUrl = this.normalizeContextMenuSourceUrl(candidate, candidateKey);
|
||||
if (!normalizedUrl) return null;
|
||||
|
||||
const cardUrl = getModelCardUrl(normalizedUrl) || normalizedUrl;
|
||||
const sourceKey = this.getContextMenuSourceKeyFromUrl(cardUrl)
|
||||
|| this.getContextMenuSourceKeyFromUrl(normalizedUrl)
|
||||
const sourceKey = getSourceKeyFromUrl(cardUrl, { fallbackToText: true })
|
||||
|| getSourceKeyFromUrl(normalizedUrl, { fallbackToText: true })
|
||||
|| candidateKey;
|
||||
const config = this.getContextMenuSourceConfig(sourceKey);
|
||||
return {
|
||||
@@ -345,7 +326,7 @@ export const modelInfoMethods = {
|
||||
|
||||
const preferredCandidates = fallbackKey
|
||||
? candidates.filter(candidate => {
|
||||
const candidateKey = this.getContextMenuSourceKeyFromUrl(candidate);
|
||||
const candidateKey = getSourceKeyFromUrl(candidate, { fallbackToText: true });
|
||||
return !candidateKey || candidateKey === fallbackKey;
|
||||
})
|
||||
: candidates;
|
||||
@@ -1868,7 +1849,7 @@ export const modelInfoMethods = {
|
||||
const sourceKey = this.getContextMenuSourceKey(data);
|
||||
if (sourceKey) return sourceKey;
|
||||
for (const url of this.getContextMenuSourceUrlCandidates(data)) {
|
||||
const key = this.getContextMenuSourceKeyFromUrl(url);
|
||||
const key = getSourceKeyFromUrl(url, { fallbackToText: true });
|
||||
if (key) return key;
|
||||
}
|
||||
return 'metadata';
|
||||
@@ -3183,8 +3164,10 @@ export const modelInfoMethods = {
|
||||
const source = String(mirror.source || '').trim();
|
||||
if (source) return source;
|
||||
const host = this.getSourceModelMirrorHost(mirror.url || '');
|
||||
if (host.includes('huggingface.co')) return 'HuggingFace';
|
||||
if (host.includes('civitai.com')) return 'CivitAI';
|
||||
const sourceKey = getSourceKeyFromUrl(mirror.url || '');
|
||||
if (sourceKey === 'huggingface' || sourceKey === 'civitai') {
|
||||
return getSourceDisplayLabel(sourceKey);
|
||||
}
|
||||
return host || `Mirror ${index + 1}`;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user