mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-05 17:35:33 -03:00
feat(update): add a Buzz price alerts panel to the notification bell
P5a of docs/plans/paid-model-price-tracking.md: one surface that answers "what got cheaper / became free", without a permanent button (the grid filter was dropped by owner decision, so the panel carries the actions itself). - price_alert_since records when an alert started, so the panel can say "dropped 3 d ago" and count what is new since the user last looked; it is set on the first sight of an already-cheap version, preserved while the alert stands, and cleared when the price rises back above the threshold - get_price_alerts() compares the threshold at read time (editing it takes effect immediately, no refresh needed) and returns both kinds in one list; model_type=None covers every type, which the shared update DB makes a single query. "became free" needs no price data, so it is reported even while price tracking is off - GET /api/lm/price-alerts, registered once in MiscRoutes rather than per model type, decorating rows best-effort with the local model name and file path from the scanner indexes (a cold cache just omits them) - a third tab in the notification bell: segments for under-threshold and became-free, the three states (tracking off / nothing matching / stale), and per-row actions (CivitAI always, Open when the model is local) - two non-permanent entry points share one helper: the controls-bar updates dropdown and the global context menu, whose label carries the unread count - unread state stays client-side (localStorage watermark); the count is fetched once on init and only when price tracking is enabled - the per-type frontend client method is removed as dead code; the per-type backend route stays for the companion extension
This commit is contained in:
@@ -48,6 +48,7 @@ from ...services.cache_health_monitor import CacheHealthMonitor, CacheHealthStat
|
||||
from ...services.use_cases.sidecar_migration_use_case import SidecarMigrationUseCase
|
||||
from ...services.websocket_progress_callback import WebSocketBroadcastCallback
|
||||
from ...utils.models import BaseModelMetadata
|
||||
from ...utils.civitai_utils import build_civitai_model_page_url
|
||||
from ...utils.constants import (
|
||||
CIVITAI_USER_MODEL_TYPES,
|
||||
DEFAULT_NODE_COLOR,
|
||||
@@ -2241,6 +2242,11 @@ class ServiceRegistryAdapter:
|
||||
get_downloaded_version_history_service: Callable[[], Awaitable[Any]]
|
||||
get_backup_service: Callable[[], Awaitable[Any]] = _noop_backup_service
|
||||
get_other_scanner: Callable[[], Awaitable[Any]] = ServiceRegistry.get_other_scanner
|
||||
# Shared across model types (the update DB is per library), which is what lets
|
||||
# the global price-alerts endpoint cover every type in one query.
|
||||
get_model_update_service: Callable[[], Awaitable[Any]] = (
|
||||
ServiceRegistry.get_model_update_service
|
||||
)
|
||||
|
||||
|
||||
class ModelLibraryHandler:
|
||||
@@ -4313,6 +4319,149 @@ class SidecarMigrationHandler:
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
|
||||
class PriceAlertsHandler:
|
||||
"""Data for the global Buzz price-alerts panel.
|
||||
|
||||
App-wide on purpose: the update DB is one file per library shared by every
|
||||
model type, and ``ServiceRegistry.get_model_update_service()`` hands out one
|
||||
instance, so the whole panel is a single query. Registered once (see
|
||||
``MISC_ROUTE_DEFINITIONS``) rather than once per model type.
|
||||
"""
|
||||
|
||||
# Model type -> adapter attribute, for the best-effort local name/path lookup.
|
||||
_SCANNER_ATTRS = {
|
||||
"lora": "get_lora_scanner",
|
||||
"checkpoint": "get_checkpoint_scanner",
|
||||
"embedding": "get_embedding_scanner",
|
||||
"other": "get_other_scanner",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
settings_service,
|
||||
service_registry: ServiceRegistryAdapter,
|
||||
) -> None:
|
||||
self._settings = settings_service
|
||||
self._service_registry = service_registry
|
||||
|
||||
def _setting(self, key: str, default: Any) -> Any:
|
||||
if self._settings is None:
|
||||
return default
|
||||
try:
|
||||
value = self._settings.get(key, default)
|
||||
except Exception:
|
||||
return default
|
||||
return default if value is None else value
|
||||
|
||||
async def get_price_alerts(self, request: web.Request) -> web.Response:
|
||||
"""Versions under the alert threshold, plus the ones that became free.
|
||||
|
||||
GET with an optional ``limit`` so the companion extension can call it too.
|
||||
"""
|
||||
|
||||
try:
|
||||
limit = int(request.query.get("limit") or 200)
|
||||
except (TypeError, ValueError):
|
||||
limit = 200
|
||||
|
||||
threshold = 0
|
||||
try:
|
||||
threshold = max(0, int(self._setting("price_alert_threshold_buzz", 0)))
|
||||
except (TypeError, ValueError):
|
||||
threshold = 0
|
||||
|
||||
try:
|
||||
update_service = await self._service_registry.get_model_update_service()
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
logger.error("Price alerts unavailable: %s", exc, exc_info=True)
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Update service unavailable"}, status=503
|
||||
)
|
||||
|
||||
try:
|
||||
alerts = await update_service.get_price_alerts(
|
||||
None, threshold_buzz=threshold, limit=limit
|
||||
)
|
||||
newest_checked_at = update_service.newest_price_checked_at()
|
||||
except Exception as exc:
|
||||
logger.error("Failed to load price alerts: %s", exc, exc_info=True)
|
||||
return web.json_response(
|
||||
{"success": False, "error": str(exc)}, status=500
|
||||
)
|
||||
|
||||
await self._decorate_local_context(alerts)
|
||||
|
||||
civitai_host = self._setting("civitai_host", "civitai.com")
|
||||
for alert in alerts:
|
||||
alert["civitaiUrl"] = build_civitai_model_page_url(
|
||||
alert.get("modelId"),
|
||||
alert.get("versionId"),
|
||||
host=civitai_host,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"enabled": bool(self._setting("price_tracking_enabled", False)),
|
||||
"thresholdBuzz": threshold,
|
||||
"newestCheckedAt": newest_checked_at,
|
||||
"alerts": alerts,
|
||||
}
|
||||
)
|
||||
|
||||
async def _decorate_local_context(self, alerts: Sequence[Dict[str, Any]]) -> None:
|
||||
"""Attach ``modelName`` / ``filePath`` / ``fileName`` when the model is local.
|
||||
|
||||
Best-effort by design: the panel must render even when a scanner cache is
|
||||
cold or a type is unavailable, so every failure just leaves the fields as
|
||||
None and the UI hides the affordances that need them.
|
||||
"""
|
||||
|
||||
by_type: Dict[str, set] = {}
|
||||
for alert in alerts:
|
||||
model_type = alert.get("modelType")
|
||||
model_id = alert.get("modelId")
|
||||
if not model_type or model_id is None:
|
||||
continue
|
||||
by_type.setdefault(model_type, set()).add(model_id)
|
||||
|
||||
for model_type, model_ids in by_type.items():
|
||||
attr = self._SCANNER_ATTRS.get(model_type)
|
||||
scanner_getter = getattr(self._service_registry, attr, None) if attr else None
|
||||
if scanner_getter is None:
|
||||
continue
|
||||
try:
|
||||
scanner = await scanner_getter()
|
||||
cache = await scanner.get_cached_data()
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
"Skipping local context for %s price alerts: %s", model_type, exc
|
||||
)
|
||||
continue
|
||||
|
||||
model_index = getattr(cache, "model_id_index", None) or {}
|
||||
version_index = getattr(cache, "version_index", None) or {}
|
||||
for alert in alerts:
|
||||
if alert.get("modelType") != model_type:
|
||||
continue
|
||||
if alert.get("modelId") not in model_ids:
|
||||
continue
|
||||
|
||||
entries = model_index.get(alert.get("modelId")) or []
|
||||
if entries and isinstance(entries[0], Mapping):
|
||||
first = entries[0]
|
||||
alert["modelName"] = first.get("model_name") or first.get(
|
||||
"file_name"
|
||||
)
|
||||
|
||||
version_entry = version_index.get(alert.get("versionId"))
|
||||
if isinstance(version_entry, Mapping):
|
||||
alert["filePath"] = version_entry.get("file_path")
|
||||
alert["fileName"] = version_entry.get("file_name")
|
||||
alert.setdefault("modelName", version_entry.get("model_name"))
|
||||
|
||||
|
||||
class MiscHandlerSet:
|
||||
"""Aggregate handlers into a lookup compatible with the registrar."""
|
||||
|
||||
@@ -4336,6 +4485,7 @@ class MiscHandlerSet:
|
||||
doctor: DoctorHandler,
|
||||
example_workflows: ExampleWorkflowsHandler,
|
||||
base_model: BaseModelHandlerSet,
|
||||
price_alerts: PriceAlertsHandler,
|
||||
model_source_handler: Any = None,
|
||||
agent_handler: Any = None,
|
||||
download_routing: Any = None,
|
||||
@@ -4358,6 +4508,7 @@ class MiscHandlerSet:
|
||||
self.doctor = doctor
|
||||
self.example_workflows = example_workflows
|
||||
self.base_model = base_model
|
||||
self.price_alerts = price_alerts
|
||||
self.model_source_handler = model_source_handler
|
||||
self.agent_handler = agent_handler
|
||||
self.download_routing = download_routing
|
||||
@@ -4371,6 +4522,7 @@ class MiscHandlerSet:
|
||||
"get_init_status": self.health.get_init_status,
|
||||
"get_settings": self.settings.get_settings,
|
||||
"update_settings": self.settings.update_settings,
|
||||
"get_price_alerts": self.price_alerts.get_price_alerts,
|
||||
"get_doctor_diagnostics": self.doctor.get_doctor_diagnostics,
|
||||
"repair_doctor_cache": self.doctor.repair_doctor_cache,
|
||||
"resolve_doctor_filename_conflicts": self.doctor.resolve_filename_conflicts,
|
||||
@@ -4447,4 +4599,5 @@ def build_service_registry_adapter() -> ServiceRegistryAdapter:
|
||||
get_other_scanner=ServiceRegistry.get_other_scanner,
|
||||
get_downloaded_version_history_service=ServiceRegistry.get_downloaded_version_history_service,
|
||||
get_backup_service=ServiceRegistry.get_backup_service,
|
||||
get_model_update_service=ServiceRegistry.get_model_update_service,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,9 @@ class RouteDefinition:
|
||||
MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition("GET", "/api/lm/settings", "get_settings"),
|
||||
RouteDefinition("POST", "/api/lm/settings", "update_settings"),
|
||||
# App-wide and registered once: the alerts panel spans every model type, and
|
||||
# the update DB is shared, so there is nothing per-type about it.
|
||||
RouteDefinition("GET", "/api/lm/price-alerts", "get_price_alerts"),
|
||||
RouteDefinition("GET", "/api/lm/llm/models", "get_llm_models"),
|
||||
RouteDefinition("GET", "/api/lm/llm/provider-models", "get_provider_models"),
|
||||
RouteDefinition("GET", "/api/lm/doctor/diagnostics", "get_doctor_diagnostics"),
|
||||
|
||||
@@ -31,6 +31,7 @@ from .handlers.misc_handlers import (
|
||||
ModelLibraryHandler,
|
||||
NodeRegistry,
|
||||
NodeRegistryHandler,
|
||||
PriceAlertsHandler,
|
||||
SettingsHandler,
|
||||
SidecarMigrationHandler,
|
||||
SupportersHandler,
|
||||
@@ -140,6 +141,10 @@ class MiscRoutes:
|
||||
doctor = DoctorHandler(settings_service=self._settings)
|
||||
example_workflows = ExampleWorkflowsHandler()
|
||||
base_model = BaseModelHandlerSet()
|
||||
price_alerts = PriceAlertsHandler(
|
||||
settings_service=self._settings,
|
||||
service_registry=self._service_registry_adapter,
|
||||
)
|
||||
model_source_handler = ModelSourceHandler()
|
||||
agent_handler = AgentHandler()
|
||||
download_routing = DownloadRoutingHandler()
|
||||
@@ -163,6 +168,7 @@ class MiscRoutes:
|
||||
doctor=doctor,
|
||||
example_workflows=example_workflows,
|
||||
base_model=base_model,
|
||||
price_alerts=price_alerts,
|
||||
model_source_handler=model_source_handler,
|
||||
agent_handler=agent_handler,
|
||||
download_routing=download_routing,
|
||||
|
||||
@@ -124,6 +124,10 @@ class ModelVersionRecord:
|
||||
# Last computed "price alert threshold hit" state, used to detect the edge
|
||||
# (0 -> 1) that fires a one-shot price_drop event.
|
||||
price_alert_state: bool = False
|
||||
# When the alert state last flipped to True (POSIX seconds); cleared when the
|
||||
# price rises back above the threshold. Gives the panel "dropped X ago" and
|
||||
# the unread count something to compare against.
|
||||
price_alert_since: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -374,6 +378,7 @@ class ModelUpdateService:
|
||||
price_sale_ends_at TEXT,
|
||||
price_checked_at REAL,
|
||||
price_alert_state INTEGER NOT NULL DEFAULT 0,
|
||||
price_alert_since REAL,
|
||||
PRIMARY KEY (model_id, version_id),
|
||||
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -657,6 +662,10 @@ class ModelUpdateService:
|
||||
"ALTER TABLE model_update_versions "
|
||||
"ADD COLUMN price_alert_state INTEGER NOT NULL DEFAULT 0"
|
||||
),
|
||||
"price_alert_since": (
|
||||
"ALTER TABLE model_update_versions "
|
||||
"ADD COLUMN price_alert_since REAL"
|
||||
),
|
||||
}
|
||||
|
||||
for column, statement in migrations.items():
|
||||
@@ -769,6 +778,7 @@ class ModelUpdateService:
|
||||
price_sale_ends_at TEXT,
|
||||
price_checked_at REAL,
|
||||
price_alert_state INTEGER NOT NULL DEFAULT 0,
|
||||
price_alert_since REAL,
|
||||
PRIMARY KEY (model_id, version_id),
|
||||
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
||||
)
|
||||
@@ -799,6 +809,7 @@ class ModelUpdateService:
|
||||
"price_sale_ends_at",
|
||||
"price_checked_at",
|
||||
"price_alert_state",
|
||||
"price_alert_since",
|
||||
]
|
||||
defaults = {
|
||||
"sort_index": "0",
|
||||
@@ -822,6 +833,7 @@ class ModelUpdateService:
|
||||
"price_sale_ends_at": "NULL",
|
||||
"price_checked_at": "NULL",
|
||||
"price_alert_state": "0",
|
||||
"price_alert_since": "NULL",
|
||||
}
|
||||
|
||||
select_parts = []
|
||||
@@ -1203,13 +1215,28 @@ class ModelUpdateService:
|
||||
return self._get_records_bulk(model_type, normalized_ids)
|
||||
|
||||
async def get_price_alerts(
|
||||
self, model_type: str, limit: int = 200
|
||||
self,
|
||||
model_type: Optional[str] = None,
|
||||
*,
|
||||
threshold_buzz: Optional[int] = None,
|
||||
limit: int = 200,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Return the gated versions whose price currently crosses the threshold.
|
||||
"""Return the versions worth alerting on, for one type or for all of them.
|
||||
|
||||
Reads the persisted ``price_alert_state`` rather than recomputing, so the
|
||||
list matches the badges the user sees. Cheapest versions first, since that
|
||||
is what a price watch is for.
|
||||
The threshold is compared **at read time** rather than reading the persisted
|
||||
``price_alert_state``: otherwise editing the threshold in the panel would
|
||||
not take effect until the next refresh. ``price_alert_state`` stays the
|
||||
source of the one-shot toast edge.
|
||||
|
||||
Two kinds are returned in one list:
|
||||
|
||||
* ``below_threshold`` — gated versions whose effective price is at or below
|
||||
the threshold (only possible when price capture has run);
|
||||
* ``became_free`` — versions whose gate lapsed, which needs no price data at
|
||||
all, so they are reported even while price tracking is switched off.
|
||||
|
||||
``model_type=None`` covers every type, which is what the global alerts panel
|
||||
wants; the update DB is shared across types, so that is still one query.
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -1217,9 +1244,25 @@ class ModelUpdateService:
|
||||
except (TypeError, ValueError):
|
||||
normalized_limit = 200
|
||||
|
||||
if threshold_buzz is None:
|
||||
threshold = self._price_alert_threshold_buzz()
|
||||
else:
|
||||
try:
|
||||
threshold = max(0, int(threshold_buzz))
|
||||
except (TypeError, ValueError):
|
||||
threshold = self._price_alert_threshold_buzz()
|
||||
|
||||
params: List[Any] = []
|
||||
type_filter = ""
|
||||
if model_type:
|
||||
type_filter = "AND s.model_type = ?"
|
||||
params.append(model_type)
|
||||
params.append(threshold)
|
||||
params.append(normalized_limit)
|
||||
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
f"""
|
||||
SELECT v.model_id,
|
||||
v.version_id,
|
||||
v.name,
|
||||
@@ -1228,6 +1271,8 @@ class ModelUpdateService:
|
||||
v.accepts_blue_buzz,
|
||||
v.price_sale_ends_at,
|
||||
v.price_checked_at,
|
||||
v.price_alert_since,
|
||||
v.gate_lapsed_at,
|
||||
v.is_in_library,
|
||||
v.early_access_ends_at,
|
||||
v.paid_access,
|
||||
@@ -1236,38 +1281,74 @@ class ModelUpdateService:
|
||||
s.model_type
|
||||
FROM model_update_versions v
|
||||
JOIN model_update_status s ON s.model_id = v.model_id
|
||||
WHERE v.price_alert_state = 1
|
||||
AND v.should_ignore = 0
|
||||
WHERE v.should_ignore = 0
|
||||
AND s.should_ignore_model = 0
|
||||
AND s.model_type = ?
|
||||
ORDER BY v.price_buzz ASC, v.model_id ASC, v.version_id ASC
|
||||
{type_filter}
|
||||
AND (
|
||||
(v.price_buzz IS NOT NULL AND v.price_buzz <= ?)
|
||||
OR (v.gate_lapsed_at IS NOT NULL
|
||||
AND v.paid_access IS NULL
|
||||
AND v.is_paid = 0
|
||||
AND v.is_early_access = 0)
|
||||
)
|
||||
ORDER BY (v.price_buzz IS NULL) ASC,
|
||||
v.price_buzz ASC,
|
||||
v.gate_lapsed_at DESC,
|
||||
v.model_id ASC,
|
||||
v.version_id ASC
|
||||
LIMIT ?
|
||||
""",
|
||||
(model_type, normalized_limit),
|
||||
tuple(params),
|
||||
).fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"modelId": int(row["model_id"]),
|
||||
"modelType": row["model_type"],
|
||||
"versionId": int(row["version_id"]),
|
||||
"versionName": row["name"],
|
||||
"priceBuzz": _normalize_int(row["price_buzz"]),
|
||||
"listPriceBuzz": _normalize_int(row["list_price_buzz"]),
|
||||
"acceptsBlueBuzz": bool(row["accepts_blue_buzz"]),
|
||||
"priceSaleEndsAt": row["price_sale_ends_at"],
|
||||
"priceCheckedAt": (
|
||||
float(row["price_checked_at"])
|
||||
if row["price_checked_at"] is not None
|
||||
else None
|
||||
),
|
||||
"isInLibrary": bool(row["is_in_library"]),
|
||||
"earlyAccessEndsAt": row["early_access_ends_at"],
|
||||
"isPaid": bool(row["is_paid"]),
|
||||
"isEarlyAccess": bool(row["is_early_access"]),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
alerts: List[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
price_buzz = _normalize_int(row["price_buzz"])
|
||||
below_threshold = price_buzz is not None and price_buzz <= threshold
|
||||
alerts.append(
|
||||
{
|
||||
"modelId": int(row["model_id"]),
|
||||
"modelType": row["model_type"],
|
||||
"versionId": int(row["version_id"]),
|
||||
"versionName": row["name"],
|
||||
"kind": "below_threshold" if below_threshold else "became_free",
|
||||
"priceBuzz": price_buzz if below_threshold else None,
|
||||
"listPriceBuzz": (
|
||||
_normalize_int(row["list_price_buzz"])
|
||||
if below_threshold
|
||||
else None
|
||||
),
|
||||
"acceptsBlueBuzz": bool(row["accepts_blue_buzz"]),
|
||||
"priceSaleEndsAt": row["price_sale_ends_at"],
|
||||
"priceCheckedAt": (
|
||||
float(row["price_checked_at"])
|
||||
if row["price_checked_at"] is not None
|
||||
else None
|
||||
),
|
||||
"priceAlertSince": (
|
||||
float(row["price_alert_since"])
|
||||
if row["price_alert_since"] is not None
|
||||
else None
|
||||
),
|
||||
"gateLapsedAt": row["gate_lapsed_at"],
|
||||
"isInLibrary": bool(row["is_in_library"]),
|
||||
"earlyAccessEndsAt": row["early_access_ends_at"],
|
||||
"isPaid": bool(row["is_paid"]),
|
||||
"isEarlyAccess": bool(row["is_early_access"]),
|
||||
}
|
||||
)
|
||||
return alerts
|
||||
|
||||
def newest_price_checked_at(self) -> Optional[float]:
|
||||
"""Newest successful price fetch across the tracked versions."""
|
||||
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT MAX(price_checked_at) AS newest FROM model_update_versions"
|
||||
).fetchone()
|
||||
if row is None or row["newest"] is None:
|
||||
return None
|
||||
return float(row["newest"])
|
||||
|
||||
async def _refresh_single_model(
|
||||
self,
|
||||
@@ -1852,6 +1933,16 @@ class ModelUpdateService:
|
||||
price_fields["price_alert_state"] = self._evaluate_price_alert(
|
||||
price_fields.get("price_buzz")
|
||||
)
|
||||
# `price_alert_since` records when the alert last started, so the panel
|
||||
# can say "dropped 3 days ago" and count what is new since the user
|
||||
# last looked. A still-standing alert keeps its original timestamp; a
|
||||
# price back above the threshold clears it.
|
||||
if not price_fields["price_alert_state"]:
|
||||
price_fields["price_alert_since"] = None
|
||||
elif existing_version is not None and existing_version.price_alert_state:
|
||||
price_fields["price_alert_since"] = existing_version.price_alert_since
|
||||
else:
|
||||
price_fields["price_alert_since"] = timestamp
|
||||
|
||||
if (
|
||||
existing_version is not None
|
||||
@@ -1987,6 +2078,7 @@ class ModelUpdateService:
|
||||
"price_sale_ends_at": None,
|
||||
"price_checked_at": None,
|
||||
"price_alert_state": False,
|
||||
"price_alert_since": None,
|
||||
}
|
||||
|
||||
if remote_version.price_checked_at is not None:
|
||||
@@ -2002,6 +2094,7 @@ class ModelUpdateService:
|
||||
"price_sale_ends_at": source.price_sale_ends_at,
|
||||
"price_checked_at": source.price_checked_at,
|
||||
"price_alert_state": source.price_alert_state,
|
||||
"price_alert_since": source.price_alert_since,
|
||||
}
|
||||
|
||||
def _price_tracking_enabled(self) -> bool:
|
||||
@@ -2374,7 +2467,8 @@ class ModelUpdateService:
|
||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||
is_early_access, usage_control, paid_access, is_paid, file_count,
|
||||
gate_lapsed_at, price_buzz, list_price_buzz, generation_price_buzz,
|
||||
accepts_blue_buzz, price_sale_ends_at, price_checked_at, price_alert_state
|
||||
accepts_blue_buzz, price_sale_ends_at, price_checked_at, price_alert_state,
|
||||
price_alert_since
|
||||
FROM model_update_versions
|
||||
WHERE model_id IN ({placeholders})
|
||||
ORDER BY model_id ASC, sort_index ASC, version_id ASC
|
||||
@@ -2418,6 +2512,11 @@ class ModelUpdateService:
|
||||
else None
|
||||
),
|
||||
price_alert_state=bool(row["price_alert_state"]),
|
||||
price_alert_since=(
|
||||
float(row["price_alert_since"])
|
||||
if row["price_alert_since"] is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2482,8 +2581,9 @@ class ModelUpdateService:
|
||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||
is_early_access, usage_control, paid_access, is_paid, file_count,
|
||||
gate_lapsed_at, price_buzz, list_price_buzz, generation_price_buzz,
|
||||
accepts_blue_buzz, price_sale_ends_at, price_checked_at, price_alert_state
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
accepts_blue_buzz, price_sale_ends_at, price_checked_at, price_alert_state,
|
||||
price_alert_since
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
version.version_id,
|
||||
@@ -2510,6 +2610,7 @@ class ModelUpdateService:
|
||||
version.price_sale_ends_at,
|
||||
version.price_checked_at,
|
||||
1 if version.price_alert_state else 0,
|
||||
version.price_alert_since,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
Reference in New Issue
Block a user