Files
DemonGatanjieu-Anomalous_Mo…/api/scan_choice.py
T
DemonGatanjieuandClaude Opus 5.5 9c814ff0c1 Tidier model folders: side files in a hidden .amb, no duplicate covers
A user found model folders cluttered after scans: four files per model.
model_sidecars.py now says where the plugin's own side files are (.info,
.anomalous.json, .civitai_bak.*): beside the model, or in the folder's
hidden .amb (Settings -> Models -> Files that come with models). Every
reader and writer goes through it and reads both places; covers stay
beside the model for ComfyUI and other plugins.

Scans no longer keep a copy of Civitai's picture identical to the cover:
the cover is noted as Civitai's (SHA-256 in the .info) and a copy is kept
aside only while the user's own cover shows. api/sidecar_layout.py moves
existing files and drops the duplicates to the Recycle Bin. The folder
tree leaves .amb out; the duplicated tree builder and cover owner check
are shared now (model_catalog.py, backup.py back under 600 lines).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:54:28 +08:00

75 lines
3.4 KiB
Python

"""The user's choice to leave a model out of scans, or keep it in: POST /anomalous/scan_choice,
kept in each model's <model>.anomalous.json ("scan"; model_identity.scan_choice reads it). The
scan page asks it about new models that look like no drawing model; a model's details change it."""
import asyncio
import os
from aiohttp import web
from .local_guard import may_change
from .path_utils import atomic_write_json, resolve_folder_subdir
from .utils import model_file_within
try:
from ..model_identity import USER_INFO_SUFFIX, read_json
from ..model_sidecars import target as sidecar_target
except ImportError: # loaded outside the package (tests)
from model_identity import USER_INFO_SUFFIX, read_json
from model_sidecars import target as sidecar_target
MAX_CHOICE_FILES = 2000
def _set_scan_choice(targets, choice):
"""Writes the user's scan choice into each picked model's <model>.anomalous.json ("scan":
"skip" or "scan"; "" removes it). `targets`: [{type, path_idx, subfolder, files}] as a scan
takes them. Returns (models changed, file names left as they were: not found, not a
.safetensors, or an unreadable .anomalous.json)."""
if choice not in ("skip", "scan", "") or not isinstance(targets, list):
raise ValueError("Invalid scan choice")
picked = []
for target in targets:
names = target.get("files") if isinstance(target, dict) else None
if not isinstance(names, list) or not all(isinstance(name, str) for name in names):
raise ValueError("each target needs a list of file names")
_, target_dir = resolve_folder_subdir(target.get("type"), int(target.get("path_idx", 0)), target.get("subfolder") or "/")
picked += [(target_dir, name) for name in names]
if len(picked) > MAX_CHOICE_FILES:
raise ValueError("Too many models")
changed, failed = 0, []
for target_dir, name in picked:
try:
path = model_file_within(target_dir, name)
except ValueError:
failed.append(name)
continue
user_file = sidecar_target(os.path.splitext(path)[0], USER_INFO_SUFFIX)
user = read_json(user_file)
if not name.endswith(".safetensors") or not os.path.isfile(path) or (user is None and os.path.exists(user_file)):
failed.append(name)
continue
user = user or {"format": 1}
if choice:
user["scan"] = choice
else:
user.pop("scan", None)
atomic_write_json(user_file, user)
changed += 1
return changed, failed
async def api_set_scan_choice(request):
"""POST /anomalous/scan_choice {targets, choice} - leave models out of scans ("skip"), keep
them in ("scan"), or forget the choice (""). Only from this computer (local_guard.may_change)."""
if not may_change(request):
return web.json_response({"status": "error", "message": "Only from this computer"}, status=403)
try:
data = await request.json()
changed, failed = await asyncio.to_thread(_set_scan_choice, data.get("targets"), data.get("choice", ""))
except (AttributeError, KeyError, TypeError, ValueError):
return web.json_response({"status": "error", "message": "Invalid scan choice"}, status=400)
except OSError:
return web.json_response({"status": "error", "message": "Could not save the choice"}, status=500)
return web.json_response({"status": "success", "changed": changed, "failed": failed})