package gobackend import ( "bytes" "encoding/binary" "fmt" "io" "os" "strconv" "strings" "github.com/go-flac/flacpicture" "github.com/go-flac/flacvorbis" "github.com/go-flac/go-flac" ) type Metadata struct { Title string Artist string Album string AlbumArtist string Date string TrackNumber int TotalTracks int DiscNumber int ISRC string Description string Lyrics string Genre string Label string Copyright string } func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { f, err := flac.ParseFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment for idx, meta := range f.Meta { if meta.Type == flac.VorbisComment { cmtIdx = idx cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { return fmt.Errorf("failed to parse vorbis comment: %w", err) } break } } if cmt == nil { cmt = flacvorbis.New() } setComment(cmt, "TITLE", metadata.Title) setComment(cmt, "ARTIST", metadata.Artist) setComment(cmt, "ALBUM", metadata.Album) setComment(cmt, "ALBUMARTIST", metadata.AlbumArtist) setComment(cmt, "DATE", metadata.Date) if metadata.TrackNumber > 0 { if metadata.TotalTracks > 0 { setComment(cmt, "TRACKNUMBER", fmt.Sprintf("%d/%d", metadata.TrackNumber, metadata.TotalTracks)) } else { setComment(cmt, "TRACKNUMBER", strconv.Itoa(metadata.TrackNumber)) } } if metadata.DiscNumber > 0 { setComment(cmt, "DISCNUMBER", strconv.Itoa(metadata.DiscNumber)) } if metadata.ISRC != "" { setComment(cmt, "ISRC", metadata.ISRC) } if metadata.Description != "" { setComment(cmt, "DESCRIPTION", metadata.Description) } if metadata.Lyrics != "" { setComment(cmt, "LYRICS", metadata.Lyrics) setComment(cmt, "UNSYNCEDLYRICS", metadata.Lyrics) } if metadata.Genre != "" { setComment(cmt, "GENRE", metadata.Genre) } if metadata.Label != "" { setComment(cmt, "ORGANIZATION", metadata.Label) } if metadata.Copyright != "" { setComment(cmt, "COPYRIGHT", metadata.Copyright) } cmtBlock := cmt.Marshal() if cmtIdx >= 0 { f.Meta[cmtIdx] = &cmtBlock } else { f.Meta = append(f.Meta, &cmtBlock) } if coverPath != "" { if fileExists(coverPath) { coverData, err := os.ReadFile(coverPath) if err != nil { fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err) } else { for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) } } picture, err := flacpicture.NewFromImageData( flacpicture.PictureTypeFrontCover, "Front Cover", coverData, "image/jpeg", ) if err != nil { fmt.Printf("[Metadata] Warning: Failed to create picture block: %v\n", err) } else { picBlock := picture.Marshal() f.Meta = append(f.Meta, &picBlock) fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) } } } else { fmt.Printf("[Metadata] Warning: Cover file does not exist: %s\n", coverPath) } } return f.Save(filePath) } func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []byte) error { f, err := flac.ParseFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment for idx, meta := range f.Meta { if meta.Type == flac.VorbisComment { cmtIdx = idx cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { return fmt.Errorf("failed to parse vorbis comment: %w", err) } break } } if cmt == nil { cmt = flacvorbis.New() } setComment(cmt, "TITLE", metadata.Title) setComment(cmt, "ARTIST", metadata.Artist) setComment(cmt, "ALBUM", metadata.Album) setComment(cmt, "ALBUMARTIST", metadata.AlbumArtist) setComment(cmt, "DATE", metadata.Date) if metadata.TrackNumber > 0 { if metadata.TotalTracks > 0 { setComment(cmt, "TRACKNUMBER", fmt.Sprintf("%d/%d", metadata.TrackNumber, metadata.TotalTracks)) } else { setComment(cmt, "TRACKNUMBER", strconv.Itoa(metadata.TrackNumber)) } } if metadata.DiscNumber > 0 { setComment(cmt, "DISCNUMBER", strconv.Itoa(metadata.DiscNumber)) } if metadata.ISRC != "" { setComment(cmt, "ISRC", metadata.ISRC) } if metadata.Description != "" { setComment(cmt, "DESCRIPTION", metadata.Description) } if metadata.Lyrics != "" { setComment(cmt, "LYRICS", metadata.Lyrics) setComment(cmt, "UNSYNCEDLYRICS", metadata.Lyrics) } if metadata.Genre != "" { setComment(cmt, "GENRE", metadata.Genre) } if metadata.Label != "" { setComment(cmt, "ORGANIZATION", metadata.Label) } if metadata.Copyright != "" { setComment(cmt, "COPYRIGHT", metadata.Copyright) } cmtBlock := cmt.Marshal() if cmtIdx >= 0 { f.Meta[cmtIdx] = &cmtBlock } else { f.Meta = append(f.Meta, &cmtBlock) } if len(coverData) > 0 { for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) } } picture, err := flacpicture.NewFromImageData( flacpicture.PictureTypeFrontCover, "Front Cover", coverData, "image/jpeg", ) if err != nil { fmt.Printf("[Metadata] Warning: Failed to create picture block: %v\n", err) } else { picBlock := picture.Marshal() f.Meta = append(f.Meta, &picBlock) fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData)) } } return f.Save(filePath) } func ReadMetadata(filePath string) (*Metadata, error) { f, err := flac.ParseFile(filePath) if err != nil { return nil, fmt.Errorf("failed to parse FLAC file: %w", err) } metadata := &Metadata{} for _, meta := range f.Meta { if meta.Type == flac.VorbisComment { cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { continue } metadata.Title = getComment(cmt, "TITLE") metadata.Artist = getComment(cmt, "ARTIST") metadata.Album = getComment(cmt, "ALBUM") metadata.AlbumArtist = getComment(cmt, "ALBUMARTIST") metadata.Date = getComment(cmt, "DATE") metadata.ISRC = getComment(cmt, "ISRC") metadata.Description = getComment(cmt, "DESCRIPTION") metadata.Lyrics = getComment(cmt, "LYRICS") if metadata.Lyrics == "" { metadata.Lyrics = getComment(cmt, "UNSYNCEDLYRICS") } trackNum := getComment(cmt, "TRACKNUMBER") if trackNum != "" { fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber) } if metadata.TrackNumber == 0 { trackNum = getComment(cmt, "TRACK") if trackNum != "" { fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber) } } discNum := getComment(cmt, "DISCNUMBER") if discNum != "" { fmt.Sscanf(discNum, "%d", &metadata.DiscNumber) } if metadata.DiscNumber == 0 { discNum = getComment(cmt, "DISC") if discNum != "" { fmt.Sscanf(discNum, "%d", &metadata.DiscNumber) } } if metadata.Date == "" { metadata.Date = getComment(cmt, "YEAR") } break } } return metadata, nil } func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) { if value == "" { return } keyUpper := strings.ToUpper(key) for i := len(cmt.Comments) - 1; i >= 0; i-- { comment := cmt.Comments[i] eqIdx := strings.Index(comment, "=") if eqIdx > 0 { existingKey := strings.ToUpper(comment[:eqIdx]) if existingKey == keyUpper { cmt.Comments = append(cmt.Comments[:i], cmt.Comments[i+1:]...) } } } cmt.Comments = append(cmt.Comments, key+"="+value) } func getComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string { keyUpper := strings.ToUpper(key) + "=" for _, comment := range cmt.Comments { if len(comment) > len(key) { commentUpper := strings.ToUpper(comment[:len(key)+1]) if commentUpper == keyUpper { return comment[len(key)+1:] } } } return "" } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func ExtractCoverArt(filePath string) ([]byte, error) { f, err := flac.ParseFile(filePath) if err != nil { return nil, fmt.Errorf("failed to parse FLAC file: %w", err) } for _, meta := range f.Meta { if meta.Type == flac.Picture { pic, err := flacpicture.ParseFromMetaDataBlock(*meta) if err != nil { continue } if pic.PictureType == flacpicture.PictureTypeFrontCover && len(pic.ImageData) > 0 { return pic.ImageData, nil } } } for _, meta := range f.Meta { if meta.Type == flac.Picture { pic, err := flacpicture.ParseFromMetaDataBlock(*meta) if err != nil { continue } if len(pic.ImageData) > 0 { return pic.ImageData, nil } } } return nil, fmt.Errorf("no cover art found in file") } func EmbedLyrics(filePath string, lyrics string) error { f, err := flac.ParseFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment for idx, meta := range f.Meta { if meta.Type == flac.VorbisComment { cmtIdx = idx cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { return fmt.Errorf("failed to parse vorbis comment: %w", err) } break } } if cmt == nil { cmt = flacvorbis.New() } setComment(cmt, "LYRICS", lyrics) setComment(cmt, "UNSYNCEDLYRICS", lyrics) cmtBlock := cmt.Marshal() if cmtIdx >= 0 { f.Meta[cmtIdx] = &cmtBlock } else { f.Meta = append(f.Meta, &cmtBlock) } return f.Save(filePath) } func EmbedGenreLabel(filePath string, genre, label string) error { if genre == "" && label == "" { return nil } f, err := flac.ParseFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment for idx, meta := range f.Meta { if meta.Type == flac.VorbisComment { cmtIdx = idx cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { return fmt.Errorf("failed to parse vorbis comment: %w", err) } break } } if cmt == nil { cmt = flacvorbis.New() } if genre != "" { setComment(cmt, "GENRE", genre) } if label != "" { setComment(cmt, "ORGANIZATION", label) } cmtBlock := cmt.Marshal() if cmtIdx >= 0 { f.Meta[cmtIdx] = &cmtBlock } else { f.Meta = append(f.Meta, &cmtBlock) } return f.Save(filePath) } func ExtractLyrics(filePath string) (string, error) { f, err := flac.ParseFile(filePath) if err != nil { return "", fmt.Errorf("failed to parse FLAC file: %w", err) } for _, meta := range f.Meta { if meta.Type == flac.VorbisComment { cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta) if err != nil { continue } lyrics, err := cmt.Get("LYRICS") if err == nil && len(lyrics) > 0 && lyrics[0] != "" { return lyrics[0], nil } lyrics, err = cmt.Get("UNSYNCEDLYRICS") if err == nil && len(lyrics) > 0 && lyrics[0] != "" { return lyrics[0], nil } } } return "", fmt.Errorf("no lyrics found in file") } type AudioQuality struct { BitDepth int `json:"bit_depth"` SampleRate int `json:"sample_rate"` TotalSamples int64 `json:"total_samples"` } func GetAudioQuality(filePath string) (AudioQuality, error) { file, err := os.Open(filePath) if err != nil { return AudioQuality{}, fmt.Errorf("failed to open file: %w", err) } defer file.Close() marker := make([]byte, 4) if _, err := file.Read(marker); err != nil { return AudioQuality{}, fmt.Errorf("failed to read marker: %w", err) } if string(marker) == "fLaC" { header := make([]byte, 4) if _, err := file.Read(header); err != nil { return AudioQuality{}, fmt.Errorf("failed to read header: %w", err) } blockType := header[0] & 0x7F if blockType != 0 { return AudioQuality{}, fmt.Errorf("first block is not STREAMINFO") } streamInfo := make([]byte, 34) if _, err := file.Read(streamInfo); err != nil { return AudioQuality{}, fmt.Errorf("failed to read STREAMINFO: %w", err) } sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4) bitsPerSample := ((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4) + 1 totalSamples := int64(streamInfo[13]&0x0F)<<32 | int64(streamInfo[14])<<24 | int64(streamInfo[15])<<16 | int64(streamInfo[16])<<8 | int64(streamInfo[17]) return AudioQuality{ BitDepth: bitsPerSample, SampleRate: sampleRate, TotalSamples: totalSamples, }, nil } file.Seek(0, 0) header8 := make([]byte, 8) if _, err := file.Read(header8); err != nil { return AudioQuality{}, fmt.Errorf("failed to read header: %w", err) } if string(header8[4:8]) == "ftyp" { file.Close() return GetM4AQuality(filePath) } return AudioQuality{}, fmt.Errorf("unsupported file format (not FLAC or M4A)") } func GetM4AQuality(filePath string) (AudioQuality, error) { f, err := os.Open(filePath) if err != nil { return AudioQuality{}, fmt.Errorf("failed to open M4A file: %w", err) } defer f.Close() info, err := f.Stat() if err != nil { return AudioQuality{}, fmt.Errorf("failed to stat M4A file: %w", err) } fileSize := info.Size() moovHeader, moovFound, err := findAtomInRange(f, 0, fileSize, "moov", fileSize) if err != nil { return AudioQuality{}, fmt.Errorf("failed to find moov atom: %w", err) } if !moovFound { return AudioQuality{}, fmt.Errorf("moov atom not found") } moovStart := moovHeader.offset moovEnd := moovHeader.offset + moovHeader.size sampleOffset, atomType, err := findAudioSampleEntry(f, moovStart, moovEnd, fileSize) if err != nil { return AudioQuality{}, err } buf := make([]byte, 24) if _, err := f.ReadAt(buf, sampleOffset); err != nil { return AudioQuality{}, fmt.Errorf("failed to read audio sample entry: %w", err) } sampleRate := int(buf[22])<<8 | int(buf[23]) bitDepth := 16 if atomType == "alac" { bitDepth = 24 } return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil } type atomHeader struct { offset int64 size int64 headerSize int64 typ string } func readAtomHeaderAt(f *os.File, offset, fileSize int64) (atomHeader, error) { if offset+8 > fileSize { return atomHeader{}, io.ErrUnexpectedEOF } headerBuf := make([]byte, 8) if _, err := f.ReadAt(headerBuf, offset); err != nil { return atomHeader{}, err } size32 := binary.BigEndian.Uint32(headerBuf[0:4]) typ := string(headerBuf[4:8]) if size32 == 1 { if offset+16 > fileSize { return atomHeader{}, io.ErrUnexpectedEOF } extBuf := make([]byte, 8) if _, err := f.ReadAt(extBuf, offset+8); err != nil { return atomHeader{}, err } size64 := binary.BigEndian.Uint64(extBuf) return atomHeader{offset: offset, size: int64(size64), headerSize: 16, typ: typ}, nil } return atomHeader{offset: offset, size: int64(size32), headerSize: 8, typ: typ}, nil } func findAtomInRange(f *os.File, start, size int64, target string, fileSize int64) (atomHeader, bool, error) { if size <= 0 { return atomHeader{}, false, nil } end := start + size pos := start for pos+8 <= end { header, err := readAtomHeaderAt(f, pos, fileSize) if err != nil { return atomHeader{}, false, err } atomSize := header.size if atomSize == 0 { atomSize = end - pos } if atomSize < header.headerSize { return atomHeader{}, false, fmt.Errorf("invalid atom size for %s", header.typ) } header.size = atomSize if header.typ == target { return header, true, nil } pos += atomSize } return atomHeader{}, false, nil } func findAudioSampleEntry(f *os.File, start, end, fileSize int64) (int64, string, error) { const chunkSize = 64 * 1024 patternMP4A := []byte("mp4a") patternALAC := []byte("alac") var tail []byte readPos := start for readPos < end { toRead := end - readPos if toRead > chunkSize { toRead = chunkSize } buf := make([]byte, toRead) n, err := f.ReadAt(buf, readPos) if err != nil && err != io.EOF { return 0, "", fmt.Errorf("failed to read M4A atom data: %w", err) } if n == 0 { break } data := append(tail, buf[:n]...) mp4aIdx := bytes.Index(data, patternMP4A) alacIdx := bytes.Index(data, patternALAC) bestIdx := -1 bestType := "" switch { case mp4aIdx >= 0 && alacIdx >= 0: if mp4aIdx <= alacIdx { bestIdx = mp4aIdx bestType = "mp4a" } else { bestIdx = alacIdx bestType = "alac" } case mp4aIdx >= 0: bestIdx = mp4aIdx bestType = "mp4a" case alacIdx >= 0: bestIdx = alacIdx bestType = "alac" } if bestIdx >= 0 { absolute := readPos - int64(len(tail)) + int64(bestIdx) if absolute+24 > fileSize { return 0, "", fmt.Errorf("audio info not found in M4A file") } return absolute, bestType, nil } if len(data) >= 3 { tail = append([]byte{}, data[len(data)-3:]...) } else { tail = append([]byte{}, data...) } readPos += int64(n) } return 0, "", fmt.Errorf("audio info not found in M4A file") }