Three bugs made merge destructive and unrecoverable:
1. Primary-track slide paths were only prefix-swapped {sourceDir} → {targetDir}.
The promoted primary track got a NEW folder name
tracks/{lang}-{newTrackId}, but the slide filename kept the old
tracks/{lang}-{srcVideoId} segment, pointing at a folder that no longer
existed. Rewrite is now computed from the owning track's updated path, so
it survives regardless of what the stored filename used to look like.
2. Legacy-layout sources (primary audio directly at {sourceDir}/audio.ext,
slides at {sourceDir}/slides/) were silently skipped. The old code
hard-coded tracks/{lang-id}/ as the source location. Merge is now
layout-aware: it reads dirname($source->path) to pick between folder-move
(new layout) and per-file moves (legacy — audio, slides/ dir, thumb).
3. Lyrics were lost on legacy sources. {sourceDir}/lyrics/primary.json and
{sourceDir}/lyrics/track-{id}.json are now explicitly moved into the
target's per-track folders as lyrics.json, matching the new layout the
LyricsService reads from.
Added moveFile() helper (mirrors moveFolder on NAS + local, queues to
pending_nas_moves when NAS is unreachable). Verified against six scenarios
including new-layout, legacy, target-with-existing-extras, minimal, 2-extras
with slides and reversed-position lyrics source: all broken-file counts are
zero and slide position order is preserved verbatim.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>