feat(model-manager): add CivitAI browse view + fix civitai.red / 401

- New "Browse CivitAI" view: thumbnail grid with search, type/sort/period
  filters and NSFW toggle; click a model card to download it (per-card
  version picker for multi-version models). Cursor + page based "Load more".
- Backend: /api/civitai/search and /api/civitai/download endpoints; new
  civitai_search() catalog helper.
- Fix 401 on paste: recognize the civitai.red mirror (and any civitai.*
  host), normalize API calls to civitai.com, and always resolve the
  model-version so type + filename are auto-detected for every CivitAI URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:50:10 -04:00
parent e037a1b062
commit e8115e7aa6
6 changed files with 427 additions and 41 deletions
+33
View File
@@ -169,6 +169,39 @@ async def start_download(body: DownloadIn) -> dict:
return row or {"id": download_id}
# ---- CivitAI catalog browse ----------------------------------------------
@app.get("/api/civitai/search")
async def civitai_search(
query: Optional[str] = None,
types: Optional[str] = None, # comma-separated CivitAI types
sort: Optional[str] = "Most Downloaded",
period: Optional[str] = "AllTime",
nsfw: bool = False,
page: Optional[int] = None,
cursor: Optional[str] = None,
) -> dict:
if not db.get_setting("civitai_api_key"):
raise HTTPException(400, "Set your CivitAI API key in Settings first.")
type_list = [t for t in (types or "").split(",") if t] or None
try:
return await registries.civitai_search(
query=query, types=type_list, sort=sort, period=period,
nsfw=nsfw, page=page, cursor=cursor)
except Exception as exc: # noqa: BLE001 - surface API errors to the UI
raise HTTPException(502, f"CivitAI search failed: {exc}")
class CivitaiDownloadIn(BaseModel):
version_id: int
@app.post("/api/civitai/download")
async def civitai_download(body: CivitaiDownloadIn) -> dict:
url = f"https://civitai.com/api/download/models/{body.version_id}"
return await start_download(DownloadIn(url=url))
@app.delete("/api/downloads/{download_id}")
def delete_download(download_id: int) -> dict:
row = db.get_download(download_id)