mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
test(routes): cover snake case model id payload
This commit is contained in:
@@ -443,6 +443,7 @@
|
||||
"setContentRating": "Inhaltsbewertung für alle festlegen",
|
||||
"copyAll": "Alle Syntax kopieren",
|
||||
"refreshAll": "Alle Metadaten aktualisieren",
|
||||
"checkUpdates": "Auswahl auf Updates prüfen",
|
||||
"moveAll": "Alle in Ordner verschieben",
|
||||
"autoOrganize": "Automatisch organisieren",
|
||||
"deleteAll": "Alle Modelle löschen",
|
||||
@@ -1206,6 +1207,12 @@
|
||||
"bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt",
|
||||
"bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen",
|
||||
"bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden",
|
||||
"bulkUpdatesChecking": "Ausgewählte {type}-Modelle werden auf Updates geprüft...",
|
||||
"bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar",
|
||||
"bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden",
|
||||
"bulkUpdatesMissing": "Ausgewählte {type}-Modelle sind nicht mit Civitai-Updates verknüpft",
|
||||
"bulkUpdatesPartialMissing": "{missing} ausgewählte {type}-Modelle ohne Civitai-Verknüpfung übersprungen",
|
||||
"bulkUpdatesFailed": "Updates für ausgewählte {type}-Modelle konnten nicht geprüft werden: {message}",
|
||||
"invalidCharactersRemoved": "Ungültige Zeichen aus Dateiname entfernt",
|
||||
"filenameCannotBeEmpty": "Dateiname darf nicht leer sein",
|
||||
"renameFailed": "Fehler beim Umbenennen der Datei: {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "Set Content Rating for Selected",
|
||||
"copyAll": "Copy Selected Syntax",
|
||||
"refreshAll": "Refresh Selected Metadata",
|
||||
"checkUpdates": "Check Updates for Selected",
|
||||
"moveAll": "Move Selected to Folder",
|
||||
"autoOrganize": "Auto-Organize Selected",
|
||||
"deleteAll": "Delete Selected Models",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "Set content rating to {level} for {count} model(s)",
|
||||
"bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed",
|
||||
"bulkContentRatingFailed": "Failed to update content rating for selected models",
|
||||
"bulkUpdatesChecking": "Checking selected {type}(s) for updates...",
|
||||
"bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)",
|
||||
"bulkUpdatesNone": "No updates found for selected {type}(s)",
|
||||
"bulkUpdatesMissing": "Selected {type}(s) are not linked to Civitai updates",
|
||||
"bulkUpdatesPartialMissing": "Skipped {missing} selected {type}(s) without Civitai links",
|
||||
"bulkUpdatesFailed": "Failed to check updates for selected {type}(s): {message}",
|
||||
"invalidCharactersRemoved": "Invalid characters removed from filename",
|
||||
"filenameCannotBeEmpty": "File name cannot be empty",
|
||||
"renameFailed": "Failed to rename file: {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "Establecer clasificación de contenido para todos",
|
||||
"copyAll": "Copiar toda la sintaxis",
|
||||
"refreshAll": "Actualizar todos los metadatos",
|
||||
"checkUpdates": "Comprobar actualizaciones para la selección",
|
||||
"moveAll": "Mover todos a carpeta",
|
||||
"autoOrganize": "Auto-organizar seleccionados",
|
||||
"deleteAll": "Eliminar todos los modelos",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "Clasificación de contenido establecida en {level} para {count} modelo(s)",
|
||||
"bulkContentRatingPartial": "Clasificación de contenido establecida en {level} para {success} modelo(s), {failed} fallaron",
|
||||
"bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados",
|
||||
"bulkUpdatesChecking": "Comprobando actualizaciones para {type} seleccionados...",
|
||||
"bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados",
|
||||
"bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados",
|
||||
"bulkUpdatesMissing": "Los {type} seleccionados no están vinculados a actualizaciones de Civitai",
|
||||
"bulkUpdatesPartialMissing": "Se omitieron {missing} {type} seleccionados sin enlace de Civitai",
|
||||
"bulkUpdatesFailed": "Error al comprobar actualizaciones para los {type} seleccionados: {message}",
|
||||
"invalidCharactersRemoved": "Caracteres inválidos eliminados del nombre de archivo",
|
||||
"filenameCannotBeEmpty": "El nombre de archivo no puede estar vacío",
|
||||
"renameFailed": "Error al renombrar archivo: {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "Définir la classification du contenu pour tous",
|
||||
"copyAll": "Copier toute la syntaxe",
|
||||
"refreshAll": "Actualiser toutes les métadonnées",
|
||||
"checkUpdates": "Vérifier les mises à jour pour la sélection",
|
||||
"moveAll": "Déplacer tout vers un dossier",
|
||||
"autoOrganize": "Auto-organiser la sélection",
|
||||
"deleteAll": "Supprimer tous les modèles",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "Classification du contenu définie sur {level} pour {count} modèle(s)",
|
||||
"bulkContentRatingPartial": "Classification du contenu définie sur {level} pour {success} modèle(s), {failed} échec(s)",
|
||||
"bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés",
|
||||
"bulkUpdatesChecking": "Vérification des mises à jour pour les {type} sélectionnés...",
|
||||
"bulkUpdatesSuccess": "Mises à jour disponibles pour {count} {type} sélectionnés",
|
||||
"bulkUpdatesNone": "Aucune mise à jour trouvée pour les {type} sélectionnés",
|
||||
"bulkUpdatesMissing": "Les {type} sélectionnés ne sont pas liés aux mises à jour Civitai",
|
||||
"bulkUpdatesPartialMissing": "{missing} {type} sélectionnés sans lien Civitai ignorés",
|
||||
"bulkUpdatesFailed": "Échec de la vérification des mises à jour pour les {type} sélectionnés : {message}",
|
||||
"invalidCharactersRemoved": "Caractères invalides supprimés du nom de fichier",
|
||||
"filenameCannotBeEmpty": "Le nom de fichier ne peut pas être vide",
|
||||
"renameFailed": "Échec du renommage du fichier : {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "הגדר דירוג תוכן לכל המודלים",
|
||||
"copyAll": "העתק את כל התחבירים",
|
||||
"refreshAll": "רענן את כל המטא-דאטה",
|
||||
"checkUpdates": "בדוק עדכונים לבחירה",
|
||||
"moveAll": "העבר הכל לתיקייה",
|
||||
"autoOrganize": "ארגן אוטומטית נבחרים",
|
||||
"deleteAll": "מחק את כל המודלים",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים",
|
||||
"bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו",
|
||||
"bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל",
|
||||
"bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...",
|
||||
"bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו",
|
||||
"bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו",
|
||||
"bulkUpdatesMissing": "ה-{type} שנבחרו אינם מקושרים לעדכוני Civitai",
|
||||
"bulkUpdatesPartialMissing": "דילג על {missing} {type} שנבחרו ללא קישור Civitai",
|
||||
"bulkUpdatesFailed": "בדיקת העדכונים עבור {type} שנבחרו נכשלה: {message}",
|
||||
"invalidCharactersRemoved": "תווים לא חוקיים הוסרו משם הקובץ",
|
||||
"filenameCannotBeEmpty": "שם הקובץ אינו יכול להיות ריק",
|
||||
"renameFailed": "שינוי שם הקובץ נכשל: {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "すべてのモデルのコンテンツレーティングを設定",
|
||||
"copyAll": "すべての構文をコピー",
|
||||
"refreshAll": "すべてのメタデータを更新",
|
||||
"checkUpdates": "選択項目の更新を確認",
|
||||
"moveAll": "すべてをフォルダに移動",
|
||||
"autoOrganize": "自動整理を実行",
|
||||
"deleteAll": "すべてのモデルを削除",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました",
|
||||
"bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました",
|
||||
"bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした",
|
||||
"bulkUpdatesChecking": "選択された{type}の更新を確認しています...",
|
||||
"bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります",
|
||||
"bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした",
|
||||
"bulkUpdatesMissing": "選択された{type}はCivitaiの更新にリンクされていません",
|
||||
"bulkUpdatesPartialMissing": "Civitaiリンクがない{missing} 件の{type}をスキップしました",
|
||||
"bulkUpdatesFailed": "選択された{type}の更新確認に失敗しました: {message}",
|
||||
"invalidCharactersRemoved": "ファイル名から無効な文字が削除されました",
|
||||
"filenameCannotBeEmpty": "ファイル名を空にすることはできません",
|
||||
"renameFailed": "ファイル名の変更に失敗しました:{message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "모든 모델에 콘텐츠 등급 설정",
|
||||
"copyAll": "모든 문법 복사",
|
||||
"refreshAll": "모든 메타데이터 새로고침",
|
||||
"checkUpdates": "선택 항목 업데이트 확인",
|
||||
"moveAll": "모두 폴더로 이동",
|
||||
"autoOrganize": "자동 정리 선택",
|
||||
"deleteAll": "모든 모델 삭제",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다",
|
||||
"bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다",
|
||||
"bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다",
|
||||
"bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...",
|
||||
"bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다",
|
||||
"bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다",
|
||||
"bulkUpdatesMissing": "선택한 {type}이 Civitai 업데이트에 연결되어 있지 않습니다",
|
||||
"bulkUpdatesPartialMissing": "Civitai 링크가 없는 {missing}개의 {type}을 건너뛰었습니다",
|
||||
"bulkUpdatesFailed": "선택한 {type}의 업데이트 확인에 실패했습니다: {message}",
|
||||
"invalidCharactersRemoved": "파일명에서 잘못된 문자가 제거되었습니다",
|
||||
"filenameCannotBeEmpty": "파일 이름은 비어있을 수 없습니다",
|
||||
"renameFailed": "파일 이름 변경 실패: {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "Установить рейтинг контента для всех",
|
||||
"copyAll": "Копировать весь синтаксис",
|
||||
"refreshAll": "Обновить все метаданные",
|
||||
"checkUpdates": "Проверить обновления для выбранных",
|
||||
"moveAll": "Переместить все в папку",
|
||||
"autoOrganize": "Автоматически организовать выбранные",
|
||||
"deleteAll": "Удалить все модели",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)",
|
||||
"bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось",
|
||||
"bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей",
|
||||
"bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...",
|
||||
"bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}",
|
||||
"bulkUpdatesNone": "Обновления для выбранных {type} не найдены",
|
||||
"bulkUpdatesMissing": "Выбранные {type} не привязаны к обновлениям Civitai",
|
||||
"bulkUpdatesPartialMissing": "Пропущено {missing} выбранных {type} без привязки Civitai",
|
||||
"bulkUpdatesFailed": "Не удалось проверить обновления для выбранных {type}: {message}",
|
||||
"invalidCharactersRemoved": "Недопустимые символы удалены из имени файла",
|
||||
"filenameCannotBeEmpty": "Имя файла не может быть пустым",
|
||||
"renameFailed": "Не удалось переименовать файл: {message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "为所选中设置内容评级",
|
||||
"copyAll": "复制所选中语法",
|
||||
"refreshAll": "刷新所选中元数据",
|
||||
"checkUpdates": "检查所选更新",
|
||||
"moveAll": "移动所选中到文件夹",
|
||||
"autoOrganize": "自动整理所选模型",
|
||||
"deleteAll": "删除选中模型",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}",
|
||||
"bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败",
|
||||
"bulkContentRatingFailed": "未能更新所选模型的内容评级",
|
||||
"bulkUpdatesChecking": "正在检查所选 {type} 的更新...",
|
||||
"bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新",
|
||||
"bulkUpdatesNone": "所选 {type} 未发现更新",
|
||||
"bulkUpdatesMissing": "所选 {type} 未关联 Civitai 更新",
|
||||
"bulkUpdatesPartialMissing": "已跳过 {missing} 个未关联 Civitai 的所选 {type}",
|
||||
"bulkUpdatesFailed": "检查所选 {type} 的更新失败:{message}",
|
||||
"invalidCharactersRemoved": "文件名中的无效字符已移除",
|
||||
"filenameCannotBeEmpty": "文件名不能为空",
|
||||
"renameFailed": "重命名文件失败:{message}",
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
"setContentRating": "為全部設定內容分級",
|
||||
"copyAll": "複製全部語法",
|
||||
"refreshAll": "刷新全部 metadata",
|
||||
"checkUpdates": "檢查所選更新",
|
||||
"moveAll": "全部移動到資料夾",
|
||||
"autoOrganize": "自動整理所選模型",
|
||||
"deleteAll": "刪除全部模型",
|
||||
@@ -1205,6 +1206,12 @@
|
||||
"bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}",
|
||||
"bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗",
|
||||
"bulkContentRatingFailed": "無法更新所選模型的內容分級",
|
||||
"bulkUpdatesChecking": "正在檢查所選 {type} 的更新...",
|
||||
"bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新",
|
||||
"bulkUpdatesNone": "所選 {type} 未找到更新",
|
||||
"bulkUpdatesMissing": "所選 {type} 未連結 Civitai 更新",
|
||||
"bulkUpdatesPartialMissing": "已略過 {missing} 個未連結 Civitai 的所選 {type}",
|
||||
"bulkUpdatesFailed": "檢查所選 {type} 更新失敗:{message}",
|
||||
"invalidCharactersRemoved": "已移除檔名中的無效字元",
|
||||
"filenameCannotBeEmpty": "檔案名稱不可為空",
|
||||
"renameFailed": "重新命名檔案失敗:{message}",
|
||||
|
||||
@@ -1048,6 +1048,21 @@ class ModelUpdateHandler:
|
||||
force_refresh = self._parse_bool(request.query.get("force")) or self._parse_bool(
|
||||
payload.get("force")
|
||||
)
|
||||
|
||||
raw_model_ids = payload.get("modelIds")
|
||||
if raw_model_ids is None:
|
||||
raw_model_ids = payload.get("model_ids")
|
||||
|
||||
target_model_ids: list[int] = []
|
||||
if isinstance(raw_model_ids, (list, tuple, set)):
|
||||
for value in raw_model_ids:
|
||||
normalized = self._normalize_model_id(value)
|
||||
if normalized is not None:
|
||||
target_model_ids.append(normalized)
|
||||
|
||||
if target_model_ids:
|
||||
target_model_ids = sorted(set(target_model_ids))
|
||||
|
||||
provider = await self._get_civitai_provider()
|
||||
if provider is None:
|
||||
return web.json_response(
|
||||
@@ -1060,6 +1075,7 @@ class ModelUpdateHandler:
|
||||
self._service.scanner,
|
||||
provider,
|
||||
force_refresh=force_refresh,
|
||||
target_model_ids=target_model_ids or None,
|
||||
)
|
||||
except RateLimitError as exc:
|
||||
return web.json_response(
|
||||
|
||||
@@ -210,15 +210,33 @@ class ModelUpdateService:
|
||||
metadata_provider,
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
target_model_ids: Optional[Sequence[int]] = None,
|
||||
) -> Dict[int, ModelUpdateRecord]:
|
||||
"""Refresh update information for every model present in the cache."""
|
||||
|
||||
local_versions = await self._collect_local_versions(scanner)
|
||||
normalized_targets = (
|
||||
self._normalize_sequence(target_model_ids)
|
||||
if target_model_ids is not None
|
||||
else []
|
||||
)
|
||||
target_filter = normalized_targets or None
|
||||
|
||||
local_versions = await self._collect_local_versions(
|
||||
scanner,
|
||||
target_model_ids=target_filter,
|
||||
)
|
||||
total_models = len(local_versions)
|
||||
if total_models == 0:
|
||||
logger.info(
|
||||
"No %s models found while refreshing update metadata", model_type
|
||||
)
|
||||
if target_filter:
|
||||
logger.info(
|
||||
"No %s models matched requested ids %s while refreshing update metadata",
|
||||
model_type,
|
||||
target_filter,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"No %s models found while refreshing update metadata", model_type
|
||||
)
|
||||
return {}
|
||||
|
||||
logger.info(
|
||||
@@ -616,12 +634,23 @@ class ModelUpdateService:
|
||||
)
|
||||
return aggregated
|
||||
|
||||
async def _collect_local_versions(self, scanner) -> Dict[int, List[int]]:
|
||||
async def _collect_local_versions(
|
||||
self,
|
||||
scanner,
|
||||
*,
|
||||
target_model_ids: Optional[Sequence[int]] = None,
|
||||
) -> Dict[int, List[int]]:
|
||||
cache = await scanner.get_cached_data()
|
||||
mapping: Dict[int, set[int]] = {}
|
||||
if not cache or not getattr(cache, "raw_data", None):
|
||||
return {}
|
||||
|
||||
target_set = None
|
||||
if target_model_ids:
|
||||
target_set = set(target_model_ids)
|
||||
if not target_set:
|
||||
return {}
|
||||
|
||||
for item in cache.raw_data:
|
||||
civitai = item.get("civitai") if isinstance(item, dict) else None
|
||||
if not isinstance(civitai, dict):
|
||||
@@ -630,6 +659,8 @@ class ModelUpdateService:
|
||||
version_id = self._normalize_int(civitai.get("id"))
|
||||
if model_id is None or version_id is None:
|
||||
continue
|
||||
if target_set is not None and model_id not in target_set:
|
||||
continue
|
||||
mapping.setdefault(model_id, set()).add(version_id)
|
||||
|
||||
return {model_id: sorted(ids) for model_id, ids in mapping.items()}
|
||||
|
||||
@@ -569,6 +569,35 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshUpdatesForModels(modelIds, { force = false } = {}) {
|
||||
if (!Array.isArray(modelIds) || modelIds.length === 0) {
|
||||
throw new Error('No model IDs provided');
|
||||
}
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.refreshUpdates, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model_ids: modelIds,
|
||||
force
|
||||
})
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (error) {
|
||||
console.warn('Unable to parse refresh updates response as JSON', error);
|
||||
}
|
||||
|
||||
if (!response.ok || payload?.success !== true) {
|
||||
const message = payload?.error || response.statusText || 'Failed to refresh updates';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async fetchCivitaiVersions(modelId, source = null) {
|
||||
try {
|
||||
let requestUrl = `${this.apiConfig.endpoints.civitaiVersions}/${modelId}`;
|
||||
|
||||
@@ -33,6 +33,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
const sendToWorkflowReplaceItem = this.menu.querySelector('[data-action="send-to-workflow-replace"]');
|
||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||
const checkUpdatesItem = this.menu.querySelector('[data-action="check-updates"]');
|
||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||
@@ -49,6 +50,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (refreshAllItem) {
|
||||
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||
}
|
||||
if (checkUpdatesItem) {
|
||||
checkUpdatesItem.style.display = config.checkUpdates ? 'flex' : 'none';
|
||||
}
|
||||
if (moveAllItem) {
|
||||
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||
}
|
||||
@@ -105,6 +109,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
case 'refresh-all':
|
||||
bulkManager.refreshAllMetadata();
|
||||
break;
|
||||
case 'check-updates':
|
||||
bulkManager.checkUpdatesForSelectedModels();
|
||||
break;
|
||||
case 'move-all':
|
||||
window.moveManager.showMoveModal('bulk');
|
||||
break;
|
||||
|
||||
@@ -436,6 +436,12 @@ export function createModelCard(model, modelType) {
|
||||
const hasUpdateAvailable = Boolean(model.update_available);
|
||||
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
|
||||
|
||||
const civitaiData = model.civitai || {};
|
||||
const modelId = civitaiData?.modelId ?? civitaiData?.model_id;
|
||||
if (modelId !== undefined && modelId !== null && modelId !== '') {
|
||||
card.dataset.modelId = modelId;
|
||||
}
|
||||
|
||||
// LoRA specific data
|
||||
if (modelType === MODEL_TYPES.LORA) {
|
||||
card.dataset.usage_tips = model.usage_tips;
|
||||
|
||||
@@ -34,6 +34,7 @@ export class BulkManager {
|
||||
sendToWorkflow: true,
|
||||
copyAll: true,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
@@ -44,6 +45,7 @@ export class BulkManager {
|
||||
sendToWorkflow: false,
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
@@ -54,6 +56,7 @@ export class BulkManager {
|
||||
sendToWorkflow: false,
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
moveAll: false,
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
@@ -271,14 +274,9 @@ export class BulkManager {
|
||||
} else {
|
||||
card.classList.add('selected');
|
||||
state.selectedModels.add(filepath);
|
||||
|
||||
|
||||
// Cache the metadata for this model
|
||||
const metadataCache = this.getMetadataCache();
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
}
|
||||
|
||||
// Update context menu header if visible
|
||||
@@ -290,7 +288,7 @@ export class BulkManager {
|
||||
getMetadataCache() {
|
||||
const currentType = state.currentPageType;
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
|
||||
// Initialize metadata cache if it doesn't exist
|
||||
if (currentType === MODEL_TYPES.LORA) {
|
||||
if (!state.loraMetadataCache) {
|
||||
@@ -305,6 +303,89 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
|
||||
parseModelId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
updateMetadataCacheFromCard(filepath, card) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataCache = this.getMetadataCache();
|
||||
const existing = metadataCache.get(filepath) || {};
|
||||
const modelId = this.parseModelId(card.dataset.modelId);
|
||||
|
||||
const updated = {
|
||||
...existing,
|
||||
fileName: card.dataset.file_name ?? existing.fileName,
|
||||
usageTips: card.dataset.usage_tips ?? existing.usageTips,
|
||||
modelName: card.dataset.name ?? existing.modelName,
|
||||
};
|
||||
|
||||
if (modelId !== null) {
|
||||
updated.modelId = modelId;
|
||||
}
|
||||
|
||||
metadataCache.set(filepath, updated);
|
||||
}
|
||||
|
||||
escapeAttributeValue(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
getModelIdForFilePath(filePath) {
|
||||
const metadataCache = this.getMetadataCache();
|
||||
const cached = metadataCache.get(filePath);
|
||||
if (cached && typeof cached.modelId === 'number') {
|
||||
return cached.modelId;
|
||||
}
|
||||
|
||||
const escapedPath = this.escapeAttributeValue(filePath);
|
||||
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||
if (!card) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.updateMetadataCacheFromCard(filePath, card);
|
||||
const updated = metadataCache.get(filePath);
|
||||
return updated && typeof updated.modelId === 'number' ? updated.modelId : null;
|
||||
}
|
||||
|
||||
collectSelectedModelIds() {
|
||||
const metadataCache = this.getMetadataCache();
|
||||
const ids = [];
|
||||
let missingCount = 0;
|
||||
|
||||
for (const filepath of state.selectedModels) {
|
||||
const cached = metadataCache.get(filepath);
|
||||
let modelId = cached && typeof cached.modelId === 'number' ? cached.modelId : null;
|
||||
if (modelId === null) {
|
||||
modelId = this.getModelIdForFilePath(filepath);
|
||||
}
|
||||
|
||||
if (typeof modelId === 'number') {
|
||||
ids.push(modelId);
|
||||
} else {
|
||||
missingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueIds = Array.from(new Set(ids));
|
||||
return { ids: uniqueIds, missingCount };
|
||||
}
|
||||
|
||||
applySelectionState() {
|
||||
if (!state.bulkMode) return;
|
||||
|
||||
@@ -312,13 +393,8 @@ export class BulkManager {
|
||||
const filepath = card.dataset.filepath;
|
||||
if (state.selectedModels.has(filepath)) {
|
||||
card.classList.add('selected');
|
||||
|
||||
const metadataCache = this.getMetadataCache();
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
@@ -477,12 +553,14 @@ export class BulkManager {
|
||||
state.virtualScroller.items.forEach(item => {
|
||||
if (item && item.file_path) {
|
||||
state.selectedModels.add(item.file_path);
|
||||
|
||||
|
||||
if (!metadataCache.has(item.file_path)) {
|
||||
const modelId = this.parseModelId(item?.civitai?.modelId);
|
||||
metadataCache.set(item.file_path, {
|
||||
fileName: item.file_name,
|
||||
usageTips: item.usage_tips || '{}',
|
||||
modelName: item.name || item.file_name
|
||||
modelName: item.name || item.file_name,
|
||||
...(modelId !== null ? { modelId } : {})
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -521,12 +599,7 @@ export class BulkManager {
|
||||
if (metadata) {
|
||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
||||
if (card) {
|
||||
metadataCache.set(filepath, {
|
||||
...metadata,
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +614,71 @@ export class BulkManager {
|
||||
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async checkUpdatesForSelectedModels() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentType = state.currentPageType;
|
||||
const currentConfig = MODEL_CONFIG[currentType] || MODEL_CONFIG[MODEL_TYPES.LORA];
|
||||
const typeLabel = (currentConfig?.displayName || 'Model').toLowerCase();
|
||||
|
||||
const { ids: modelIds, missingCount } = this.collectSelectedModelIds();
|
||||
|
||||
if (modelIds.length === 0) {
|
||||
showToast('toast.models.bulkUpdatesMissing', { type: typeLabel }, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (missingCount > 0) {
|
||||
showToast('toast.models.bulkUpdatesPartialMissing', { missing: missingCount, type: typeLabel }, 'info');
|
||||
}
|
||||
|
||||
const apiClient = getModelApiClient();
|
||||
if (!apiClient || typeof apiClient.refreshUpdatesForModels !== 'function') {
|
||||
console.warn('Model API client does not support refreshUpdatesForModels');
|
||||
showToast('toast.models.bulkUpdatesFailed', { type: typeLabel, message: 'Operation not supported' }, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingMessage = translate(
|
||||
'toast.models.bulkUpdatesChecking',
|
||||
{ count: state.selectedModels.size, type: typeLabel },
|
||||
`Checking selected ${typeLabel}(s) for updates...`
|
||||
);
|
||||
state.loadingManager?.showSimpleLoading?.(loadingMessage);
|
||||
|
||||
try {
|
||||
const response = await apiClient.refreshUpdatesForModels(modelIds);
|
||||
const records = Array.isArray(response?.records) ? response.records : [];
|
||||
const updatesCount = records.length;
|
||||
|
||||
if (updatesCount > 0) {
|
||||
showToast('toast.models.bulkUpdatesSuccess', { count: updatesCount, type: typeLabel }, 'success');
|
||||
} else {
|
||||
showToast('toast.models.bulkUpdatesNone', { type: typeLabel }, 'info');
|
||||
}
|
||||
|
||||
await resetAndReload(false);
|
||||
} catch (error) {
|
||||
console.error('Error checking updates for selected models:', error);
|
||||
showToast(
|
||||
'toast.models.bulkUpdatesFailed',
|
||||
{ type: typeLabel, message: error?.message ?? 'Unknown error' },
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
if (state.loadingManager?.hide) {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
if (typeof state.loadingManager?.restoreProgressBar === 'function') {
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showBulkAddTagsModal() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
@@ -1263,15 +1400,11 @@ export class BulkManager {
|
||||
// Add to selection if intersecting
|
||||
newSelection.add(filepath);
|
||||
card.classList.add('selected');
|
||||
|
||||
|
||||
// Cache metadata if not already cached
|
||||
const metadataCache = this.getMetadataCache();
|
||||
if (!metadataCache.has(filepath)) {
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
}
|
||||
} else if (!this.initialSelectedModels.has(filepath)) {
|
||||
// Remove from selection if not intersecting and wasn't initially selected
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<div class="context-menu-item" data-action="refresh-all">
|
||||
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="check-updates">
|
||||
<i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="copy-all">
|
||||
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,22 @@ class DummyUpdateService:
|
||||
self.records = records
|
||||
self.calls = []
|
||||
|
||||
async def refresh_for_model_type(self, model_type, scanner, provider, *, force_refresh=False):
|
||||
async def refresh_for_model_type(
|
||||
self,
|
||||
model_type,
|
||||
scanner,
|
||||
provider,
|
||||
*,
|
||||
force_refresh=False,
|
||||
target_model_ids=None,
|
||||
):
|
||||
self.calls.append(
|
||||
{
|
||||
"model_type": model_type,
|
||||
"scanner": scanner,
|
||||
"provider": provider,
|
||||
"force_refresh": force_refresh,
|
||||
"target_model_ids": target_model_ids,
|
||||
}
|
||||
)
|
||||
return self.records
|
||||
@@ -152,3 +161,106 @@ async def test_refresh_model_updates_filters_records_without_updates():
|
||||
assert call["scanner"] is service.scanner
|
||||
assert call["force_refresh"] is False
|
||||
assert call["provider"] is not None
|
||||
assert call["target_model_ids"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_model_updates_with_target_ids():
|
||||
cache = SimpleNamespace(version_index={})
|
||||
service = DummyService(cache)
|
||||
|
||||
record_with_update = ModelUpdateRecord(
|
||||
model_type="lora",
|
||||
model_id=1,
|
||||
versions=[
|
||||
ModelVersionRecord(
|
||||
version_id=10,
|
||||
name="v1",
|
||||
base_model=None,
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=False,
|
||||
should_ignore=False,
|
||||
)
|
||||
],
|
||||
last_checked_at=None,
|
||||
should_ignore_model=False,
|
||||
)
|
||||
|
||||
update_service = DummyUpdateService({1: record_with_update})
|
||||
|
||||
async def metadata_selector(name):
|
||||
assert name == "civitai_api"
|
||||
return object()
|
||||
|
||||
handler = ModelUpdateHandler(
|
||||
service=service,
|
||||
update_service=update_service,
|
||||
metadata_provider_selector=metadata_selector,
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
class DummyRequest:
|
||||
can_read_body = True
|
||||
query = {}
|
||||
|
||||
async def json(self):
|
||||
return {"modelIds": [1, "2", None]}
|
||||
|
||||
response = await handler.refresh_model_updates(DummyRequest())
|
||||
assert response.status == 200
|
||||
|
||||
call = update_service.calls[0]
|
||||
assert call["target_model_ids"] == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_model_updates_accepts_snake_case_ids():
|
||||
cache = SimpleNamespace(version_index={})
|
||||
service = DummyService(cache)
|
||||
|
||||
record_with_update = ModelUpdateRecord(
|
||||
model_type="lora",
|
||||
model_id=3,
|
||||
versions=[
|
||||
ModelVersionRecord(
|
||||
version_id=30,
|
||||
name="v3",
|
||||
base_model=None,
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=False,
|
||||
should_ignore=False,
|
||||
)
|
||||
],
|
||||
last_checked_at=None,
|
||||
should_ignore_model=False,
|
||||
)
|
||||
|
||||
update_service = DummyUpdateService({3: record_with_update})
|
||||
|
||||
async def metadata_selector(name):
|
||||
assert name == "civitai_api"
|
||||
return object()
|
||||
|
||||
handler = ModelUpdateHandler(
|
||||
service=service,
|
||||
update_service=update_service,
|
||||
metadata_provider_selector=metadata_selector,
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
class DummyRequest:
|
||||
can_read_body = True
|
||||
query = {}
|
||||
|
||||
async def json(self):
|
||||
return {"model_ids": [3, "4", "abc", None]}
|
||||
|
||||
response = await handler.refresh_model_updates(DummyRequest())
|
||||
assert response.status == 200
|
||||
|
||||
call = update_service.calls[0]
|
||||
assert call["target_model_ids"] == [3, 4]
|
||||
|
||||
@@ -143,6 +143,47 @@ async def test_refresh_persists_versions_and_uses_cache(tmp_path):
|
||||
assert provider.bulk_calls == [[1]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_filters_to_requested_models(tmp_path):
|
||||
db_path = tmp_path / "updates.sqlite"
|
||||
service = ModelUpdateService(str(db_path), ttl_seconds=3600)
|
||||
raw_data = [
|
||||
{"civitai": {"modelId": 1, "id": 11}},
|
||||
{"civitai": {"modelId": 2, "id": 21}},
|
||||
]
|
||||
scanner = DummyScanner(raw_data)
|
||||
provider = DummyProvider({"modelVersions": []})
|
||||
|
||||
result = await service.refresh_for_model_type(
|
||||
"lora",
|
||||
scanner,
|
||||
provider,
|
||||
target_model_ids=[2],
|
||||
)
|
||||
|
||||
assert list(result.keys()) == [2]
|
||||
assert provider.bulk_calls == [[2]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_returns_empty_when_targets_missing(tmp_path):
|
||||
db_path = tmp_path / "updates.sqlite"
|
||||
service = ModelUpdateService(str(db_path), ttl_seconds=3600)
|
||||
raw_data = [{"civitai": {"modelId": 1, "id": 11}}]
|
||||
scanner = DummyScanner(raw_data)
|
||||
provider = DummyProvider({"modelVersions": []})
|
||||
|
||||
result = await service.refresh_for_model_type(
|
||||
"lora",
|
||||
scanner,
|
||||
provider,
|
||||
target_model_ids=[5],
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
assert provider.bulk_calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_respects_ignore_flag(tmp_path):
|
||||
db_path = tmp_path / "updates.sqlite"
|
||||
|
||||
Reference in New Issue
Block a user