Add an opt-in 'centralized' sidecar storage mode alongside the default
'alongside' layout. In centralized mode, .metadata.json sidecars and
preview assets live under a configurable root (sidecar_storage_path,
default <settings_dir>/sidecars), mirroring the library-relative
directory structure: <root>/<library>/<root_basename>/<rel_dir>/.
Backend:
- settings: sidecar_storage_mode / sidecar_storage_path with validation;
changing either refreshes the preview allowlist
- config: centralized root added to preview-serving allowlist
- lifecycle: delete / move / rename / folder-rename / folder-delete and
undoable-delete staging all operate on the mirror tree in centralized
mode (model files themselves never move); EXDEV-tolerant cross-
filesystem moves
- scanners: pending-hash filesystem scan walks the mirror tree in
centralized mode; preview discovery reads from the sidecar dir;
.civitai.info stays co-located in both modes
- migration: SidecarMigrationUseCase moves sidecars+previews between
layouts both directions (keep-newer conflict resolution, preview_url
rewriting, WebSocket progress), exposed as POST+GET
/api/lm/sidecars/migrate with a mode guard (force=true for the
settings-first flow)
Frontend:
- settings modal: sidecar storage section (mode select + path input with
browse/validation), mode-change confirmation offering immediate
migration (force=true), and a 'Migrate Sidecars Now' action
- i18n keys synced to all locales ([TODO: Translate] placeholders)
Docs: metadata-json-schema.md gains a storage-location section;
AGENTS.md records the sidecar_paths helper convention.
Phase 1 of #1045 (optional centralized sidecar storage): introduce
py/utils/sidecar_paths.py as the single place that resolves .metadata.json
and preview locations, and replace all inline splitext-based derivations
across scanners, services, download manager, and route handlers.
No behavior change: the default 'alongside' storage mode resolves every
path exactly as before. .civitai.info (third-party sidecar) derivation is
intentionally left co-located.
Refresh after manual .metadata.json deletion rebuilds the payload without
file_name/size/modified, which are required by BaseModelMetadata.from_dict.
The recreated sidecar then fails to parse and the scanner skips the model.
- load_metadata_payload fills missing file facts from os.stat
- hydrate_model_data restores every missing key from the cache snapshot
only when the sidecar is missing entirely (disk stays authoritative
otherwise), preferring the cached import timestamp for modified
- save_metadata fills file facts on write so no write path can produce
an unparseable sidecar
- Three-state autov3 field (not-checked / checked-unavailable / 12-hex value)
in .metadata.json sidecars, in-memory ModelHashIndex, and SQLite
(models.autov3 column + autov3_index table) with column-presence migration
- Background self-terminating backfill for legacy rows: per-model-type
concurrency guard, executor-offloaded I/O, Civitai-first resolution
(SHA256-matched version file) falling back to the embedded safetensors
header hash
- Civitai-first propagation on metadata refresh, scan, and download paths;
reject the empty-string SHA256 placeholder and strip OneTrainer 0x prefix
- List API hash filters and hash index lookups accept 12-char AutoV3
- Cap safetensors header reads at 64 MiB to prevent crafted-file allocation
- Prevent stale AutoV3 mappings on file replacement while preserving them on
same-file re-registration (lazy-hash completion)
- Change `model_type` field to `sub_type` for checkpoint models to improve naming consistency
- Add `sub_type="embedding"` for embedding models to properly categorize model subtypes
- Maintain backward compatibility with existing metadata structure
Change `load_metadata` method to return a tuple containing both the metadata object and a boolean success flag instead of just the metadata object. This provides clearer error handling and allows callers to distinguish between successful loads with null metadata versus actual load failures.