diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 156b350..de5ed9f 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -19,6 +19,7 @@ import { } from '../web/resolver/node_context_menu.js'; import { startSplitterDrag } from '../web/resolver/utils/splitter_drag.js'; import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js'; +import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js'; import { getCustomNodeModelAdapter, getCustomNodeModelCategory, @@ -5039,10 +5040,12 @@ test('missing model preview shows accepted GGUF format next to its category', () }); test('hash match label map numbers distinct matched hashes', () => { - const normalizeSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeSearchResultSha256')})`); - const getLocalMatchHash = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchHash')})`); - const collectHashLabelMapHashes = eval(`(${extractMethod(searchPanelMethodsSource, 'collectHashLabelMapHashes')})`); - const getHashMatchLabelMap = eval(`(${extractMethod(searchPanelMethodsSource, 'getHashMatchLabelMap')})`); + const { + normalizeSearchResultSha256, + getLocalMatchHash, + collectHashLabelMapHashes, + getHashMatchLabelMap, + } = searchHashMethods; const firstHash = 'a'.repeat(64); const secondHash = 'b'.repeat(64); const dialog = { @@ -5068,9 +5071,11 @@ test('hash match label map numbers distinct matched hashes', () => { }); test('search result hash labels use linked local hash identity', () => { - const normalizeSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeSearchResultSha256')})`); - const getSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'getSearchResultSha256')})`); - const getHashMatchLabelForSearchResult = eval(`(${extractMethod(searchPanelMethodsSource, 'getHashMatchLabelForSearchResult')})`); + const { + normalizeSearchResultSha256, + getSearchResultSha256, + getHashMatchLabelForSearchResult, + } = searchHashMethods; const getSearchResultMatchDisplay = eval(`(${extractMethod(searchPanelMethodsSource, 'getSearchResultMatchDisplay')})`); const sha256 = 'd'.repeat(64); const hashLabelMap = new Map([[sha256, 'Hash 2']]); @@ -5098,11 +5103,13 @@ test('search result hash labels use linked local hash identity', () => { }); test('local hash identities link search results across sources by sha', () => { - const normalizeSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeSearchResultSha256')})`); - const getLocalMatchHash = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchHash')})`); - const getSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'getSearchResultSha256')})`); - const normalizeHashLookupSourceKey = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeHashLookupSourceKey')})`); - const getLocalHashMatchIdentitiesForResult = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalHashMatchIdentitiesForResult')})`); + const { + normalizeSearchResultSha256, + getLocalMatchHash, + getSearchResultSha256, + normalizeHashLookupSourceKey, + getLocalHashMatchIdentitiesForResult, + } = searchHashMethods; const sha256 = 'e'.repeat(64); const dialog = { normalizeSearchResultSha256, @@ -5127,8 +5134,7 @@ test('local hash identities link search results across sources by sha', () => { }); test('remote hash sync fetches local matches by selected result sha', async () => { - const normalizeSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeSearchResultSha256')})`); - const getSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'getSearchResultSha256')})`); + const { normalizeSearchResultSha256, getSearchResultSha256 } = searchHashMethods; const getExistingLocalHashMatchesForSha = eval(`(${extractMethod(resolveDownloadMethodsSource, 'getExistingLocalHashMatchesForSha')})`); const syncRemoteHashMatchesForResult = eval(`(${extractMethod(resolveDownloadMethodsSource, 'syncRemoteHashMatchesForResult')})`); const sha256 = 'f'.repeat(64); diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index 9818fee..a640c74 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -14,6 +14,7 @@ import { downloadTargetMethods } from "./search/download_target_methods.js"; import { searchPanelMethods } from "./search/search_panel.js"; import { searchSourceMethods } from "./search/search_source_methods.js"; import { baseModelAliasMethods } from "./search/base_model_alias_methods.js"; +import { searchHashMethods } from "./search/search_hash_methods.js"; import { queueMethods } from "./actions/queue_methods.js"; import { queueStorageMethods } from "./actions/queue_storage_methods.js"; import { downloadProgressMethods } from "./actions/download_progress_methods.js"; @@ -337,6 +338,7 @@ function applyDialogMethods(...sources) { applyDialogMethods( searchSourceMethods, baseModelAliasMethods, + searchHashMethods, searchPanelMethods, modelInfoMethods, renderFormatMethods, diff --git a/web/resolver/search/search_hash_methods.js b/web/resolver/search/search_hash_methods.js new file mode 100644 index 0000000..3c6f529 --- /dev/null +++ b/web/resolver/search/search_hash_methods.js @@ -0,0 +1,162 @@ +export const searchHashMethods = { + normalizeSearchResultSha256(value = '') { + let text = String(value || '').trim(); + text = text.replace(/^sha256[:=]/i, '').trim().toLowerCase(); + return /^[a-f0-9]{64}$/.test(text) ? text : ''; + }, + + getSearchResultSha256(result = {}) { + if (!result || typeof result !== 'object') return ''; + + const hashes = result.hashes && typeof result.hashes === 'object' ? result.hashes : {}; + const fileInfo = result.file_info && typeof result.file_info === 'object' ? result.file_info : {}; + const fileHashes = fileInfo.hashes && typeof fileInfo.hashes === 'object' ? fileInfo.hashes : {}; + const candidates = [ + result.hash_verified_sha256, + result.sha256, + result.hash, + hashes.SHA256, + hashes.sha256, + fileInfo.sha256, + fileInfo.hash, + fileHashes.SHA256, + fileHashes.sha256 + ]; + + for (const candidate of candidates) { + const hash = this.normalizeSearchResultSha256(candidate); + if (hash) return hash; + } + return ''; + }, + + collectHashLabelMapHashes(missing = {}, results = null) { + const hashes = []; + const seen = new Set(); + const addHash = (value) => { + const hash = this.normalizeSearchResultSha256?.(value) + || String(value || '').trim().toLowerCase(); + if (!hash || !/^[a-f0-9]{64}$/.test(hash) || seen.has(hash)) return; + seen.add(hash); + hashes.push(hash); + }; + const addMatch = (match) => { + if (!match || typeof match !== 'object') return; + if (!(match.hash_match || match.match_type === 'hash' || match.hash_lookup_source)) return; + addHash(this.getLocalMatchHash?.(match) || match.sha256 || match.hash); + }; + const addResult = (result) => { + if (Array.isArray(result)) { + result.forEach(addResult); + return; + } + if (!result || typeof result !== 'object') return; + const matchType = String(result.match_type || '').toLowerCase(); + if (!(result.hash_verified || result.hash_verified_sha256 || matchType === 'hash')) return; + addHash(this.getSearchResultSha256?.(result) || result.sha256 || result.hash); + }; + + (Array.isArray(missing.matches) ? missing.matches : []).forEach(addMatch); + + const stateResults = results + || this.getSearchState?.(missing)?.results + || this.searchResultCache?.get?.(this.getMissingSearchKey?.(missing))?.results + || {}; + (Array.isArray(stateResults.local_hash_matches) ? stateResults.local_hash_matches : []).forEach(addMatch); + + [ + missing.download_source, + stateResults.popular, + stateResults.model_list, + stateResults.huggingface, + stateResults.civitai, + stateResults.civarchive, + stateResults.lora_manager_archive, + ...(Array.isArray(stateResults.custom) ? stateResults.custom : []) + ].forEach(addResult); + + return hashes; + }, + + getHashMatchLabelMap(missing = {}, results = null) { + const hashes = this.collectHashLabelMapHashes?.(missing, results) || []; + const labelMap = new Map(); + if (!hashes.length) return labelMap; + + const useNumbers = hashes.length > 1; + hashes.forEach((hash, index) => { + labelMap.set(hash, useNumbers ? `Hash ${index + 1}` : 'Hash'); + }); + return labelMap; + }, + + getHashMatchLabelForSearchResult(result = {}, hashLabelMap = null, identities = []) { + const hash = this.getSearchResultSha256?.(result) || ''; + const label = hash && hashLabelMap?.get?.(hash) ? hashLabelMap.get(hash) : ''; + if (!label) return ''; + + const matchType = String(result.match_type || '').toLowerCase(); + const hasLinkedLocalMatch = Array.isArray(identities) && identities.length > 0; + return (hasLinkedLocalMatch || result.hash_verified || result.hash_verified_sha256 || matchType === 'hash') + ? label + : ''; + }, + + normalizeHashLookupSourceKey(source = '') { + return String(source || '') + .trim() + .toLowerCase() + .replace(/-/g, '_'); + }, + + getLocalHashMatchIdentitiesForResult(hashMatches = [], sourceKey = '', sourceResult = {}) { + const identities = []; + const seen = new Set(); + const addIdentity = (identity) => { + const normalized = String(identity || '').trim(); + if (!normalized || seen.has(normalized)) return; + seen.add(normalized); + identities.push(normalized); + }; + + if (Array.isArray(sourceResult?.hash_verified_local_match_identities)) { + sourceResult.hash_verified_local_match_identities.forEach(addIdentity); + } + + const normalizedSource = this.normalizeHashLookupSourceKey(sourceKey); + const sourceHash = this.getSearchResultSha256(sourceResult); + if (!Array.isArray(hashMatches) || !hashMatches.length || !normalizedSource) { + return identities; + } + + hashMatches.forEach(match => { + if (!match || typeof match !== 'object') return; + + const matchSource = this.normalizeHashLookupSourceKey(match.hash_lookup_source || ''); + const matchHash = this.normalizeSearchResultSha256(this.getLocalMatchHash(match)); + if (sourceHash) { + if (matchHash !== sourceHash) return; + } else if (!matchSource || matchSource !== normalizedSource) { + return; + } + + addIdentity(this.getLocalMatchIdentity?.(match) || ''); + }); + + return identities; + }, + + getLocalMatchHash(match = {}) { + const model = match.model || {}; + const hashes = model.hashes && typeof model.hashes === 'object' ? model.hashes : {}; + return String( + match.sha256 + || match.hash + || model.sha256 + || model.hash + || hashes.SHA256 + || hashes.sha256 + || '' + ).trim(); + }, +}; diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 25a1bbc..3e891d2 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -1629,153 +1629,6 @@ export const searchPanelMethods = { return { label: fallbackLabel, className: fallbackClass }; }, - normalizeSearchResultSha256(value = '') { - let text = String(value || '').trim(); - text = text.replace(/^sha256[:=]/i, '').trim().toLowerCase(); - return /^[a-f0-9]{64}$/.test(text) ? text : ''; - }, - - getSearchResultSha256(result = {}) { - if (!result || typeof result !== 'object') return ''; - - const hashes = result.hashes && typeof result.hashes === 'object' ? result.hashes : {}; - const fileInfo = result.file_info && typeof result.file_info === 'object' ? result.file_info : {}; - const fileHashes = fileInfo.hashes && typeof fileInfo.hashes === 'object' ? fileInfo.hashes : {}; - const candidates = [ - result.hash_verified_sha256, - result.sha256, - result.hash, - hashes.SHA256, - hashes.sha256, - fileInfo.sha256, - fileInfo.hash, - fileHashes.SHA256, - fileHashes.sha256 - ]; - - for (const candidate of candidates) { - const hash = this.normalizeSearchResultSha256(candidate); - if (hash) return hash; - } - return ''; - }, - - collectHashLabelMapHashes(missing = {}, results = null) { - const hashes = []; - const seen = new Set(); - const addHash = (value) => { - const hash = this.normalizeSearchResultSha256?.(value) - || String(value || '').trim().toLowerCase(); - if (!hash || !/^[a-f0-9]{64}$/.test(hash) || seen.has(hash)) return; - seen.add(hash); - hashes.push(hash); - }; - const addMatch = (match) => { - if (!match || typeof match !== 'object') return; - if (!(match.hash_match || match.match_type === 'hash' || match.hash_lookup_source)) return; - addHash(this.getLocalMatchHash?.(match) || match.sha256 || match.hash); - }; - const addResult = (result) => { - if (Array.isArray(result)) { - result.forEach(addResult); - return; - } - if (!result || typeof result !== 'object') return; - const matchType = String(result.match_type || '').toLowerCase(); - if (!(result.hash_verified || result.hash_verified_sha256 || matchType === 'hash')) return; - addHash(this.getSearchResultSha256?.(result) || result.sha256 || result.hash); - }; - - (Array.isArray(missing.matches) ? missing.matches : []).forEach(addMatch); - - const stateResults = results - || this.getSearchState?.(missing)?.results - || this.searchResultCache?.get?.(this.getMissingSearchKey?.(missing))?.results - || {}; - (Array.isArray(stateResults.local_hash_matches) ? stateResults.local_hash_matches : []).forEach(addMatch); - - [ - missing.download_source, - stateResults.popular, - stateResults.model_list, - stateResults.huggingface, - stateResults.civitai, - stateResults.civarchive, - stateResults.lora_manager_archive, - ...(Array.isArray(stateResults.custom) ? stateResults.custom : []) - ].forEach(addResult); - - return hashes; - }, - - getHashMatchLabelMap(missing = {}, results = null) { - const hashes = this.collectHashLabelMapHashes?.(missing, results) || []; - const labelMap = new Map(); - if (!hashes.length) return labelMap; - - const useNumbers = hashes.length > 1; - hashes.forEach((hash, index) => { - labelMap.set(hash, useNumbers ? `Hash ${index + 1}` : 'Hash'); - }); - return labelMap; - }, - - getHashMatchLabelForSearchResult(result = {}, hashLabelMap = null, identities = []) { - const hash = this.getSearchResultSha256?.(result) || ''; - const label = hash && hashLabelMap?.get?.(hash) ? hashLabelMap.get(hash) : ''; - if (!label) return ''; - - const matchType = String(result.match_type || '').toLowerCase(); - const hasLinkedLocalMatch = Array.isArray(identities) && identities.length > 0; - return (hasLinkedLocalMatch || result.hash_verified || result.hash_verified_sha256 || matchType === 'hash') - ? label - : ''; - }, - - normalizeHashLookupSourceKey(source = '') { - return String(source || '') - .trim() - .toLowerCase() - .replace(/-/g, '_'); - }, - - getLocalHashMatchIdentitiesForResult(hashMatches = [], sourceKey = '', sourceResult = {}) { - const identities = []; - const seen = new Set(); - const addIdentity = (identity) => { - const normalized = String(identity || '').trim(); - if (!normalized || seen.has(normalized)) return; - seen.add(normalized); - identities.push(normalized); - }; - - if (Array.isArray(sourceResult?.hash_verified_local_match_identities)) { - sourceResult.hash_verified_local_match_identities.forEach(addIdentity); - } - - const normalizedSource = this.normalizeHashLookupSourceKey(sourceKey); - const sourceHash = this.getSearchResultSha256(sourceResult); - if (!Array.isArray(hashMatches) || !hashMatches.length || !normalizedSource) { - return identities; - } - - hashMatches.forEach(match => { - if (!match || typeof match !== 'object') return; - - const matchSource = this.normalizeHashLookupSourceKey(match.hash_lookup_source || ''); - const matchHash = this.normalizeSearchResultSha256(this.getLocalMatchHash(match)); - if (sourceHash) { - if (matchHash !== sourceHash) return; - } else if (!matchSource || matchSource !== normalizedSource) { - return; - } - - addIdentity(this.getLocalMatchIdentity?.(match) || ''); - }); - - return identities; - }, - getDownloadSourceTableRow(missing, downloadSource = {}, hashLabelMap = null) { if (!downloadSource?.url) return null; @@ -2247,20 +2100,6 @@ export const searchPanelMethods = { }; }, - getLocalMatchHash(match = {}) { - const model = match.model || {}; - const hashes = model.hashes && typeof model.hashes === 'object' ? model.hashes : {}; - return String( - match.sha256 - || match.hash - || model.sha256 - || model.hash - || hashes.SHA256 - || hashes.sha256 - || '' - ).trim(); - }, - getLocalMatchContextData(missing = {}, match = {}) { const hash = this.getLocalMatchHash(match); const metadataPath = match.metadata_path || match.model?.metadata_path || '';