76669f551e
* Update lyrics code * Refactor DownloadFile to use default HTTP client
250 lines
5.6 KiB
Go
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
|
|
}
|