From 505f212c4db8a36e30ee92bb0ae491d9fa1ac624 Mon Sep 17 00:00:00 2001 From: TBNilles Date: Sun, 7 Jun 2026 16:27:33 -0400 Subject: [PATCH] feat(model-manager): flag Early Access models in CivitAI browse CivitAI Early Access versions require purchased access and otherwise fail with 401. Surface version `availability` from the API as an `early_access` flag (per card and per version), show an amber "EARLY ACCESS" tag on the card, label such entries in the version dropdown, and warn before attempting to download one. Co-Authored-By: Claude Opus 4.8 --- README.md | 3 ++- model-manager/app/registries.py | 14 +++++++++++++- model-manager/app/static/app.js | 29 +++++++++++++++++++++++++---- model-manager/app/static/style.css | 5 +++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 77164e1..6330201 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,8 @@ SparkyUI includes a **StabilityMatrix-style Model Manager** - a lightweight Fast all at once (with confirm). - **Browse CivitAI** - search the CivitAI catalog in a thumbnail grid (filter by type, sort, period, NSFW toggle) and **click a model to download it** - no URL pasting needed. - Multi-version models get a version picker on the card. + Multi-version models get a version picker on the card. **Early Access** versions are + flagged (they require purchased access on CivitAI and otherwise fail with 401). - **Installed Models** - browse what's on disk, grouped by type, with size and delete actions - **Add / Download** - paste a download URL and pick a type; live progress bars - **Direct URLs** - any direct download link diff --git a/model-manager/app/registries.py b/model-manager/app/registries.py index 5254fe6..b83abf5 100644 --- a/model-manager/app/registries.py +++ b/model-manager/app/registries.py @@ -176,6 +176,12 @@ def _pick_thumbnail(version: dict) -> Optional[dict]: return None +def _is_early_access(version: dict) -> bool: + """A version is early-access (download requires purchase/login) when its + availability is anything other than Public.""" + return (version.get("availability") or "Public") != "Public" + + def _to_card(item: dict) -> dict: versions = item.get("modelVersions") or [] v0 = versions[0] if versions else {} @@ -192,8 +198,14 @@ def _to_card(item: dict) -> dict: "thumbnail": thumb.get("url") if thumb else None, "thumbnail_type": thumb.get("type") if thumb else None, "primary_version_id": v0.get("id"), + "early_access": _is_early_access(v0), "versions": [ - {"id": v.get("id"), "name": v.get("name")} for v in versions + { + "id": v.get("id"), + "name": v.get("name"), + "early_access": _is_early_access(v), + } + for v in versions ], } diff --git a/model-manager/app/static/app.js b/model-manager/app/static/app.js index 4932826..3e0fd91 100644 --- a/model-manager/app/static/app.js +++ b/model-manager/app/static/app.js @@ -477,6 +477,12 @@ function renderBrowseCard(m) { n.textContent = "NSFW"; thumb.appendChild(n); } + if (m.early_access) { + const ea = document.createElement("span"); + ea.className = "ea-tag"; + ea.textContent = "EARLY ACCESS"; + thumb.appendChild(ea); + } const body = document.createElement("div"); body.className = "body"; @@ -497,7 +503,8 @@ function renderBrowseCard(m) { for (const v of m.versions) { const o = document.createElement("option"); o.value = v.id; - o.textContent = v.name || `v${v.id}`; + o.textContent = (v.name || `v${v.id}`) + (v.early_access ? " — Early Access" : ""); + o.dataset.ea = v.early_access ? "1" : ""; versionSel.appendChild(o); } foot.appendChild(versionSel); @@ -507,8 +514,15 @@ function renderBrowseCard(m) { btn.className = "btn primary"; btn.textContent = "Download"; btn.addEventListener("click", () => { - const vid = versionSel ? Number(versionSel.value) : m.primary_version_id; - downloadVersion(vid, btn); + let vid, isEa; + if (versionSel) { + vid = Number(versionSel.value); + isEa = versionSel.selectedOptions[0]?.dataset.ea === "1"; + } else { + vid = m.primary_version_id; + isEa = m.early_access; + } + downloadVersion(vid, btn, isEa); }); foot.appendChild(btn); @@ -517,8 +531,15 @@ function renderBrowseCard(m) { return card; } -async function downloadVersion(versionId, btn) { +async function downloadVersion(versionId, btn, isEarlyAccess) { if (!versionId) { toast($("#browseToast"), "No version available", true); return; } + if (isEarlyAccess) { + const ok = await confirmDialog( + "This is an Early Access version. CivitAI only allows downloading it if " + + "your account has purchased/unlocked access — otherwise it will fail with " + + "401. Try anyway?", "Download"); + if (!ok) return; + } btn.disabled = true; const original = btn.textContent; btn.textContent = "Queued ✓"; diff --git a/model-manager/app/static/style.css b/model-manager/app/static/style.css index 1a1d07e..6dd1dca 100644 --- a/model-manager/app/static/style.css +++ b/model-manager/app/static/style.css @@ -242,6 +242,11 @@ code { background: var(--bg-3); padding: 1px 5px; border-radius: 4px; font-size: background: rgba(239,93,107,.85); color: #fff; font-size: 10px; padding: 2px 7px; border-radius: 8px; font-weight: 600; } +.browse-card .thumb .ea-tag { + position: absolute; bottom: 8px; left: 8px; + background: rgba(240,180,0,.92); color: #1a1400; + font-size: 10px; padding: 2px 7px; border-radius: 8px; font-weight: 700; +} .browse-card .body { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; flex: 1; } .browse-card .b-name { font-weight: 600; font-size: 13px; line-height: 1.25; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }