diff --git a/CHANGELOG.md b/CHANGELOG.md index e9da639..bbd3237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.20.44] — 2026-08-19 + +### Fixed +- **Voice metadata is now written atomically.** Interrupted saves cannot leave truncated `.meta.json` files that make ratings or voice settings disappear. + ## [1.20.43] — 2026-08-19 ### Added diff --git a/VERSION b/VERSION index ce07dea..6a36890 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.43 +1.20.44 diff --git a/core/voice.py b/core/voice.py index 17463cf..2f85be5 100644 --- a/core/voice.py +++ b/core/voice.py @@ -2,8 +2,10 @@ from __future__ import annotations import json +import os import re import shutil +import tempfile import time import uuid from datetime import datetime, timezone @@ -119,7 +121,24 @@ def _load_meta(wav: Path) -> dict: def _save_meta(wav: Path, meta: dict) -> None: - _meta_path(wav).write_text(json.dumps(meta, indent=2)) + # Write beside the final file and replace it atomically. A process exit or + # host/container interruption during write must never leave a truncated + # meta.json that makes ratings/settings appear to have vanished. + target = _meta_path(wav) + target.parent.mkdir(parents=True, exist_ok=True) + fd, tmp_name = tempfile.mkstemp(prefix=f".{target.name}.", suffix=".tmp", dir=target.parent) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + json.dump(meta, handle, indent=2, ensure_ascii=False) + handle.write("\n") + handle.flush() + os.fsync(handle.fileno()) + os.replace(tmp_name, target) + finally: + try: + os.unlink(tmp_name) + except FileNotFoundError: + pass def _picture_path(wav: Path) -> Path | None: diff --git a/static/index.html b/static/index.html index cf584f1..d68fdac 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) { - +