mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-05 17:35:33 -03:00
refactor(update)!: make obtainability a property of updates, not a surface
The owner could not tell from the UI what "Buzz Price Tracking" enabled, what the "Price alert threshold" number meant, or what "Price Alerts" was alerting about. That was not a copy problem: the implementation exposed our mechanism (a page scrape) and our SQL predicates as the user's concepts. Two concrete defects came from the same root: - the alert population included versions the user already owns (neither the event generator nor the query filtered on is_in_library; in the owner's library 28 of 52 gated versions were already downloaded, so most "alerts" were about files already on disk, which cannot become cheaper *for them*); - a threshold-filtered state list lived in a notification surface, so an empty panel had three indistinguishable causes and read as a broken feature. The information model is now the version plus ownership: cost is shown only where a decision exists. Owned -> nothing. Not owned and free -> nothing. Not owned and gated -> the price when it is known, `Paid` without a number when it is not, and early access keeps its countdown because "free on <date>" decides between waiting and paying. The numeric threshold has no place in that model: every decision is categorical (wait / pay / skip), so the setting, the comparison and the whole alert-state machine are gone. - both alert-state columns are removed from the schema rather than left dead; a database created by an unreleased build has them dropped on open (native ALTER TABLE ... DROP COLUMN, guarded), which is a no-op for everyone else - gate events are emitted only for versions the user does not have, and the price-drop event goes with the threshold it belonged to - both alert endpoints, PriceAlertsHandler and the service-registry adapter field it needed are removed: events already reach the UI through the refresh response - the bell tab, panel, CSS, both entry points, the unread watermark and their locales are removed; the setting keeps only the enable flag and the refresh interval and is framed as plumbing - "Price unavailable" is replaced by `Paid`: the gate is certain from the public API, only the number is best-effort, and that is our plumbing, not the user's problem Verified against a copy of the owner's real database: 52 gated versions -> 28 owned (now silent) + 24 the feature is actually about; the drop migration ran and both removed endpoints 404.
This commit is contained in:
@@ -2242,11 +2242,6 @@ 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:
|
||||
@@ -4319,157 +4314,6 @@ 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()
|
||||
unavailable_count = update_service.count_unavailable_prices()
|
||||
priced_count = update_service.count_priced_versions()
|
||||
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,
|
||||
# Gated versions whose price could not be read (mature models
|
||||
# are served only by the challenged civitai.red host).
|
||||
"unavailableCount": unavailable_count,
|
||||
# Lets the empty state explain a 0 Buzz threshold instead of
|
||||
# claiming there is simply nothing to show.
|
||||
"pricedCount": priced_count,
|
||||
"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."""
|
||||
|
||||
@@ -4493,7 +4337,6 @@ 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,
|
||||
@@ -4516,7 +4359,6 @@ 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
|
||||
@@ -4530,7 +4372,6 @@ 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,
|
||||
@@ -4607,5 +4448,4 @@ 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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user