mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-14 17:00:08 +00:00
v3 library: per-field pack overwrite (R4b) — fix wrong author values, with receipts
DRAFT — gated on the feedpak-spec §7 amendment (got-feedback/feedpak-spec#47). Gap-fill (#724) fills absent keys; this fixes WRONG author-set values (a typo'd artist, a wrong year) one field at a time, with provenance and an undo. Built fully compliant with the amendment shape so it's ready the moment §7 is amended. Server (extends the merged gap-fill endpoints, doesn't fork): - Preview GET /api/song/{fn}/gap-fill now also returns `differs` = [{key, current, proposed}] over the allowlist (title/artist/album/year/ genres) where the manifest HAS an author value and the confirmed match supplies a DIFFERENT one — plus `overwrite_allowed` (the Settings gate) and `has_backup`. differs is EMPTY unless match_state == 'manual': a user-confirmed pin can authorize a replace; an auto-match never can. mbid/ isrc are never in differs (identity changes only via re-match). - POST accepts `overwrite_keys` alongside gap-fill's `keys`; gated on the allow_pack_overwrite setting + manual state + the allowlist, proposals recomputed under _song_io_lock so a field that changed since preview is skipped. Writes via the re-serialize writer (extended to handle the genres list). Every written key (gap-fill AND overwrite) lands in a new write_log receipts table (filename, key, old, new, source, score, ts), pruned to the newest 5000 rows. - POST /api/song/{fn}/revert-original restores the pack from its .bak (dir + zip forms), re-stats + re-syncs the DB; 404 when no backup; the .bak is PRESERVED after revert (re-appliable). Demo-blocked, like the write. - GET /api/song/{fn}/write-log — the song's receipts, newest first. - Setting allow_pack_overwrite (default OFF). The .bak safety property is unchanged from the shipped writer: the backup is written ONCE (first write) and is always the pristine author original — a second overwrite never clobbers it, so Revert always returns the untouched pack. Frontend (Details drawer, extends the gap-fill block): when the preview returns differs AND overwriting is enabled, an "Overwrite existing fields" sub-block renders per-key rows `Artist: "ACDC" -> "AC/DC"`, ALL UNTICKED by default, with the warning that it replaces what the author wrote and the original is kept as a backup; the Write button carries the ticked keys. When differs exist but the setting is off, one muted line points to Settings. A "Revert file to original…" link (confirm dialog) appears whenever a backup exists. Settings gains the allow_pack_overwrite checkbox. Tests: tests/test_pack_overwrite.py — differs manual-only + identity-excluded + equal-skipped, refused when setting-off / not-manual / bad keys, dir + zip overwrite, .bak stays the pristine original across a second write, write_log records old+new (and gap-fills), prune at cap, revert dir+zip+404, demo blocks write & revert. 30 pass with test_gap_fill green. node --check clean; no new Tailwind classes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN
This commit is contained in:
co-authored by
Claude Fable 5
parent
f5c9c34291
commit
4db3db4622
+19
-1
@@ -49,6 +49,15 @@ def _apply_to_sloppak_manifest(manifest: dict, fields: dict) -> bool:
|
||||
if "year" in fields:
|
||||
manifest["year"] = _coerce_year(fields["year"])
|
||||
dirty = True
|
||||
# `genres` is the feedpak list field (spec 1.12.0). Accepts a list/tuple
|
||||
# (stringified item-wise) or a single name (wrapped); None leaves the
|
||||
# existing value alone, mirroring the string fields above. Sent by the
|
||||
# overwrite lane (R4b) — the manual Edit Metadata path never includes it.
|
||||
if fields.get("genres") is not None:
|
||||
raw = fields["genres"]
|
||||
manifest["genres"] = ([str(g) for g in raw]
|
||||
if isinstance(raw, (list, tuple)) else [str(raw)])
|
||||
dirty = True
|
||||
# Opportunistically declare the format version (spec §4) when we're already
|
||||
# rewriting because a metadata field was supplied. Gated on `dirty` (i.e. a
|
||||
# field was given) so this never forces a *standalone* rewrite with no fields
|
||||
@@ -102,7 +111,16 @@ def write_sloppak_metadata(path: Path, fields: dict) -> bool:
|
||||
mf = path / "manifest.yaml"
|
||||
if not mf.exists() and (path / "manifest.yml").exists():
|
||||
mf = path / "manifest.yml"
|
||||
mf.write_text(dumped, encoding="utf-8")
|
||||
# One-time backup + temp + atomic replace, exactly like the zip
|
||||
# rewriter and gap_fill_sloppak's dir branch: the FIRST backup is the
|
||||
# pristine author original and is never clobbered by a later write —
|
||||
# it's what "Revert file to original" (R4b) restores.
|
||||
backup = mf.with_name(mf.name + ".bak")
|
||||
if mf.exists() and not backup.exists():
|
||||
shutil.copy2(mf, backup)
|
||||
tmp = mf.with_name(mf.name + ".tmp")
|
||||
tmp.write_text(dumped, encoding="utf-8")
|
||||
tmp.replace(mf)
|
||||
return True
|
||||
return _rewrite_zip_manifest(path, dumped)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user