feat(update): track buzz prices and alert below a threshold

CivitAI's public API deliberately omits prices — paidAccess is trimmed to
{permanent, endsAt} because "pricing belongs to the purchase flow" — but the
public model page embeds the site's own model.getById result, including
paidAccess.terms, in its server-rendered payload. That is read anonymously
(no API key, no internal endpoint, no forged Origin), one request per gated
model, so only the ~2% of models that actually carry a gate pay for it.

- optional capture, off by default: price_tracking_enabled,
  price_alert_threshold_buzz (0 = alert on "became free" only) and
  price_check_ttl_hours; prices refresh on their own TTL and immediately when a
  gate changes, and a failed fetch keeps the stored price instead of blanking it
- versions that stop carrying a gate are marked free (persisted gate_lapsed_at)
  and gate transitions are reported as events on the refresh response, so a
  version already in the library can announce that it became free
- price_alert_state plus a price_drop edge event; new
  GET /api/lm/{type}/updates/price-alerts lists what is under the threshold
- versions tab shows the price (effective, with the list price struck through
  and a Blue Buzz note) and a Free Now badge; an update check toasts the
  transitions in one message
- the parser and the alerts query are unit-tested against a trimmed page
  fixture, and every route definition is now asserted to resolve to a handler

Plan, verification notes and the deviations from it are in
docs/plans/paid-model-price-tracking.md.
This commit is contained in:
Will Miao
2026-10-04 08:53:24 +08:00
parent 5f4054265d
commit ad2402724b
31 changed files with 2829 additions and 33 deletions
+22 -1
View File
@@ -201,7 +201,12 @@
"loading": "{type} のアップデートを確認中...",
"success": "{type} のアップデートが {count} 件見つかりました",
"none": "すべての {type} は最新です",
"error": "{type} のアップデート確認に失敗しました: {message}"
"error": "{type} のアップデート確認に失敗しました: {message}",
"gateEvents": {
"becameFree": "[TODO: Translate] {count} version(s) became free",
"priceDrop": "[TODO: Translate] {count} version(s) dropped below your price threshold",
"newGate": "[TODO: Translate] {count} version(s) are now paid"
}
},
"cleanupExampleImages": {
"label": "例画像フォルダをクリーンアップ",
@@ -387,6 +392,7 @@
"downloadPathTemplates": "ダウンロードパステンプレート",
"priorityTags": "優先タグ",
"versionScope": "アップデートフラグ",
"priceTracking": "[TODO: Translate] Buzz Price Tracking",
"exampleImages": "例画像",
"autoOrganize": "自動整理",
"metadata": "メタデータ",
@@ -753,6 +759,16 @@
"label": "有料更新を非表示",
"help": "有効にすると、有料の更新のみがあるモデルには「更新あり」バッジが表示されません"
},
"priceTracking": {
"label": "[TODO: Translate] Buzz Price Tracking",
"help": "[TODO: Translate] CivitAI does not publish prices in its public API, so prices are read from the model page - one extra request per paid model, refreshed on the interval below. Requires an update check to pick up changes.",
"enabled": "[TODO: Translate] Track Buzz prices for paid versions",
"enabledHelp": "[TODO: Translate] Show the price on paid and early access versions, and alert when a price drops below your threshold",
"threshold": "[TODO: Translate] Price alert threshold (Buzz)",
"thresholdHelp": "[TODO: Translate] Alert when a paid version costs this much or less. Use 0 to only be told when a version becomes free.",
"refreshHours": "[TODO: Translate] Price refresh interval (hours)",
"refreshHoursHelp": "[TODO: Translate] How long a stored price stays fresh before it is fetched again"
},
"licenseIcons": {
"useNewStyle": "更新されたライセンスアイコンを使用",
"useNewStyleHelp": "カラーインジケーター付きでライセンス許可を表示(新スタイル)するか、制限のみのアイコンを表示(クラシックスタイル)します。現在のCivitAIデザインを反映しています。"
@@ -2008,6 +2024,11 @@
"earlyAccessTooltip": "このバージョンは現在 CivitAI の早期アクセスが必要です",
"paid": "有料",
"paidTooltip": "このバージョンのダウンロードには支払いが必要です",
"priceSaleTooltip": "[TODO: Translate] On sale, normally {list} Buzz",
"priceBlueBuzzTooltip": "[TODO: Translate] Can also be paid with Blue Buzz",
"priceAlertTooltip": "[TODO: Translate] {price} - below your price alert threshold",
"freeNow": "[TODO: Translate] Free Now",
"freeNowTooltip": "[TODO: Translate] This version no longer requires payment",
"ignored": "無視中",
"ignoredTooltip": "このバージョンの更新通知は無効です",
"onSiteOnly": "サイト内のみ",