diff --git a/go_backend/exports.go b/go_backend/exports.go index 1c5f84ed..e65b0197 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1694,6 +1694,8 @@ func ExtractCoverToFile(audioPath string, outputPath string) error { if strings.HasSuffix(lower, ".flac") { coverData, err = ExtractCoverArt(audioPath) + } else if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac") { + coverData, err = extractCoverFromM4A(audioPath) } else if strings.HasSuffix(lower, ".mp3") { coverData, _, err = extractMP3CoverArt(audioPath) } else if strings.HasSuffix(lower, ".opus") || strings.HasSuffix(lower, ".ogg") { diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 19783cc2..d4d28e37 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -663,6 +663,79 @@ func extractLyricsFromM4A(filePath string) (string, error) { return string(buf), nil } +func extractCoverFromM4A(filePath string) ([]byte, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, err + } + fileSize := fi.Size() + + moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) + if err != nil || !found { + return nil, 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 nil, 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 nil, fmt.Errorf("meta not found") + } + + 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 nil, fmt.Errorf("ilst not found") + } + + bodyStart = ilst.offset + ilst.headerSize + bodySize = ilst.size - ilst.headerSize + + covr, found, err := findAtomInRange(f, bodyStart, bodySize, "covr", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("cover atom not found") + } + + dataStart := covr.offset + covr.headerSize + dataSize := covr.size - covr.headerSize + + dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize) + if err != nil || !found { + return nil, fmt.Errorf("data atom not found in cover") + } + + // data atom: header + 4 bytes type indicator + 4 bytes locale + imgStart := dataAtom.offset + dataAtom.headerSize + 8 + imgLen := dataAtom.size - dataAtom.headerSize - 8 + if imgLen <= 0 { + return nil, fmt.Errorf("empty cover data") + } + + buf := make([]byte, imgLen) + if _, err := f.ReadAt(buf, imgStart); err != nil { + return nil, err + } + + return buf, nil +} + func extractLyricsFromSidecarLRC(filePath string) (string, error) { ext := filepath.Ext(filePath) base := strings.TrimSuffix(filePath, ext)