Files
SpotiFLAC/backend/metadata.go
T
Sepehr Aroofzade 76669f551e Add FLAC lyrics embedding with LRCLIB fallback (#151)
* Update lyrics code

* Refactor DownloadFile to use default HTTP client
2025-12-13 04:09:22 +07:00

250 lines
5.6 KiB
Go

package backend
import (
"fmt"
"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
Date string
TrackNumber int
DiscNumber int
ISRC string
Lyrics 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 = -1
for idx, block := range f.Meta {
if block.Type == flac.VorbisComment {
cmtIdx = idx
break
}
}
cmt := flacvorbis.New()
if metadata.Title != "" {
_ = cmt.Add(flacvorbis.FIELD_TITLE, metadata.Title)
}
if metadata.Artist != "" {
_ = cmt.Add(flacvorbis.FIELD_ARTIST, metadata.Artist)
}
if metadata.Album != "" {
_ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album)
}
if metadata.Date != "" {
_ = cmt.Add(flacvorbis.FIELD_DATE, metadata.Date)
}
if metadata.TrackNumber > 0 {
_ = cmt.Add(flacvorbis.FIELD_TRACKNUMBER, strconv.Itoa(metadata.TrackNumber))
}
if metadata.DiscNumber > 0 {
_ = cmt.Add("DISCNUMBER", strconv.Itoa(metadata.DiscNumber))
}
if metadata.ISRC != "" {
_ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC)
}
if metadata.Lyrics != "" {
_ = cmt.Add("LYRICS", metadata.Lyrics) // Or "UNSYNCEDLYRICS" for unsynced
}
cmtBlock := cmt.Marshal()
if cmtIdx < 0 {
f.Meta = append(f.Meta, &cmtBlock)
} else {
f.Meta[cmtIdx] = &cmtBlock
}
if coverPath != "" && fileExists(coverPath) {
if err := embedCoverArt(f, coverPath); err != nil {
fmt.Printf("Warning: Failed to embed cover art: %v\n", err)
}
}
if err := f.Save(filepath); err != nil {
return fmt.Errorf("failed to save FLAC file: %w", err)
}
return nil
}
func embedCoverArt(f *flac.File, coverPath string) error {
imgData, err := os.ReadFile(coverPath)
if err != nil {
return fmt.Errorf("failed to read cover image: %w", err)
}
picture, err := flacpicture.NewFromImageData(
flacpicture.PictureTypeFrontCover,
"Cover",
imgData,
"image/jpeg",
)
if err != nil {
return fmt.Errorf("failed to create picture block: %w", err)
}
pictureBlock := picture.Marshal()
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:]...)
}
}
f.Meta = append(f.Meta, &pictureBlock)
return nil
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// EmbedLyricsOnly adds lyrics to a FLAC file while preserving existing metadata
func EmbedLyricsOnly(filepath string, lyrics string) error {
if lyrics == "" {
return nil
}
f, err := flac.ParseFile(filepath)
if err != nil {
return fmt.Errorf("failed to parse FLAC file: %w", err)
}
var cmtIdx = -1
var existingCmt *flacvorbis.MetaDataBlockVorbisComment
for idx, block := range f.Meta {
if block.Type == flac.VorbisComment {
cmtIdx = idx
existingCmt, err = flacvorbis.ParseFromMetaDataBlock(*block)
if err != nil {
existingCmt = nil
}
break
}
}
// Create new comment block, preserving existing comments
cmt := flacvorbis.New()
// Copy existing comments except LYRICS
if existingCmt != nil {
for _, comment := range existingCmt.Comments {
parts := strings.SplitN(comment, "=", 2)
if len(parts) == 2 {
fieldName := strings.ToUpper(parts[0])
if fieldName != "LYRICS" && fieldName != "UNSYNCEDLYRICS" && fieldName != "SYNCEDLYRICS" {
_ = cmt.Add(parts[0], parts[1])
}
}
}
}
// Add lyrics
_ = cmt.Add("LYRICS", lyrics)
cmtBlock := cmt.Marshal()
if cmtIdx < 0 {
f.Meta = append(f.Meta, &cmtBlock)
} else {
f.Meta[cmtIdx] = &cmtBlock
}
if err := f.Save(filepath); err != nil {
return fmt.Errorf("failed to save FLAC file: %w", err)
}
return nil
}
// ReadISRCFromFile reads ISRC metadata from a FLAC file
func ReadISRCFromFile(filepath string) (string, error) {
if !fileExists(filepath) {
return "", fmt.Errorf("file does not exist")
}
f, err := flac.ParseFile(filepath)
if err != nil {
return "", fmt.Errorf("failed to parse FLAC file: %w", err)
}
// Find VorbisComment block
for _, block := range f.Meta {
if block.Type == flac.VorbisComment {
cmt, err := flacvorbis.ParseFromMetaDataBlock(*block)
if err != nil {
continue
}
// Get ISRC field
isrcValues, err := cmt.Get(flacvorbis.FIELD_ISRC)
if err == nil && len(isrcValues) > 0 {
return isrcValues[0], nil
}
}
}
return "", nil // No ISRC found
}
// CheckISRCExists checks if a file with the given ISRC already exists in the directory
func CheckISRCExists(outputDir string, targetISRC string) (string, bool) {
if targetISRC == "" {
return "", false
}
// Read all .flac files in directory
entries, err := os.ReadDir(outputDir)
if err != nil {
return "", false
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
// Check only .flac files
filename := entry.Name()
if len(filename) < 5 || filename[len(filename)-5:] != ".flac" {
continue
}
filepath := fmt.Sprintf("%s/%s", outputDir, filename)
// Read ISRC from file (this will fail for corrupted files)
isrc, err := ReadISRCFromFile(filepath)
if err != nil {
// File is corrupted or unreadable, delete it
fmt.Printf("Removing corrupted/unreadable file: %s (error: %v)\n", filepath, err)
if removeErr := os.Remove(filepath); removeErr != nil {
fmt.Printf("Warning: Failed to remove corrupted file %s: %v\n", filepath, removeErr)
}
continue
}
// Compare ISRC (case-insensitive)
if isrc != "" && strings.EqualFold(isrc, targetISRC) {
return filepath, true
}
}
return "", false
}