feat(update): detect CivitAI paidAccess versions and add hide paid updates (#1060)

CivitAI's PaidAccess cutover deprecated the availability=EarlyAccess and
earlyAccessEndsAt signals; gated versions now report availability=Public
with a paidAccess DTO that LoRA Manager previously ignored, so "Hide
Early Access Updates" missed paid/early-access models and downloads
failed with 401.

Parse and persist paidAccess from model-level, bulk, and by-hash
responses; treat timed paid gates as early access and permanent paid
versions as a distinct is_paid state; add a hide_paid_updates setting
with a "Paid" badge in the versions tab; warn before downloading gated
versions. Includes SQLite migration, i18n for all locales, and
backend/frontend tests.
This commit is contained in:
Will Miao
2026-08-15 18:08:14 +08:00
parent c85b6b64a1
commit ef3e7d7bf4
22 changed files with 630 additions and 31 deletions
+39 -14
View File
@@ -3,6 +3,7 @@
# reportImportCycles, so the ServiceRegistry singleton pattern necessarily forms
# import cycles. Breaking them would require an architectural refactor.
import copy
import json
import logging
import os
import asyncio
@@ -1434,24 +1435,48 @@ class DownloadManager:
# Create directory if it doesn't exist
os.makedirs(save_dir, exist_ok=True)
# Check if this is an early access model
if version_info.get("earlyAccessEndsAt"):
early_access_date = version_info.get("earlyAccessEndsAt", "")
# Convert to a readable date if possible
# Check if this is a paid or early access model
paid_access = version_info.get("paidAccess")
if isinstance(paid_access, str):
# Some providers (e.g. CivArchive fallback) carry the DTO as JSON text
try:
from datetime import datetime
date_obj = datetime.fromisoformat(
early_access_date.replace("Z", "+00:00")
)
formatted_date = date_obj.strftime("%Y-%m-%d")
parsed = json.loads(paid_access)
paid_access = parsed if isinstance(parsed, dict) else None
except (TypeError, ValueError):
paid_access = None
if not isinstance(paid_access, dict):
paid_access = None
# An empty DTO ({"permanent": false, "endsAt": null}) is not a gate
if paid_access and not paid_access.get("permanent") and not paid_access.get("endsAt"):
paid_access = None
if version_info.get("earlyAccessEndsAt") or paid_access:
permanent_paid = bool(paid_access.get("permanent")) if paid_access else False
if permanent_paid:
early_access_msg = (
f"This model requires payment (until {formatted_date}). "
"This model requires payment. Please ensure you have "
"purchased access and are logged in to Civitai."
)
except:
early_access_msg = "This model requires payment. "
else:
early_access_date = version_info.get("earlyAccessEndsAt")
if not early_access_date and paid_access:
early_access_date = paid_access.get("endsAt")
if not early_access_date:
early_access_date = ""
# Convert to a readable date if possible
try:
from datetime import datetime
early_access_msg += "Please ensure you have purchased early access and are logged in to Civitai."
date_obj = datetime.fromisoformat(
early_access_date.replace("Z", "+00:00")
)
formatted_date = date_obj.strftime("%Y-%m-%d")
early_access_msg = (
f"This model requires payment (until {formatted_date}). "
)
except Exception:
early_access_msg = "This model requires payment. "
early_access_msg += "Please ensure you have purchased early access and are logged in to Civitai."
logger.warning(
f"Early access model detected: {version_info.get('name', 'Unknown')}"
)