mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-28 08:21:27 -03:00
fix(recipes): serve duplicate scan from cache and guard against re-entry
This commit is contained in:
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "{count} Duplikat-Gruppen gefunden",
|
"found": "{count} Duplikat-Gruppen gefunden",
|
||||||
"noGroups": "Keine Duplikat-Gruppen mit dem aktuellen Abgleichskriterium gefunden",
|
"noGroups": "Keine Duplikat-Gruppen mit dem aktuellen Abgleichskriterium gefunden",
|
||||||
"keepLatest": "Neueste Versionen behalten",
|
"keepLatest": "Neueste Versionen behalten",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "Scanning for duplicate recipes...",
|
||||||
"found": "Found {count} duplicate groups",
|
"found": "Found {count} duplicate groups",
|
||||||
"noGroups": "No duplicate groups found with the current matching basis",
|
"noGroups": "No duplicate groups found with the current matching basis",
|
||||||
"keepLatest": "Keep Latest Versions",
|
"keepLatest": "Keep Latest Versions",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "Se encontraron {count} grupos de duplicados",
|
"found": "Se encontraron {count} grupos de duplicados",
|
||||||
"noGroups": "No se encontraron grupos de duplicados con el criterio de coincidencia actual",
|
"noGroups": "No se encontraron grupos de duplicados con el criterio de coincidencia actual",
|
||||||
"keepLatest": "Mantener versiones más recientes",
|
"keepLatest": "Mantener versiones más recientes",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "Trouvé {count} groupes de doublons",
|
"found": "Trouvé {count} groupes de doublons",
|
||||||
"noGroups": "Aucun groupe de doublons trouvé avec le critère de correspondance actuel",
|
"noGroups": "Aucun groupe de doublons trouvé avec le critère de correspondance actuel",
|
||||||
"keepLatest": "Garder les dernières versions",
|
"keepLatest": "Garder les dernières versions",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "נמצאו {count} קבוצות כפולות",
|
"found": "נמצאו {count} קבוצות כפולות",
|
||||||
"noGroups": "לא נמצאו קבוצות כפולות לפי קריטריון ההתאמה הנוכחי",
|
"noGroups": "לא נמצאו קבוצות כפולות לפי קריטריון ההתאמה הנוכחי",
|
||||||
"keepLatest": "שמור גרסאות אחרונות",
|
"keepLatest": "שמור גרסאות אחרונות",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "{count} 個の重複グループが見つかりました",
|
"found": "{count} 個の重複グループが見つかりました",
|
||||||
"noGroups": "現在の一致基準では重複グループが見つかりませんでした",
|
"noGroups": "現在の一致基準では重複グループが見つかりませんでした",
|
||||||
"keepLatest": "最新バージョンを保持",
|
"keepLatest": "最新バージョンを保持",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "{count}개의 중복 그룹 발견",
|
"found": "{count}개의 중복 그룹 발견",
|
||||||
"noGroups": "현재 일치 기준으로 중복 그룹을 찾을 수 없습니다",
|
"noGroups": "현재 일치 기준으로 중복 그룹을 찾을 수 없습니다",
|
||||||
"keepLatest": "최신 버전 유지",
|
"keepLatest": "최신 버전 유지",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "Найдено {count} групп дубликатов",
|
"found": "Найдено {count} групп дубликатов",
|
||||||
"noGroups": "Дубликатов с текущим критерием не найдено",
|
"noGroups": "Дубликатов с текущим критерием не найдено",
|
||||||
"keepLatest": "Оставить последние версии",
|
"keepLatest": "Оставить последние версии",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "发现 {count} 个重复组",
|
"found": "发现 {count} 个重复组",
|
||||||
"noGroups": "按当前判重依据未找到重复组",
|
"noGroups": "按当前判重依据未找到重复组",
|
||||||
"keepLatest": "保留最新版本",
|
"keepLatest": "保留最新版本",
|
||||||
|
|||||||
@@ -963,6 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
|
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
||||||
"found": "發現 {count} 組重複項",
|
"found": "發現 {count} 組重複項",
|
||||||
"noGroups": "按目前判重依據未找到重複組",
|
"noGroups": "按目前判重依據未找到重複組",
|
||||||
"keepLatest": "保留最新版本",
|
"keepLatest": "保留最新版本",
|
||||||
|
|||||||
@@ -618,16 +618,31 @@ class RecipeQueryHandler:
|
|||||||
include_prompt=include_prompt
|
include_prompt=include_prompt
|
||||||
)
|
)
|
||||||
url_groups = await recipe_scanner.find_duplicate_recipes_by_source()
|
url_groups = await recipe_scanner.find_duplicate_recipes_by_source()
|
||||||
|
|
||||||
|
# Assemble the response directly from the cached recipe summaries.
|
||||||
|
# Resolving each id via get_recipe_by_id would re-read every recipe
|
||||||
|
# JSON from disk — thousands of blocking reads on the event loop
|
||||||
|
# for large libraries — while all required fields already live in
|
||||||
|
# the cache.
|
||||||
|
cache = await recipe_scanner.get_cached_data()
|
||||||
|
recipes_by_id = {
|
||||||
|
str(recipe.get("id", "")): recipe for recipe in cache.raw_data
|
||||||
|
}
|
||||||
|
|
||||||
response_data = []
|
response_data = []
|
||||||
|
|
||||||
for fingerprint, recipe_ids in fingerprint_groups.items():
|
def append_groups(
|
||||||
if len(recipe_ids) <= 1:
|
groups: Dict[str, List[Any]], group_type: str
|
||||||
continue
|
) -> None:
|
||||||
|
for group_key, recipe_ids in groups.items():
|
||||||
|
if len(recipe_ids) <= 1:
|
||||||
|
continue
|
||||||
|
|
||||||
recipes = []
|
recipes = []
|
||||||
for recipe_id in recipe_ids:
|
for recipe_id in recipe_ids:
|
||||||
recipe = await recipe_scanner.get_recipe_by_id(recipe_id)
|
recipe = recipes_by_id.get(str(recipe_id))
|
||||||
if recipe:
|
if recipe is None:
|
||||||
|
continue
|
||||||
recipes.append(
|
recipes.append(
|
||||||
{
|
{
|
||||||
"id": recipe.get("id"),
|
"id": recipe.get("id"),
|
||||||
@@ -642,55 +657,23 @@ class RecipeQueryHandler:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(recipes) >= 2:
|
if len(recipes) >= 2:
|
||||||
recipes.sort(
|
recipes.sort(
|
||||||
key=lambda entry: entry.get("modified", 0), reverse=True
|
key=lambda entry: entry.get("modified") or 0,
|
||||||
)
|
reverse=True,
|
||||||
response_data.append(
|
)
|
||||||
{
|
response_data.append(
|
||||||
"type": "fingerprint",
|
|
||||||
"key": f"g-{len(response_data) + 1}",
|
|
||||||
"fingerprint": fingerprint,
|
|
||||||
"count": len(recipes),
|
|
||||||
"recipes": recipes,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for url, recipe_ids in url_groups.items():
|
|
||||||
if len(recipe_ids) <= 1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
recipes = []
|
|
||||||
for recipe_id in recipe_ids:
|
|
||||||
recipe = await recipe_scanner.get_recipe_by_id(recipe_id)
|
|
||||||
if recipe:
|
|
||||||
recipes.append(
|
|
||||||
{
|
{
|
||||||
"id": recipe.get("id"),
|
"type": group_type,
|
||||||
"title": recipe.get("title"),
|
"key": f"g-{len(response_data) + 1}",
|
||||||
"file_url": recipe.get("file_url")
|
"fingerprint": group_key,
|
||||||
or self._format_recipe_file_url(
|
"count": len(recipes),
|
||||||
recipe.get("file_path", "")
|
"recipes": recipes,
|
||||||
),
|
|
||||||
"modified": recipe.get("modified"),
|
|
||||||
"created_date": recipe.get("created_date"),
|
|
||||||
"lora_count": len(recipe.get("loras", [])),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(recipes) >= 2:
|
append_groups(fingerprint_groups, "fingerprint")
|
||||||
recipes.sort(
|
append_groups(url_groups, "source_path")
|
||||||
key=lambda entry: entry.get("modified", 0), reverse=True
|
|
||||||
)
|
|
||||||
response_data.append(
|
|
||||||
{
|
|
||||||
"type": "source_path",
|
|
||||||
"key": f"g-{len(response_data) + 1}",
|
|
||||||
"fingerprint": url,
|
|
||||||
"count": len(recipes),
|
|
||||||
"recipes": recipes,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
response_data.sort(key=lambda entry: entry["count"], reverse=True)
|
response_data.sort(key=lambda entry: entry["count"], reverse=True)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export class DuplicatesManager {
|
|||||||
this.duplicateGroups = [];
|
this.duplicateGroups = [];
|
||||||
this.inDuplicateMode = false;
|
this.inDuplicateMode = false;
|
||||||
this.selectedForDeletion = new Set();
|
this.selectedForDeletion = new Set();
|
||||||
|
this._isFindingDuplicates = false;
|
||||||
this._initPromptMatchToggle();
|
this._initPromptMatchToggle();
|
||||||
this._initHelpTooltip();
|
this._initHelpTooltip();
|
||||||
}
|
}
|
||||||
@@ -87,6 +88,19 @@ export class DuplicatesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findDuplicates() {
|
async findDuplicates() {
|
||||||
|
// Guard against re-entry: the scan can take a while on large
|
||||||
|
// libraries, and repeated clicks would pile up identical requests
|
||||||
|
// on the backend.
|
||||||
|
if (this._isFindingDuplicates) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._isFindingDuplicates = true;
|
||||||
|
const triggerButton = document.querySelector('[data-action="find-duplicates"]');
|
||||||
|
if (triggerButton) {
|
||||||
|
triggerButton.disabled = true;
|
||||||
|
triggerButton.classList.add('loading');
|
||||||
|
}
|
||||||
|
state.loadingManager?.showSimpleLoading(translate('recipes.duplicates.finding'));
|
||||||
try {
|
try {
|
||||||
const includePrompt = this._getPromptMatchPreference();
|
const includePrompt = this._getPromptMatchPreference();
|
||||||
const endpoint = includePrompt
|
const endpoint = includePrompt
|
||||||
@@ -96,14 +110,14 @@ export class DuplicatesManager {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to find duplicates');
|
throw new Error('Failed to find duplicates');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
throw new Error(data.error || 'Unknown error finding duplicates');
|
throw new Error(data.error || 'Unknown error finding duplicates');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.duplicateGroups = data.duplicate_groups || [];
|
this.duplicateGroups = data.duplicate_groups || [];
|
||||||
|
|
||||||
if (this.duplicateGroups.length === 0) {
|
if (this.duplicateGroups.length === 0) {
|
||||||
showToast('toast.duplicates.noDuplicatesFound', { type: 'recipes' }, 'info');
|
showToast('toast.duplicates.noDuplicatesFound', { type: 'recipes' }, 'info');
|
||||||
// Keep (or enter) the duplicates view when the user is tuning
|
// Keep (or enter) the duplicates view when the user is tuning
|
||||||
@@ -115,13 +129,20 @@ export class DuplicatesManager {
|
|||||||
this.enterDuplicateMode();
|
this.enterDuplicateMode();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.enterDuplicateMode();
|
this.enterDuplicateMode();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error finding duplicates:', error);
|
console.error('Error finding duplicates:', error);
|
||||||
showToast('toast.duplicates.findFailed', { message: error.message }, 'error');
|
showToast('toast.duplicates.findFailed', { message: error.message }, 'error');
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
this._isFindingDuplicates = false;
|
||||||
|
if (triggerButton) {
|
||||||
|
triggerButton.disabled = false;
|
||||||
|
triggerButton.classList.remove('loading');
|
||||||
|
}
|
||||||
|
state.loadingManager?.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2003,10 +2003,10 @@ async def test_find_duplicates_defaults_to_fingerprint_only(
|
|||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
harness.scanner.recipes = {
|
harness.scanner.cached_raw = [
|
||||||
"r1": {"id": "r1", "title": "One", "modified": 100},
|
{"id": "r1", "title": "One", "modified": 100},
|
||||||
"r2": {"id": "r2", "title": "Two", "modified": 200},
|
{"id": "r2", "title": "Two", "modified": 200},
|
||||||
}
|
]
|
||||||
harness.scanner.duplicate_groups_override = {"abc:0.8": ["r1", "r2"]}
|
harness.scanner.duplicate_groups_override = {"abc:0.8": ["r1", "r2"]}
|
||||||
harness.scanner.duplicate_source_groups_override = {}
|
harness.scanner.duplicate_source_groups_override = {}
|
||||||
|
|
||||||
@@ -2028,12 +2028,12 @@ async def test_find_duplicates_forwards_include_prompt_and_assigns_unique_keys(
|
|||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
harness.scanner.recipes = {
|
harness.scanner.cached_raw = [
|
||||||
"r1": {"id": "r1", "title": "One", "modified": 100},
|
{"id": "r1", "title": "One", "modified": 100},
|
||||||
"r2": {"id": "r2", "title": "Two", "modified": 200},
|
{"id": "r2", "title": "Two", "modified": 200},
|
||||||
"r3": {"id": "r3", "title": "Three", "modified": 300},
|
{"id": "r3", "title": "Three", "modified": 300},
|
||||||
"r4": {"id": "r4", "title": "Four", "modified": 400},
|
{"id": "r4", "title": "Four", "modified": 400},
|
||||||
}
|
]
|
||||||
harness.scanner.duplicate_groups_override = {"abc:0.8\x1fa girl": ["r1", "r2"]}
|
harness.scanner.duplicate_groups_override = {"abc:0.8\x1fa girl": ["r1", "r2"]}
|
||||||
harness.scanner.duplicate_source_groups_override = {
|
harness.scanner.duplicate_source_groups_override = {
|
||||||
"civitai.com/images/9": ["r3", "r4"]
|
"civitai.com/images/9": ["r3", "r4"]
|
||||||
|
|||||||
Reference in New Issue
Block a user