mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-25 06:51:26 -03:00
feat(versions): add file-variant badge and hide download button for in-library versions (#1058)
This commit is contained in:
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
"downloadTooltip": "Diese Version herunterladen",
|
"downloadTooltip": "Diese Version herunterladen",
|
||||||
"downloadRemainingTooltip": "Verbleibende Dateien dieser Version herunterladen",
|
"downloadChooseFilesTooltip": "Auswählen, welche Dateien heruntergeladen werden sollen",
|
||||||
"downloadEarlyAccessTooltip": "Diese Early-Access-Version von Civitai herunterladen",
|
"downloadEarlyAccessTooltip": "Diese Early-Access-Version von Civitai herunterladen",
|
||||||
"downloadPaidTooltip": "Diese bezahlte Version von Civitai herunterladen",
|
"downloadPaidTooltip": "Diese bezahlte Version von Civitai herunterladen",
|
||||||
"downloadNotAllowedTooltip": "Diese Version ist nur für die On-Site-Generierung auf Civitai verfügbar",
|
"downloadNotAllowedTooltip": "Diese Version ist nur für die On-Site-Generierung auf Civitai verfügbar",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadTooltip": "Download this version",
|
"downloadTooltip": "Download this version",
|
||||||
"downloadRemainingTooltip": "Download remaining files of this version",
|
"downloadChooseFilesTooltip": "Choose which files to download",
|
||||||
"downloadEarlyAccessTooltip": "Download this early access version from Civitai",
|
"downloadEarlyAccessTooltip": "Download this early access version from Civitai",
|
||||||
"downloadPaidTooltip": "Download this paid version from Civitai",
|
"downloadPaidTooltip": "Download this paid version from Civitai",
|
||||||
"downloadNotAllowedTooltip": "This version is only available for on-site generation on Civitai",
|
"downloadNotAllowedTooltip": "This version is only available for on-site generation on Civitai",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
"downloadTooltip": "Descargar esta versión",
|
"downloadTooltip": "Descargar esta versión",
|
||||||
"downloadRemainingTooltip": "Descargar los archivos restantes de esta versión",
|
"downloadChooseFilesTooltip": "Elegir qué archivos descargar",
|
||||||
"downloadEarlyAccessTooltip": "Descargar esta versión de acceso temprano desde Civitai",
|
"downloadEarlyAccessTooltip": "Descargar esta versión de acceso temprano desde Civitai",
|
||||||
"downloadPaidTooltip": "Descargar esta versión de pago desde Civitai",
|
"downloadPaidTooltip": "Descargar esta versión de pago desde Civitai",
|
||||||
"downloadNotAllowedTooltip": "Esta versión solo está disponible para generación en el sitio de Civitai",
|
"downloadNotAllowedTooltip": "Esta versión solo está disponible para generación en el sitio de Civitai",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
"downloadTooltip": "Télécharger cette version",
|
"downloadTooltip": "Télécharger cette version",
|
||||||
"downloadRemainingTooltip": "Télécharger les fichiers restants de cette version",
|
"downloadChooseFilesTooltip": "Choisir les fichiers à télécharger",
|
||||||
"downloadEarlyAccessTooltip": "Télécharger cette version en accès anticipé depuis Civitai",
|
"downloadEarlyAccessTooltip": "Télécharger cette version en accès anticipé depuis Civitai",
|
||||||
"downloadPaidTooltip": "Télécharger cette version payante depuis Civitai",
|
"downloadPaidTooltip": "Télécharger cette version payante depuis Civitai",
|
||||||
"downloadNotAllowedTooltip": "Cette version n'est disponible que pour la génération sur le site Civitai",
|
"downloadNotAllowedTooltip": "Cette version n'est disponible que pour la génération sur le site Civitai",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "הורדה",
|
"download": "הורדה",
|
||||||
"downloadTooltip": "הורד את הגרסה הזו",
|
"downloadTooltip": "הורד את הגרסה הזו",
|
||||||
"downloadRemainingTooltip": "הורד את הקבצים הנותרים של גרסה זו",
|
"downloadChooseFilesTooltip": "בחר אילו קבצים להוריד",
|
||||||
"downloadEarlyAccessTooltip": "הורד את גרסת ה-Early Access הזו מ-Civitai",
|
"downloadEarlyAccessTooltip": "הורד את גרסת ה-Early Access הזו מ-Civitai",
|
||||||
"downloadPaidTooltip": "הורד את הגרסה בתשלום הזו מ-Civitai",
|
"downloadPaidTooltip": "הורד את הגרסה בתשלום הזו מ-Civitai",
|
||||||
"downloadNotAllowedTooltip": "גרסה זו זמינה רק ליצירה באתר Civitai",
|
"downloadNotAllowedTooltip": "גרסה זו זמינה רק ליצירה באתר Civitai",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "ダウンロード",
|
"download": "ダウンロード",
|
||||||
"downloadTooltip": "このバージョンをダウンロード",
|
"downloadTooltip": "このバージョンをダウンロード",
|
||||||
"downloadRemainingTooltip": "このバージョンの残りのファイルをダウンロード",
|
"downloadChooseFilesTooltip": "ダウンロードするファイルを選択",
|
||||||
"downloadEarlyAccessTooltip": "Civitai からこの早期アクセス版をダウンロード",
|
"downloadEarlyAccessTooltip": "Civitai からこの早期アクセス版をダウンロード",
|
||||||
"downloadPaidTooltip": "Civitai からこの有料バージョンをダウンロード",
|
"downloadPaidTooltip": "Civitai からこの有料バージョンをダウンロード",
|
||||||
"downloadNotAllowedTooltip": "このバージョンはCivitaiサイト内でのみ利用可能で、ダウンロードはできません",
|
"downloadNotAllowedTooltip": "このバージョンはCivitaiサイト内でのみ利用可能で、ダウンロードはできません",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "다운로드",
|
"download": "다운로드",
|
||||||
"downloadTooltip": "이 버전 다운로드",
|
"downloadTooltip": "이 버전 다운로드",
|
||||||
"downloadRemainingTooltip": "이 버전의 나머지 파일 다운로드",
|
"downloadChooseFilesTooltip": "다운로드할 파일 선택",
|
||||||
"downloadEarlyAccessTooltip": "Civitai에서 이 얼리 액세스 버전 다운로드",
|
"downloadEarlyAccessTooltip": "Civitai에서 이 얼리 액세스 버전 다운로드",
|
||||||
"downloadPaidTooltip": "Civitai에서 이 유료 버전 다운로드",
|
"downloadPaidTooltip": "Civitai에서 이 유료 버전 다운로드",
|
||||||
"downloadNotAllowedTooltip": "이 버전은 Civitai 사이트 내에서만 사용 가능하며 다운로드할 수 없습니다",
|
"downloadNotAllowedTooltip": "이 버전은 Civitai 사이트 내에서만 사용 가능하며 다운로드할 수 없습니다",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "Скачать",
|
"download": "Скачать",
|
||||||
"downloadTooltip": "Скачать эту версию",
|
"downloadTooltip": "Скачать эту версию",
|
||||||
"downloadRemainingTooltip": "Скачать оставшиеся файлы этой версии",
|
"downloadChooseFilesTooltip": "Выбрать файлы для скачивания",
|
||||||
"downloadEarlyAccessTooltip": "Скачать эту версию раннего доступа с Civitai",
|
"downloadEarlyAccessTooltip": "Скачать эту версию раннего доступа с Civitai",
|
||||||
"downloadPaidTooltip": "Скачать эту платную версию с Civitai",
|
"downloadPaidTooltip": "Скачать эту платную версию с Civitai",
|
||||||
"downloadNotAllowedTooltip": "Эта версия доступна только для генерации на сайте Civitai",
|
"downloadNotAllowedTooltip": "Эта версия доступна только для генерации на сайте Civitai",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"downloadTooltip": "下载此版本",
|
"downloadTooltip": "下载此版本",
|
||||||
"downloadRemainingTooltip": "下载此版本的剩余文件",
|
"downloadChooseFilesTooltip": "选择要下载的文件",
|
||||||
"downloadEarlyAccessTooltip": "从 Civitai 下载此抢先体验版本",
|
"downloadEarlyAccessTooltip": "从 Civitai 下载此抢先体验版本",
|
||||||
"downloadPaidTooltip": "从 Civitai 下载此付费版本",
|
"downloadPaidTooltip": "从 Civitai 下载此付费版本",
|
||||||
"downloadNotAllowedTooltip": "此版本仅在 Civitai 站内可用,无法下载",
|
"downloadNotAllowedTooltip": "此版本仅在 Civitai 站内可用,无法下载",
|
||||||
|
|||||||
+1
-1
@@ -1616,7 +1616,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"download": "下載",
|
"download": "下載",
|
||||||
"downloadTooltip": "下載此版本",
|
"downloadTooltip": "下載此版本",
|
||||||
"downloadRemainingTooltip": "下載此版本的剩餘檔案",
|
"downloadChooseFilesTooltip": "選擇要下載的檔案",
|
||||||
"downloadEarlyAccessTooltip": "從 Civitai 下載此搶先體驗版本",
|
"downloadEarlyAccessTooltip": "從 Civitai 下載此搶先體驗版本",
|
||||||
"downloadPaidTooltip": "從 Civitai 下載此付費版本",
|
"downloadPaidTooltip": "從 Civitai 下載此付費版本",
|
||||||
"downloadNotAllowedTooltip": "此版本僅在 Civitai 站內可用,無法下載",
|
"downloadNotAllowedTooltip": "此版本僅在 Civitai 站內可用,無法下載",
|
||||||
|
|||||||
@@ -3120,6 +3120,9 @@ class ModelUpdateHandler:
|
|||||||
"paidAccess": paid_access_payload,
|
"paidAccess": paid_access_payload,
|
||||||
"filePath": context.get("file_path"),
|
"filePath": context.get("file_path"),
|
||||||
"fileName": context.get("file_name"),
|
"fileName": context.get("file_name"),
|
||||||
|
# Weight-file variant count (None when unknown); lets the UI hide
|
||||||
|
# the download affordance for single-file in-library versions.
|
||||||
|
"fileCount": getattr(version, "file_count", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _build_version_context(
|
async def _build_version_context(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
|||||||
from .errors import RateLimitError, ResourceNotFoundError
|
from .errors import RateLimitError, ResourceNotFoundError
|
||||||
from .settings_manager import get_settings_manager
|
from .settings_manager import get_settings_manager
|
||||||
from ..utils.cache_paths import CacheType, resolve_cache_path_with_migration
|
from ..utils.cache_paths import CacheType, resolve_cache_path_with_migration
|
||||||
|
from ..utils.constants import MODEL_WEIGHT_FILE_TYPES
|
||||||
from ..utils.civitai_utils import rewrite_preview_url
|
from ..utils.civitai_utils import rewrite_preview_url
|
||||||
from ..utils.preview_selection import resolve_mature_threshold, select_preview_media
|
from ..utils.preview_selection import resolve_mature_threshold, select_preview_media
|
||||||
|
|
||||||
@@ -77,6 +78,10 @@ class ModelVersionRecord:
|
|||||||
usage_control: Optional[str] = None # "Download", "Generation", "InternalGeneration"
|
usage_control: Optional[str] = None # "Download", "Generation", "InternalGeneration"
|
||||||
paid_access: Optional[str] = None # JSON string of the CivitAI paidAccess DTO
|
paid_access: Optional[str] = None # JSON string of the CivitAI paidAccess DTO
|
||||||
is_paid: bool = False # True when paidAccess.permanent is True (permanent paid gate)
|
is_paid: bool = False # True when paidAccess.permanent is True (permanent paid gate)
|
||||||
|
# Number of downloadable weight files for the version (None when unknown,
|
||||||
|
# e.g. records persisted before this field existed or locally-synthesized
|
||||||
|
# entries). Mirrors the frontend isModelWeightFile() filter.
|
||||||
|
file_count: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -273,6 +278,7 @@ class ModelUpdateService:
|
|||||||
usage_control TEXT,
|
usage_control TEXT,
|
||||||
paid_access TEXT,
|
paid_access TEXT,
|
||||||
is_paid INTEGER NOT NULL DEFAULT 0,
|
is_paid INTEGER NOT NULL DEFAULT 0,
|
||||||
|
file_count INTEGER,
|
||||||
PRIMARY KEY (model_id, version_id),
|
PRIMARY KEY (model_id, version_id),
|
||||||
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -520,6 +526,10 @@ class ModelUpdateService:
|
|||||||
"ALTER TABLE model_update_versions "
|
"ALTER TABLE model_update_versions "
|
||||||
"ADD COLUMN is_paid INTEGER NOT NULL DEFAULT 0"
|
"ADD COLUMN is_paid INTEGER NOT NULL DEFAULT 0"
|
||||||
),
|
),
|
||||||
|
"file_count": (
|
||||||
|
"ALTER TABLE model_update_versions "
|
||||||
|
"ADD COLUMN file_count INTEGER"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
for column, statement in migrations.items():
|
for column, statement in migrations.items():
|
||||||
@@ -623,6 +633,7 @@ class ModelUpdateService:
|
|||||||
is_early_access INTEGER NOT NULL DEFAULT 0,
|
is_early_access INTEGER NOT NULL DEFAULT 0,
|
||||||
paid_access TEXT,
|
paid_access TEXT,
|
||||||
is_paid INTEGER NOT NULL DEFAULT 0,
|
is_paid INTEGER NOT NULL DEFAULT 0,
|
||||||
|
file_count INTEGER,
|
||||||
PRIMARY KEY (model_id, version_id),
|
PRIMARY KEY (model_id, version_id),
|
||||||
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
@@ -644,6 +655,7 @@ class ModelUpdateService:
|
|||||||
"is_early_access",
|
"is_early_access",
|
||||||
"paid_access",
|
"paid_access",
|
||||||
"is_paid",
|
"is_paid",
|
||||||
|
"file_count",
|
||||||
]
|
]
|
||||||
defaults = {
|
defaults = {
|
||||||
"sort_index": "0",
|
"sort_index": "0",
|
||||||
@@ -658,6 +670,7 @@ class ModelUpdateService:
|
|||||||
"is_early_access": "0",
|
"is_early_access": "0",
|
||||||
"paid_access": "NULL",
|
"paid_access": "NULL",
|
||||||
"is_paid": "0",
|
"is_paid": "0",
|
||||||
|
"file_count": "NULL",
|
||||||
}
|
}
|
||||||
|
|
||||||
select_parts = []
|
select_parts = []
|
||||||
@@ -1504,6 +1517,7 @@ class ModelUpdateService:
|
|||||||
)
|
)
|
||||||
ignore_map = {version.version_id: version.should_ignore for version in existing.versions} if existing else {}
|
ignore_map = {version.version_id: version.should_ignore for version in existing.versions} if existing else {}
|
||||||
preview_map = {version.version_id: version.preview_url for version in existing.versions} if existing else {}
|
preview_map = {version.version_id: version.preview_url for version in existing.versions} if existing else {}
|
||||||
|
file_count_map = {version.version_id: version.file_count for version in existing.versions} if existing else {}
|
||||||
sort_map = {version.version_id: version.sort_index for version in existing.versions} if existing else {}
|
sort_map = {version.version_id: version.sort_index for version in existing.versions} if existing else {}
|
||||||
existing_map = {version.version_id: version for version in existing.versions} if existing else {}
|
existing_map = {version.version_id: version for version in existing.versions} if existing else {}
|
||||||
|
|
||||||
@@ -1528,6 +1542,11 @@ class ModelUpdateService:
|
|||||||
usage_control=remote_version.usage_control,
|
usage_control=remote_version.usage_control,
|
||||||
paid_access=remote_version.paid_access,
|
paid_access=remote_version.paid_access,
|
||||||
is_paid=remote_version.is_paid,
|
is_paid=remote_version.is_paid,
|
||||||
|
file_count=(
|
||||||
|
remote_version.file_count
|
||||||
|
if remote_version.file_count is not None
|
||||||
|
else file_count_map.get(version_id)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1620,6 +1639,7 @@ class ModelUpdateService:
|
|||||||
base_model = _normalize_string(entry.get("baseModel"))
|
base_model = _normalize_string(entry.get("baseModel"))
|
||||||
released_at = _normalize_string(entry.get("publishedAt") or entry.get("createdAt"))
|
released_at = _normalize_string(entry.get("publishedAt") or entry.get("createdAt"))
|
||||||
size_bytes = self._extract_size_bytes(entry.get("files"))
|
size_bytes = self._extract_size_bytes(entry.get("files"))
|
||||||
|
file_count = self._extract_file_count(entry.get("files"))
|
||||||
preview_url = self._extract_preview_url(entry.get("images"))
|
preview_url = self._extract_preview_url(entry.get("images"))
|
||||||
early_access_ends_at = _normalize_string(entry.get("earlyAccessEndsAt"))
|
early_access_ends_at = _normalize_string(entry.get("earlyAccessEndsAt"))
|
||||||
|
|
||||||
@@ -1655,6 +1675,7 @@ class ModelUpdateService:
|
|||||||
usage_control=usage_control,
|
usage_control=usage_control,
|
||||||
paid_access=paid_access_json,
|
paid_access=paid_access_json,
|
||||||
is_paid=is_paid,
|
is_paid=is_paid,
|
||||||
|
file_count=file_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1683,6 +1704,25 @@ class ModelUpdateService:
|
|||||||
return None
|
return None
|
||||||
return {"permanent": permanent, "endsAt": ends_at}
|
return {"permanent": permanent, "endsAt": ends_at}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_file_count(files) -> Optional[int]:
|
||||||
|
"""Count downloadable weight files in a version entry's ``files`` list.
|
||||||
|
|
||||||
|
Returns None when the payload carries no files array (unknown), so
|
||||||
|
callers can distinguish "no weight files" from "no data".
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(files, list):
|
||||||
|
return None
|
||||||
|
count = 0
|
||||||
|
for entry in files:
|
||||||
|
if not isinstance(entry, Mapping):
|
||||||
|
continue
|
||||||
|
entry_type = entry.get("type")
|
||||||
|
if isinstance(entry_type, str) and entry_type in MODEL_WEIGHT_FILE_TYPES:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
def _extract_size_bytes(self, files) -> Optional[int]:
|
def _extract_size_bytes(self, files) -> Optional[int]:
|
||||||
if not isinstance(files, Iterable):
|
if not isinstance(files, Iterable):
|
||||||
return None
|
return None
|
||||||
@@ -1795,7 +1835,7 @@ class ModelUpdateService:
|
|||||||
f"""
|
f"""
|
||||||
SELECT model_id, version_id, sort_index, name, base_model, released_at,
|
SELECT model_id, version_id, sort_index, name, base_model, released_at,
|
||||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||||
is_early_access, usage_control, paid_access, is_paid
|
is_early_access, usage_control, paid_access, is_paid, file_count
|
||||||
FROM model_update_versions
|
FROM model_update_versions
|
||||||
WHERE model_id IN ({placeholders})
|
WHERE model_id IN ({placeholders})
|
||||||
ORDER BY model_id ASC, sort_index ASC, version_id ASC
|
ORDER BY model_id ASC, sort_index ASC, version_id ASC
|
||||||
@@ -1826,6 +1866,7 @@ class ModelUpdateService:
|
|||||||
usage_control=row["usage_control"],
|
usage_control=row["usage_control"],
|
||||||
paid_access=row["paid_access"],
|
paid_access=row["paid_access"],
|
||||||
is_paid=bool(row["is_paid"]),
|
is_paid=bool(row["is_paid"]),
|
||||||
|
file_count=_normalize_int(row["file_count"]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1888,8 +1929,8 @@ class ModelUpdateService:
|
|||||||
INSERT INTO model_update_versions (
|
INSERT INTO model_update_versions (
|
||||||
version_id, model_id, sort_index, name, base_model, released_at,
|
version_id, model_id, sort_index, name, base_model, released_at,
|
||||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||||
is_early_access, usage_control, paid_access, is_paid
|
is_early_access, usage_control, paid_access, is_paid, file_count
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
version.version_id,
|
version.version_id,
|
||||||
@@ -1907,6 +1948,7 @@ class ModelUpdateService:
|
|||||||
version.usage_control,
|
version.usage_control,
|
||||||
paid_access_value,
|
paid_access_value,
|
||||||
1 if version.is_paid else 0,
|
1 if version.is_paid else 0,
|
||||||
|
version.file_count,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -514,6 +514,7 @@
|
|||||||
background: oklch(var(--lora-accent) / 0.18);
|
background: oklch(var(--lora-accent) / 0.18);
|
||||||
color: var(--lora-accent);
|
color: var(--lora-accent);
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: var(--transition-base);
|
transition: var(--transition-base);
|
||||||
|
|||||||
@@ -575,6 +575,15 @@ function renderRow(version, options) {
|
|||||||
const actions = [];
|
const actions = [];
|
||||||
const canDownload = isDownloadAllowed(version);
|
const canDownload = isDownloadAllowed(version);
|
||||||
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
|
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
|
||||||
|
// The Download button always fetches the default (primary) file, keeping
|
||||||
|
// the single-file experience for users who don't care about variants.
|
||||||
|
// In-library versions hide it: their default file already exists locally,
|
||||||
|
// and multi-file versions use the "N files" badge below for the remaining
|
||||||
|
// variants instead (#1058). fileCount is null for records persisted before
|
||||||
|
// the field existed; default to the single-file behavior in that case.
|
||||||
|
const fileCount = typeof version.fileCount === 'number' ? version.fileCount : null;
|
||||||
|
const showDownload = !version.isInLibrary;
|
||||||
|
if (showDownload) {
|
||||||
let downloadTitle;
|
let downloadTitle;
|
||||||
if (!canDownload) {
|
if (!canDownload) {
|
||||||
downloadTitle = translate(
|
downloadTitle = translate(
|
||||||
@@ -582,14 +591,6 @@ function renderRow(version, options) {
|
|||||||
{},
|
{},
|
||||||
'This version is only available for on-site generation on Civitai'
|
'This version is only available for on-site generation on Civitai'
|
||||||
);
|
);
|
||||||
} else if (version.isInLibrary) {
|
|
||||||
// In-library versions may still have undownloaded weight files; the
|
|
||||||
// download modal's file dialog decides what remains (#1058).
|
|
||||||
downloadTitle = translate(
|
|
||||||
'modals.model.versions.actions.downloadRemainingTooltip',
|
|
||||||
{},
|
|
||||||
'Download remaining files of this version'
|
|
||||||
);
|
|
||||||
} else if (isPaidPermanent(version)) {
|
} else if (isPaidPermanent(version)) {
|
||||||
downloadTitle = translate(
|
downloadTitle = translate(
|
||||||
'modals.model.versions.actions.downloadPaidTooltip',
|
'modals.model.versions.actions.downloadPaidTooltip',
|
||||||
@@ -619,6 +620,15 @@ function renderRow(version, options) {
|
|||||||
disabled: !canDownload,
|
disabled: !canDownload,
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-file versions get an explicit entry into the download modal's
|
||||||
|
// file-selection step, mirroring the version step's file badge (#1058).
|
||||||
|
const fileSelectionBadge = fileCount !== null && fileCount > 1
|
||||||
|
? `<button type="button" class="file-select-badge" data-version-files title="${escapeHtml(translate('modals.model.versions.actions.downloadChooseFilesTooltip', {}, 'Choose which files to download'))}">
|
||||||
|
<i class="fas fa-th-list"></i> ${fileCount} ${escapeHtml(translate('modals.download.fileSelection.files', {}, 'files'))} <i class="fas fa-chevron-right badge-arrow"></i>
|
||||||
|
</button>`
|
||||||
|
: '';
|
||||||
if (version.isInLibrary && version.filePath) {
|
if (version.isInLibrary && version.filePath) {
|
||||||
actions.push(buildActionButton(
|
actions.push(buildActionButton(
|
||||||
deleteLabel,
|
deleteLabel,
|
||||||
@@ -696,6 +706,7 @@ function renderRow(version, options) {
|
|||||||
<div class="version-badges">${badges.join('')}</div>
|
<div class="version-badges">${badges.join('')}</div>
|
||||||
<div class="version-meta">
|
<div class="version-meta">
|
||||||
${buildMetaMarkup(version, { showEarlyAccess: true })}
|
${buildMetaMarkup(version, { showEarlyAccess: true })}
|
||||||
|
${fileSelectionBadge}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-actions">
|
<div class="version-actions">
|
||||||
@@ -1429,15 +1440,9 @@ export function initVersionsTab({
|
|||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// In-library versions may still have undownloaded weight files
|
// The Download button only renders for versions not in the library
|
||||||
// (#1058). The tab payload has no per-file state, so open the
|
// and always fetches the default (primary) file. Multi-file
|
||||||
// download modal's file dialog, which refetches the full version
|
// variants are reached through the "N files" badge instead.
|
||||||
// payload and shows what remains.
|
|
||||||
if (version.isInLibrary) {
|
|
||||||
await downloadManager.openFileSelectionForVersion(modelType, modelId, versionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathInfo = await resolveDownloadPathFromCurrentVersion();
|
const pathInfo = await resolveDownloadPathFromCurrentVersion();
|
||||||
const resolveTemplatePath = shouldResolveTemplatePath(version, pathInfo);
|
const resolveTemplatePath = shouldResolveTemplatePath(version, pathInfo);
|
||||||
const success = await downloadManager.downloadVersionWithDefaults(modelType, modelId, versionId, {
|
const success = await downloadManager.downloadVersionWithDefaults(modelType, modelId, versionId, {
|
||||||
@@ -1516,6 +1521,21 @@ export function initVersionsTab({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File-selection badge: enter the download modal's file step directly.
|
||||||
|
// Must run before the row-click navigation below (rows are clickable).
|
||||||
|
const filesBadge = event.target.closest('[data-version-files]');
|
||||||
|
if (filesBadge) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const row = filesBadge.closest('.model-version-row');
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const versionId = Number(row.dataset.versionId);
|
||||||
|
await downloadManager.openFileSelectionForVersion(modelType, modelId, versionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const row = event.target.closest('.model-version-row.is-clickable');
|
const row = event.target.closest('.model-version-row.is-clickable');
|
||||||
const civitaiLink = event.target.closest('.version-civitai-link');
|
const civitaiLink = event.target.closest('.version-civitai-link');
|
||||||
if (civitaiLink) {
|
if (civitaiLink) {
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const downloadVersionWithDefaults = vi.fn();
|
||||||
|
const openFileSelectionForVersion = vi.fn();
|
||||||
|
|
||||||
|
vi.mock(DOWNLOAD_MANAGER_MODULE, () => ({
|
||||||
|
downloadManager: {
|
||||||
|
downloadVersionWithDefaults,
|
||||||
|
openFileSelectionForVersion,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||||
|
showToast: vi.fn(),
|
||||||
|
openCivitaiUrl: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const stateMock = {
|
||||||
|
global: {
|
||||||
|
settings: {
|
||||||
|
autoplay_on_hover: false,
|
||||||
|
version_grouping: 'any',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function buildRecord(versions) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
record: {
|
||||||
|
shouldIgnore: false,
|
||||||
|
inLibraryVersionIds: versions.filter(v => v.isInLibrary).map(v => v.versionId),
|
||||||
|
versions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderVersions(record) {
|
||||||
|
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||||
|
const controller = initVersionsTab({
|
||||||
|
modalId: 'model-versions-modal',
|
||||||
|
modelType: 'loras',
|
||||||
|
modelId: 123,
|
||||||
|
currentVersionId: null,
|
||||||
|
});
|
||||||
|
await controller.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadButtonFor(versionId) {
|
||||||
|
return document.querySelector(
|
||||||
|
`.model-version-row[data-version-id="${versionId}"] [data-version-action="download"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filesBadgeFor(versionId) {
|
||||||
|
return document.querySelector(
|
||||||
|
`.model-version-row[data-version-id="${versionId}"] [data-version-files]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ModelVersionsTab download button visibility', () => {
|
||||||
|
let getModelApiClient;
|
||||||
|
let fetchModelUpdateVersions;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
downloadVersionWithDefaults.mockReset();
|
||||||
|
downloadVersionWithDefaults.mockResolvedValue(true);
|
||||||
|
openFileSelectionForVersion.mockReset();
|
||||||
|
openFileSelectionForVersion.mockResolvedValue(undefined);
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="model-versions-modal">
|
||||||
|
<div id="versions-tab">
|
||||||
|
<div class="model-versions-tab"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
({ getModelApiClient } = await import(API_FACTORY_MODULE));
|
||||||
|
fetchModelUpdateVersions = vi.fn();
|
||||||
|
getModelApiClient.mockReturnValue({
|
||||||
|
fetchModelUpdateVersions,
|
||||||
|
fetchModelRoots: vi.fn(),
|
||||||
|
setModelUpdateIgnore: vi.fn(),
|
||||||
|
setVersionUpdateIgnore: vi.fn(),
|
||||||
|
deleteModel: vi.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the download button for a single-file in-library version', async () => {
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue(buildRecord([
|
||||||
|
{
|
||||||
|
versionId: 10,
|
||||||
|
name: 'v1.0',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: true,
|
||||||
|
shouldIgnore: false,
|
||||||
|
filePath: '/models/loras/file.safetensors',
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
await renderVersions();
|
||||||
|
|
||||||
|
expect(downloadButtonFor(10)).toBeFalsy();
|
||||||
|
expect(filesBadgeFor(10)).toBeFalsy();
|
||||||
|
// The delete affordance must remain for in-library versions.
|
||||||
|
expect(document.querySelector(
|
||||||
|
'.model-version-row[data-version-id="10"] [data-version-action="delete"]'
|
||||||
|
)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the files badge instead of a download button for a multi-file in-library version', async () => {
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue(buildRecord([
|
||||||
|
{
|
||||||
|
versionId: 10,
|
||||||
|
name: 'v1.0',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: true,
|
||||||
|
shouldIgnore: false,
|
||||||
|
filePath: '/models/loras/file.safetensors',
|
||||||
|
fileCount: 3,
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
await renderVersions();
|
||||||
|
|
||||||
|
expect(downloadButtonFor(10)).toBeFalsy();
|
||||||
|
const badge = filesBadgeFor(10);
|
||||||
|
expect(badge).toBeTruthy();
|
||||||
|
expect(badge.textContent).toContain('3 files');
|
||||||
|
expect(badge.getAttribute('title')).toBe('Choose which files to download');
|
||||||
|
|
||||||
|
badge.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
expect(openFileSelectionForVersion).toHaveBeenCalledWith('loras', 123, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the download button when fileCount is unknown for an in-library version', async () => {
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue(buildRecord([
|
||||||
|
{
|
||||||
|
versionId: 10,
|
||||||
|
name: 'v1.0',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: true,
|
||||||
|
shouldIgnore: false,
|
||||||
|
filePath: '/models/loras/file.safetensors',
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
await renderVersions();
|
||||||
|
|
||||||
|
expect(downloadButtonFor(10)).toBeFalsy();
|
||||||
|
expect(filesBadgeFor(10)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the download button for versions not in the library', async () => {
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue(buildRecord([
|
||||||
|
{
|
||||||
|
versionId: 11,
|
||||||
|
name: 'v1.1',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: false,
|
||||||
|
shouldIgnore: false,
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
versionId: 12,
|
||||||
|
name: 'v1.2',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: false,
|
||||||
|
shouldIgnore: false,
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
await renderVersions();
|
||||||
|
|
||||||
|
expect(downloadButtonFor(11)).toBeTruthy();
|
||||||
|
expect(downloadButtonFor(12)).toBeTruthy();
|
||||||
|
// Single-file and unknown-count versions get no files badge.
|
||||||
|
expect(filesBadgeFor(11)).toBeFalsy();
|
||||||
|
expect(filesBadgeFor(12)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the default-file download button and offers the files badge for a multi-file version not in the library', async () => {
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue(buildRecord([
|
||||||
|
{
|
||||||
|
versionId: 11,
|
||||||
|
name: 'v1.1',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: false,
|
||||||
|
shouldIgnore: false,
|
||||||
|
fileCount: 2,
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
await renderVersions();
|
||||||
|
|
||||||
|
// The Download button stays bound to the default (primary) file.
|
||||||
|
const button = downloadButtonFor(11);
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
expect(button.getAttribute('title')).toBe('Download this version');
|
||||||
|
|
||||||
|
button.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
expect(downloadVersionWithDefaults).toHaveBeenCalledWith(
|
||||||
|
'loras', 123, 11,
|
||||||
|
expect.objectContaining({ versionName: 'v1.1' })
|
||||||
|
);
|
||||||
|
expect(openFileSelectionForVersion).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// The badge is the advanced entry into the file-selection step.
|
||||||
|
const badge = filesBadgeFor(11);
|
||||||
|
expect(badge).toBeTruthy();
|
||||||
|
expect(badge.textContent).toContain('2 files');
|
||||||
|
|
||||||
|
badge.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
expect(openFileSelectionForVersion).toHaveBeenCalledWith('loras', 123, 11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the direct default download for a single-file version not in the library', async () => {
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue(buildRecord([
|
||||||
|
{
|
||||||
|
versionId: 11,
|
||||||
|
name: 'v1.1',
|
||||||
|
baseModel: 'Illustrious',
|
||||||
|
isInLibrary: false,
|
||||||
|
shouldIgnore: false,
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
await renderVersions();
|
||||||
|
|
||||||
|
expect(filesBadgeFor(11)).toBeFalsy();
|
||||||
|
downloadButtonFor(11).click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
expect(downloadVersionWithDefaults).toHaveBeenCalledWith(
|
||||||
|
'loras', 123, 11,
|
||||||
|
expect.objectContaining({ versionName: 'v1.1' })
|
||||||
|
);
|
||||||
|
expect(openFileSelectionForVersion).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -693,3 +693,21 @@ async def test_enrich_early_access_details_skips_permanent_paid(monkeypatch):
|
|||||||
enriched_map = {v.version_id: v for v in enriched.versions}
|
enriched_map = {v.version_id: v for v in enriched.versions}
|
||||||
assert enriched_map[200].early_access_ends_at == "2099-01-01T00:00:00.000Z"
|
assert enriched_map[200].early_access_ends_at == "2099-01-01T00:00:00.000Z"
|
||||||
assert enriched_map[100].early_access_ends_at is None
|
assert enriched_map[100].early_access_ends_at is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_serialize_version_includes_file_count():
|
||||||
|
version = ModelVersionRecord(
|
||||||
|
version_id=11, name="v11", base_model=None, released_at=None, size_bytes=None,
|
||||||
|
preview_url=None, is_in_library=True, should_ignore=False, file_count=2,
|
||||||
|
)
|
||||||
|
serialized = ModelUpdateHandler._serialize_version(version, None)
|
||||||
|
assert serialized["fileCount"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_serialize_version_file_count_defaults_to_none():
|
||||||
|
version = ModelVersionRecord(
|
||||||
|
version_id=12, name="v12", base_model=None, released_at=None, size_bytes=None,
|
||||||
|
preview_url=None, is_in_library=False, should_ignore=False,
|
||||||
|
)
|
||||||
|
serialized = ModelUpdateHandler._serialize_version(version, None)
|
||||||
|
assert serialized["fileCount"] is None
|
||||||
|
|||||||
@@ -798,3 +798,126 @@ def test_build_record_from_remote_preserves_paid_fields(tmp_path):
|
|||||||
rebuilt = record.versions[0]
|
rebuilt = record.versions[0]
|
||||||
assert rebuilt.paid_access == '{"permanent": true, "endsAt": null}'
|
assert rebuilt.paid_access == '{"permanent": true, "endsAt": null}'
|
||||||
assert rebuilt.is_paid is True
|
assert rebuilt.is_paid is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_file_count_counts_weight_files(tmp_path):
|
||||||
|
"""file_count counts only weight-type files; a missing files array stays
|
||||||
|
None (unknown) so the UI can distinguish it from "no weight files"."""
|
||||||
|
db_path = tmp_path / "updates.sqlite"
|
||||||
|
service = ModelUpdateService(str(db_path))
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"modelVersions": [
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"files": [
|
||||||
|
{"sizeKB": 100, "type": "Model", "primary": True},
|
||||||
|
{"sizeKB": 10, "type": "Training Data"},
|
||||||
|
{"sizeKB": 50, "type": "Pruned Model"},
|
||||||
|
],
|
||||||
|
"images": [],
|
||||||
|
},
|
||||||
|
{"id": 43, "images": []},
|
||||||
|
{"id": 44, "files": [], "images": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
versions = service._extract_versions(response)
|
||||||
|
assert versions is not None
|
||||||
|
assert versions[0].file_count == 2
|
||||||
|
assert versions[1].file_count is None
|
||||||
|
assert versions[2].file_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_persists_file_count(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": [
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"name": "v1",
|
||||||
|
"baseModel": "SD15",
|
||||||
|
"files": [
|
||||||
|
{"sizeKB": 1024, "type": "Model", "primary": True},
|
||||||
|
{"sizeKB": 2048, "type": "Model"},
|
||||||
|
{"sizeKB": 128, "type": "Training Data"},
|
||||||
|
],
|
||||||
|
"images": [],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.refresh_for_model_type("lora", scanner, provider)
|
||||||
|
record = await service.get_record("lora", 1)
|
||||||
|
|
||||||
|
assert record is not None
|
||||||
|
assert record.versions[0].file_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_record_from_remote_preserves_file_count(tmp_path):
|
||||||
|
"""A remote payload without files data must not clobber the previously
|
||||||
|
persisted file_count; a populated payload wins."""
|
||||||
|
db_path = tmp_path / "updates.sqlite"
|
||||||
|
service = ModelUpdateService(str(db_path))
|
||||||
|
|
||||||
|
existing = make_record(
|
||||||
|
ModelVersionRecord(
|
||||||
|
version_id=7,
|
||||||
|
name="v7",
|
||||||
|
base_model=None,
|
||||||
|
released_at=None,
|
||||||
|
size_bytes=None,
|
||||||
|
preview_url=None,
|
||||||
|
is_in_library=True,
|
||||||
|
should_ignore=False,
|
||||||
|
file_count=3,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
remote_without_count = ModelVersionRecord(
|
||||||
|
version_id=7,
|
||||||
|
name="v7",
|
||||||
|
base_model=None,
|
||||||
|
released_at=None,
|
||||||
|
size_bytes=None,
|
||||||
|
preview_url=None,
|
||||||
|
is_in_library=False,
|
||||||
|
should_ignore=False,
|
||||||
|
file_count=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
record = service._build_record_from_remote(
|
||||||
|
model_type="lora",
|
||||||
|
model_id=999,
|
||||||
|
local_versions=[7],
|
||||||
|
remote_versions=[remote_without_count],
|
||||||
|
existing=existing,
|
||||||
|
timestamp=1.0,
|
||||||
|
)
|
||||||
|
assert record.versions[0].file_count == 3
|
||||||
|
|
||||||
|
remote_with_count = ModelVersionRecord(
|
||||||
|
version_id=7,
|
||||||
|
name="v7",
|
||||||
|
base_model=None,
|
||||||
|
released_at=None,
|
||||||
|
size_bytes=None,
|
||||||
|
preview_url=None,
|
||||||
|
is_in_library=False,
|
||||||
|
should_ignore=False,
|
||||||
|
file_count=1,
|
||||||
|
)
|
||||||
|
record = service._build_record_from_remote(
|
||||||
|
model_type="lora",
|
||||||
|
model_id=999,
|
||||||
|
local_versions=[7],
|
||||||
|
remote_versions=[remote_with_count],
|
||||||
|
existing=existing,
|
||||||
|
timestamp=2.0,
|
||||||
|
)
|
||||||
|
assert record.versions[0].file_count == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user