From 7d8cf5f7ca8f9144c8b64bacca787b2ed1fa58d6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 16 Mar 2026 02:43:13 +0700 Subject: [PATCH] fix: detect embedded lyrics in M4A/ALAC files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add extractLyricsFromM4A() that walks the MP4 box tree (moov/udta/meta/ilst/©lyr) to read lyrics. Wire it into ExtractLyrics so the Embed Lyrics button is hidden when lyrics already exist in the file. --- go_backend/metadata.go | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 391b7e5d..19783cc2 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -552,6 +552,14 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } + if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac") { + lyrics, err := extractLyricsFromM4A(filePath) + if err == nil && strings.TrimSpace(lyrics) != "" { + return lyrics, nil + } + return extractLyricsFromSidecarLRC(filePath) + } + if strings.HasSuffix(lower, ".mp3") { meta, err := ReadID3Tags(filePath) if err == nil && meta != nil { @@ -581,6 +589,80 @@ func ExtractLyrics(filePath string) (string, error) { return extractLyricsFromSidecarLRC(filePath) } +func extractLyricsFromM4A(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return "", err + } + fileSize := fi.Size() + + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return "", fmt.Errorf("moov not found") + } + + bodyStart := moov.offset + moov.headerSize + bodySize := moov.size - moov.headerSize + + udta, found, err := findAtomInRange(f, bodyStart, bodySize, "udta", fileSize) + if err != nil || !found { + return "", fmt.Errorf("udta not found") + } + + bodyStart = udta.offset + udta.headerSize + bodySize = udta.size - udta.headerSize + + meta, found, err := findAtomInRange(f, bodyStart, bodySize, "meta", fileSize) + if err != nil || !found { + return "", fmt.Errorf("meta not found") + } + + // meta atom has 4-byte version/flags after the header + bodyStart = meta.offset + meta.headerSize + 4 + bodySize = meta.size - meta.headerSize - 4 + + ilst, found, err := findAtomInRange(f, bodyStart, bodySize, "ilst", fileSize) + if err != nil || !found { + return "", fmt.Errorf("ilst not found") + } + + bodyStart = ilst.offset + ilst.headerSize + bodySize = ilst.size - ilst.headerSize + + lyr, found, err := findAtomInRange(f, bodyStart, bodySize, "\xa9lyr", fileSize) + if err != nil || !found { + return "", fmt.Errorf("lyrics atom not found") + } + + dataStart := lyr.offset + lyr.headerSize + dataSize := lyr.size - lyr.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return "", fmt.Errorf("data atom not found in lyrics") + } + + // data atom: 8 bytes header + 4 bytes type indicator + 4 bytes locale = skip 8 + textStart := dataAtom.offset + dataAtom.headerSize + 8 + textLen := dataAtom.size - dataAtom.headerSize - 8 + if textLen <= 0 { + return "", fmt.Errorf("empty lyrics") + } + + buf := make([]byte, textLen) + if _, err := f.ReadAt(buf, textStart); err != nil { + return "", err + } + + return string(buf), nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext)