mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-05 09:35:31 -03:00
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.
200 lines
8.6 KiB
Python
200 lines
8.6 KiB
Python
"""Route registrar for model endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Callable, Iterable, Mapping
|
|
|
|
from aiohttp import web
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RouteDefinition:
|
|
"""Declarative definition for a HTTP route."""
|
|
|
|
method: str
|
|
path_template: str
|
|
handler_name: str
|
|
|
|
def build_path(self, prefix: str) -> str:
|
|
return self.path_template.replace("{prefix}", prefix)
|
|
|
|
|
|
COMMON_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|
RouteDefinition("GET", "/api/lm/{prefix}/list", "get_models"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/excluded", "get_excluded_models"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/delete", "delete_model"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/exclude", "exclude_model"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/unexclude", "unexclude_model"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/fetch-civitai", "fetch_civitai"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/fetch-all-civitai", "fetch_all_civitai"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/relink-civitai", "relink_civitai"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/replace-preview", "replace_preview"),
|
|
RouteDefinition(
|
|
"POST", "/api/lm/{prefix}/set-preview-from-url", "set_preview_from_url"
|
|
),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/save-metadata", "save_metadata"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/add-tags", "add_tags"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/rename", "rename_model"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/bulk-delete", "bulk_delete_models"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/verify-duplicates", "verify_duplicates"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/move_model", "move_model"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/move_models_bulk", "move_models_bulk"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/create-folder", "create_folder"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/delete-folder", "delete_folder"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/rename-folder", "rename_folder"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/auto-organize", "auto_organize_models"),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/auto-organize", "auto_organize_models"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/auto-organize-progress", "get_auto_organize_progress"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/apply-filename-template", "apply_filename_template"
|
|
),
|
|
RouteDefinition(
|
|
"POST", "/api/lm/{prefix}/apply-filename-template", "apply_filename_template"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/top-tags", "get_top_tags"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/search-tags", "search_tags"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/base-models", "get_base_models"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/model-types", "get_model_types"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/scan", "scan_models"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/roots", "get_model_roots"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/folders", "get_folders"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/folder-tree", "get_folder_tree"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/unified-folder-tree", "get_unified_folder_tree"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/find-duplicates", "find_duplicate_models"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/find-filename-conflicts", "find_filename_conflicts"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/get-notes", "get_model_notes"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/preview-url", "get_model_preview_url"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/civitai-url", "get_model_civitai_url"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/metadata", "get_model_metadata"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/model-description", "get_model_description"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/relative-paths", "get_relative_paths"),
|
|
RouteDefinition("PUT", "/api/lm/{prefix}/active-filters", "update_active_filters"),
|
|
RouteDefinition("GET", "/api/lm/{prefix}/active-filters", "get_active_filters"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/civitai/versions/{model_id}", "get_civitai_versions"
|
|
),
|
|
RouteDefinition(
|
|
"GET",
|
|
"/api/lm/{prefix}/civitai/model/version/{modelVersionId}",
|
|
"get_civitai_model_by_version",
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/civitai/model/hash/{hash}", "get_civitai_model_by_hash"
|
|
),
|
|
RouteDefinition(
|
|
"POST", "/api/lm/{prefix}/updates/refresh", "refresh_model_updates"
|
|
),
|
|
RouteDefinition(
|
|
"POST",
|
|
"/api/lm/{prefix}/updates/fetch-missing-license",
|
|
"fetch_missing_civitai_license_data",
|
|
),
|
|
RouteDefinition(
|
|
"POST", "/api/lm/{prefix}/updates/ignore", "set_model_update_ignore"
|
|
),
|
|
RouteDefinition(
|
|
"POST", "/api/lm/{prefix}/updates/ignore-version", "set_version_update_ignore"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/updates/status/{model_id}", "get_model_update_status"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/{prefix}/updates/versions/{model_id}", "get_model_versions"
|
|
),
|
|
RouteDefinition("POST", "/api/lm/download-model", "download_model"),
|
|
RouteDefinition("GET", "/api/lm/download-model-get", "download_model_get"),
|
|
RouteDefinition("GET", "/api/lm/cancel-download-get", "cancel_download_get"),
|
|
RouteDefinition("GET", "/api/lm/skip-download", "skip_download_get"),
|
|
RouteDefinition("GET", "/api/lm/pause-download", "pause_download_get"),
|
|
RouteDefinition("GET", "/api/lm/resume-download", "resume_download_get"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/download-progress/{download_id}", "get_download_progress"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/downloads/queue", "get_download_queue"),
|
|
RouteDefinition("GET", "/api/lm/downloads/queue/add", "add_to_download_queue"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/queue/remove", "remove_from_download_queue"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/queue/move-to-top", "move_queue_item_to_top"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/queue/move-to-end", "move_queue_item_to_end"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/queue/clear", "clear_download_queue"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/downloads/history", "get_download_history"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/history/clear", "clear_download_history"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/history/delete", "delete_download_history_item"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/history/retry", "retry_download_from_history"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/history/retry-all", "retry_all_failed_downloads"
|
|
),
|
|
RouteDefinition("GET", "/api/lm/downloads/stats", "get_download_stats"),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/queue/complete", "complete_download_in_queue"
|
|
),
|
|
RouteDefinition(
|
|
"GET", "/api/lm/downloads/queue/status", "update_download_queue_status"
|
|
),
|
|
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
|
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
|
)
|
|
|
|
|
|
class ModelRouteRegistrar:
|
|
"""Bind declarative definitions to an aiohttp router."""
|
|
|
|
_METHOD_MAP = {
|
|
"GET": "add_get",
|
|
"POST": "add_post",
|
|
"PUT": "add_put",
|
|
"DELETE": "add_delete",
|
|
}
|
|
|
|
def __init__(self, app: web.Application) -> None:
|
|
self._app = app
|
|
|
|
def register_common_routes(
|
|
self,
|
|
prefix: str,
|
|
handler_lookup: Mapping[str, Callable[[web.Request], object]],
|
|
*,
|
|
definitions: Iterable[RouteDefinition] = COMMON_ROUTE_DEFINITIONS,
|
|
) -> None:
|
|
for definition in definitions:
|
|
self._bind_route(
|
|
definition.method,
|
|
definition.build_path(prefix),
|
|
handler_lookup[definition.handler_name],
|
|
)
|
|
|
|
def add_route(self, method: str, path: str, handler: Callable[..., Any]) -> None:
|
|
self._bind_route(method, path, handler)
|
|
|
|
def add_prefixed_route(
|
|
self, method: str, path_template: str, prefix: str, handler: Callable[..., Any]
|
|
) -> None:
|
|
self._bind_route(method, path_template.replace("{prefix}", prefix), handler)
|
|
|
|
def _bind_route(self, method: str, path: str, handler: Callable[..., Any]) -> None:
|
|
add_method_name = self._METHOD_MAP[method.upper()]
|
|
add_method = getattr(self._app.router, add_method_name)
|
|
add_method(path, handler)
|