Write voice metadata atomically

This commit is contained in:
mARTin-B78 2026-08-19 18:39:11 +02:00
parent 11cd5b500c
commit e452b0f000
4 changed files with 29 additions and 5 deletions

View File

@ -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 ## [1.20.43] — 2026-08-19
### Added ### Added

View File

@ -1 +1 @@
1.20.43 1.20.44

View File

@ -2,8 +2,10 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import re import re
import shutil import shutil
import tempfile
import time import time
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
@ -119,7 +121,24 @@ def _load_meta(wav: Path) -> dict:
def _save_meta(wav: Path, meta: dict) -> None: 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: def _picture_path(wav: Path) -> Path | None:

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB"> <meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.20.43"> <meta name="app-version" content="1.20.44">
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml"> <link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg"> <link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.20.43"> <link rel="stylesheet" href="/static/style.css?v=1.20.44">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script> </script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton --> <!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.20.43"></script> <script src="/static/loader.js?v=1.20.44"></script>
</body> </body>
</html> </html>