fix(metadata): m4b text-track fallback; in-app confirm; save-first write

Three defects from real-world use:

- The m4b rewrite assumed every stream survives -map 0 -c copy; files
  carrying text-ish tracks (the movenc 'text' fourcc) fail the ipod
  muxer with "Tag text incompatible with output codec id" — exactly the
  error a write attempt hit in the field. The rewrite now attempts full
  fidelity first, then retries mapping audio + attached pictures only
  (-dn -sn): chapters live in the container header (-map_chapters 0)
  so they survive; every attempt still ffprobe-validates before the
  atomic rename and the original is never touched on total failure.

- The browser confirm() is replaced by an in-app dialog
  (WriteToFileConfirmDialog, Alpine + templ — no native dialogs).

- "Write to file" now SAVES the pending editor edits first (persisted
  through the same PUT the Save button uses), then writes the file
  from those DB values, and closes the editor + refreshes the page
  exactly like Save — previously the endpoint wrote the LAST-SAVED DB
  values, so edits sitting in the modal never reached the file, and
  the page didn't visibly update.

Test coverage per the owner's bar: EVERY single-file container (MP3,
FLAC, M4A, M4B) gets its own runtime-generated fixture, gets written,
and the FILE's metadata is then verified independently — ffprobe reads
the rewritten container and every mapped key is asserted per format,
including the subtitle round-trip split (MP3/FLAC yes, M4A/B no) — and
the EPUB/CBZ/sidecar assertions now PARSE the written XML (dc titles/
creators, ComicInfo fields, calibre meta) instead of substring
matching. A smart-genre unit test also pins the mixed-blob contract: a
recognized shelf wins the genre column even when demoted format words
ride along in the same blob.
This commit is contained in:
John O'Keefe committed 2026-10-27 19:05:23 -04:00
1 parent 96c1228805
commit fb1645c90c
8 files changed
+396 -152

No files matched your search

+49
View File
@@ -716,3 +716,52 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</div>
</div>
}
// WriteToFileConfirmDialog is the in-app replacement for the browser
// confirm() on the metadata editor's "Write to file" action (CSP and
// UX: no native dialogs). The target-specific copy comes from the
// bookDetail Alpine component (same scope as the editor modal).
templ WriteToFileConfirmDialog(book handlers.MediaDetail) {
<div
id="write-confirm-dialog"
x-show="writeConfirmOpen"
x-cloak
x-transition.opacity
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
style="background-color: var(--surface-overlay);"
>
<div class="card w-full max-w-md p-6" style="box-shadow: var(--shadow-pop);">
<div class="flex items-center gap-3 mb-4">
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("save", "h-5 w-5")
</span>
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Write metadata to file?</h3>
</div>
<p class="text-sm mb-2" style="color: var(--text-secondary)" x-text="writeConfirmMessage()"></p>
<p class="text-xs mb-6" style="color: var(--text-secondary)">
Your pending edits are saved first, then the file is rewritten and re-scanned.
</p>
<div class="flex justify-end gap-3">
<button
type="button"
@click="writeConfirmOpen = false"
class="btn btn-secondary"
>
Cancel
</button>
<button
type="button"
@click="performWriteToFile()"
:disabled="writingFile"
class="btn btn-primary"
>
<span x-show="!writingFile" class="inline-flex items-center gap-2">@Icon("save", "h-4 w-4")<span>Save &amp; write</span></span>
<svg x-show="writingFile" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</button>
</div>
</div>
</div>
}