mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-06 01:45:31 -03:00
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:
@@ -98,6 +98,7 @@ export function getApiEndpoints(modelType) {
|
||||
modelUpdateVersions: `/api/lm/${modelType}/updates/versions`,
|
||||
ignoreModelUpdate: `/api/lm/${modelType}/updates/ignore`,
|
||||
ignoreVersionUpdate: `/api/lm/${modelType}/updates/ignore-version`,
|
||||
priceAlerts: `/api/lm/${modelType}/updates/price-alerts`,
|
||||
|
||||
// Preview management
|
||||
replacePreview: `/api/lm/${modelType}/replace-preview`,
|
||||
|
||||
@@ -1086,6 +1086,33 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versions whose stored price currently crosses the alert threshold.
|
||||
* Backed by the persisted alert state, so it matches the version badges.
|
||||
* @param {{limit?: number}} [options]
|
||||
* @returns {Promise<{alerts: Array, thresholdBuzz: number, enabled: boolean}>}
|
||||
*/
|
||||
async getPriceAlerts({ limit = 200 } = {}) {
|
||||
const endpoint = this.apiConfig?.endpoints?.priceAlerts;
|
||||
if (!endpoint) {
|
||||
return { alerts: [], thresholdBuzz: 0, enabled: false };
|
||||
}
|
||||
|
||||
const response = await fetch(`${endpoint}?limit=${encodeURIComponent(limit)}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload?.success !== true) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to load price alerts');
|
||||
}
|
||||
|
||||
return {
|
||||
alerts: Array.isArray(payload.alerts) ? payload.alerts : [],
|
||||
thresholdBuzz: payload.thresholdBuzz ?? 0,
|
||||
enabled: payload.enabled === true
|
||||
};
|
||||
}
|
||||
|
||||
async refreshUpdatesForModels(modelIds, { force = false } = {}) {
|
||||
if (!Array.isArray(modelIds) || modelIds.length === 0) {
|
||||
throw new Error('No model IDs provided');
|
||||
|
||||
Reference in New Issue
Block a user