embed & move covers, remove m4a support

This commit is contained in:
Gabe Farrell 2025-11-15 13:05:24 -05:00
parent 9de7dc83d8
commit 6480f93bb5
6 changed files with 193 additions and 3 deletions

164
media/media.go Normal file
View file

@ -0,0 +1,164 @@
package media
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
id3v2 "github.com/bogem/id3v2" // optional alternative
)
var coverNames = []string{
"cover.jpg", "cover.jpeg", "cover.png",
"folder.jpg", "folder.jpeg", "folder.png",
}
// EmbedAlbumArtIntoFolder scans one album folder and embeds cover art.
func EmbedAlbumArtIntoFolder(albumDir string) error {
coverFile, err := FindCoverImage(albumDir)
if err != nil {
fmt.Println("Could not find cover image. Skipping embed...")
return nil
}
coverData, err := os.ReadFile(coverFile)
if err != nil {
return fmt.Errorf("failed to read cover image: %w", err)
}
err = filepath.Walk(albumDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
lower := strings.ToLower(info.Name())
switch {
case strings.HasSuffix(lower, ".mp3"):
return embedCoverMP3(path, coverData)
case strings.HasSuffix(lower, ".flac"):
return embedCoverFLAC(path, coverData)
default:
return nil
}
})
return err
}
// -------------------------
// Find cover image
// -------------------------
func FindCoverImage(dir string) (string, error) {
entries, _ := os.ReadDir(dir)
for _, e := range entries {
if e.IsDir() {
continue
}
l := strings.ToLower(e.Name())
for _, name := range coverNames {
if l == name {
return filepath.Join(dir, e.Name()), nil
}
}
}
return "", fmt.Errorf("no cover image found in %s", dir)
}
// -------------------------
// Embed into MP3
// -------------------------
func embedCoverMP3(path string, cover []byte) error {
tag, err := id3v2.Open(path, id3v2.Options{Parse: true})
if err != nil {
return fmt.Errorf("mp3 open: %w", err)
}
defer tag.Close()
mime := guessMimeType(cover)
pic := id3v2.PictureFrame{
Encoding: id3v2.EncodingUTF8,
MimeType: mime,
PictureType: id3v2.PTFrontCover,
Description: "Cover",
Picture: cover,
}
tag.AddAttachedPicture(pic)
if err := tag.Save(); err != nil {
return fmt.Errorf("mp3 save: %w", err)
}
fmt.Println("→ Embedded art into MP3:", filepath.Base(path))
return nil
}
// embedCoverFLAC writes cover bytes to a tempfile and uses metaflac to import it.
// Requires `metaflac` (from the flac package) to be installed and in PATH.
func embedCoverFLAC(path string, cover []byte) error {
// Ensure metaflac exists
if _, err := exec.LookPath("metaflac"); err != nil {
return fmt.Errorf("metaflac not found in PATH; please install package 'flac' (provides metaflac): %w", err)
}
// Create a temp file for the cover image
tmp, err := os.CreateTemp("", "cover-*.img")
if err != nil {
return fmt.Errorf("creating temp file for cover: %w", err)
}
tmpPath := tmp.Name()
// Ensure we remove the temp file later
defer func() {
tmp.Close()
os.Remove(tmpPath)
}()
// Write cover bytes
if _, err := tmp.Write(cover); err != nil {
return fmt.Errorf("writing cover to temp file: %w", err)
}
if err := tmp.Sync(); err != nil {
// non-fatal, but report if it happens
return fmt.Errorf("sync temp cover file: %w", err)
}
// Remove existing PICTURE blocks (ignore non-zero exit -> continue, but report)
removeCmd := exec.Command("metaflac", "--remove", "--block-type=PICTURE", path)
removeOut, removeErr := removeCmd.CombinedOutput()
if removeErr != nil {
// metaflac returns non-zero if there were no picture blocks — that's OK.
// Only fail if it's some unexpected error.
// We'll print the output for debugging and continue.
fmt.Printf("metaflac --remove output (may be fine): %s\n", string(removeOut))
}
// Import the new picture. metaflac will auto-detect mime type from the file.
importCmd := exec.Command("metaflac", "--import-picture-from="+tmpPath, path)
importOut, importErr := importCmd.CombinedOutput()
if importErr != nil {
return fmt.Errorf("metaflac --import-picture-from failed: %v; output: %s", importErr, string(importOut))
}
fmt.Println("→ Embedded art into FLAC:", filepath.Base(path))
return nil
}
// -------------------------
// Helpers
// -------------------------
func guessMimeType(data []byte) string {
if bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}) {
return "image/jpeg"
}
if bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47}) {
return "image/png"
}
return "image/jpeg" // fallback
}