v6.8
This commit is contained in:
@@ -3,9 +3,11 @@ package backend
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
pathfilepath "path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
id3v2 "github.com/bogem/id3v2/v2"
|
||||
"github.com/go-flac/flacpicture"
|
||||
"github.com/go-flac/flacvorbis"
|
||||
"github.com/go-flac/go-flac"
|
||||
@@ -247,3 +249,262 @@ func CheckISRCExists(outputDir string, targetISRC string) (string, bool) {
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ExtractCoverArt extracts cover art from an audio file and saves it to a temporary file
|
||||
func ExtractCoverArt(filePath string) (string, error) {
|
||||
ext := strings.ToLower(pathfilepath.Ext(filePath))
|
||||
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return extractCoverFromMp3(filePath)
|
||||
case ".m4a", ".flac":
|
||||
return extractCoverFromM4AOrFlac(filePath)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported file format: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// extractCoverFromMp3 extracts cover art from MP3 file
|
||||
func extractCoverFromMp3(filePath string) (string, error) {
|
||||
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open MP3 file: %w", err)
|
||||
}
|
||||
defer tag.Close()
|
||||
|
||||
pictures := tag.GetFrames(tag.CommonID("Attached picture"))
|
||||
if len(pictures) == 0 {
|
||||
return "", fmt.Errorf("no cover art found")
|
||||
}
|
||||
|
||||
pic, ok := pictures[0].(id3v2.PictureFrame)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid picture frame")
|
||||
}
|
||||
|
||||
// Create temporary file
|
||||
tmpFile, err := os.CreateTemp("", "cover-*.jpg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
if _, err := tmpFile.Write(pic.Picture); err != nil {
|
||||
os.Remove(tmpFile.Name())
|
||||
return "", fmt.Errorf("failed to write cover art: %w", err)
|
||||
}
|
||||
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
// extractCoverFromM4AOrFlac extracts cover art from M4A or FLAC file
|
||||
func extractCoverFromM4AOrFlac(filePath string) (string, error) {
|
||||
ext := strings.ToLower(pathfilepath.Ext(filePath))
|
||||
|
||||
if ext == ".flac" {
|
||||
f, err := flac.ParseFile(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse FLAC file: %w", err)
|
||||
}
|
||||
|
||||
for _, block := range f.Meta {
|
||||
if block.Type == flac.Picture {
|
||||
pic, err := flacpicture.ParseFromMetaDataBlock(*block)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create temporary file
|
||||
tmpFile, err := os.CreateTemp("", "cover-*.jpg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
if _, err := tmpFile.Write(pic.ImageData); err != nil {
|
||||
os.Remove(tmpFile.Name())
|
||||
return "", fmt.Errorf("failed to write cover art: %w", err)
|
||||
}
|
||||
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no cover art found")
|
||||
}
|
||||
|
||||
// For M4A, try to extract using ffmpeg or return empty
|
||||
// M4A cover art should be preserved by ffmpeg during conversion
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ExtractLyrics extracts lyrics from an audio file
|
||||
func ExtractLyrics(filePath string) (string, error) {
|
||||
ext := strings.ToLower(pathfilepath.Ext(filePath))
|
||||
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return extractLyricsFromMp3(filePath)
|
||||
case ".flac":
|
||||
return extractLyricsFromFlac(filePath)
|
||||
case ".m4a":
|
||||
// M4A lyrics extraction would need different approach
|
||||
return "", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported file format: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// extractLyricsFromMp3 extracts lyrics from MP3 file
|
||||
func extractLyricsFromMp3(filePath string) (string, error) {
|
||||
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open MP3 file: %w", err)
|
||||
}
|
||||
defer tag.Close()
|
||||
|
||||
usltFrames := tag.GetFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
|
||||
if len(usltFrames) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
uslt, ok := usltFrames[0].(id3v2.UnsynchronisedLyricsFrame)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return uslt.Lyrics, nil
|
||||
}
|
||||
|
||||
// extractLyricsFromFlac extracts lyrics from FLAC file
|
||||
func extractLyricsFromFlac(filePath string) (string, error) {
|
||||
f, err := flac.ParseFile(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse FLAC file: %w", err)
|
||||
}
|
||||
|
||||
for _, block := range f.Meta {
|
||||
if block.Type == flac.VorbisComment {
|
||||
cmt, err := flacvorbis.ParseFromMetaDataBlock(*block)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Search through comments for lyrics
|
||||
for _, comment := range cmt.Comments {
|
||||
parts := strings.SplitN(comment, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
fieldName := strings.ToUpper(parts[0])
|
||||
if fieldName == "LYRICS" || fieldName == "UNSYNCEDLYRICS" {
|
||||
return parts[1], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// EmbedCoverArtOnly embeds cover art into an audio file
|
||||
func EmbedCoverArtOnly(filePath string, coverPath string) error {
|
||||
if coverPath == "" || !fileExists(coverPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := strings.ToLower(pathfilepath.Ext(filePath))
|
||||
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return embedCoverToMp3(filePath, coverPath)
|
||||
case ".m4a":
|
||||
// M4A cover art should be handled by ffmpeg during conversion
|
||||
// If not, we can try to embed using atomicparsley or similar tool
|
||||
// For now, return nil as ffmpeg should handle it
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported file format: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// embedCoverToMp3 embeds cover art into MP3 file
|
||||
func embedCoverToMp3(filePath string, coverPath string) error {
|
||||
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open MP3 file: %w", err)
|
||||
}
|
||||
defer tag.Close()
|
||||
|
||||
// Remove existing cover art
|
||||
tag.DeleteFrames(tag.CommonID("Attached picture"))
|
||||
|
||||
// Read cover art
|
||||
artwork, err := os.ReadFile(coverPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read cover art: %w", err)
|
||||
}
|
||||
|
||||
// Add new cover art
|
||||
pic := id3v2.PictureFrame{
|
||||
Encoding: id3v2.EncodingUTF8,
|
||||
MimeType: "image/jpeg",
|
||||
PictureType: id3v2.PTFrontCover,
|
||||
Description: "Front cover",
|
||||
Picture: artwork,
|
||||
}
|
||||
tag.AddAttachedPicture(pic)
|
||||
|
||||
if err := tag.Save(); err != nil {
|
||||
return fmt.Errorf("failed to save MP3 tags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmbedLyricsOnlyMP3 adds lyrics to an MP3 file using ID3v2 USLT frame
|
||||
func EmbedLyricsOnlyMP3(filepath string, lyrics string) error {
|
||||
if lyrics == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tag, err := id3v2.Open(filepath, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open MP3 file: %w", err)
|
||||
}
|
||||
defer tag.Close()
|
||||
|
||||
// Remove existing USLT frames
|
||||
tag.DeleteFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
|
||||
|
||||
// Add new USLT frame with lyrics
|
||||
// Use UTF-8 encoding for better compatibility with AIMP and other players
|
||||
usltFrame := id3v2.UnsynchronisedLyricsFrame{
|
||||
Encoding: id3v2.EncodingUTF8, // Use UTF-8 instead of default encoding
|
||||
Language: "eng",
|
||||
ContentDescriptor: "", // Empty descriptor for better compatibility
|
||||
Lyrics: lyrics,
|
||||
}
|
||||
tag.AddUnsynchronisedLyricsFrame(usltFrame)
|
||||
|
||||
if err := tag.Save(); err != nil {
|
||||
return fmt.Errorf("failed to save MP3 tags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmbedLyricsOnlyUniversal embeds lyrics to MP3 or FLAC file
|
||||
func EmbedLyricsOnlyUniversal(filepath string, lyrics string) error {
|
||||
if lyrics == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := strings.ToLower(pathfilepath.Ext(filepath))
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return EmbedLyricsOnlyMP3(filepath, lyrics)
|
||||
case ".flac":
|
||||
return EmbedLyricsOnly(filepath, lyrics)
|
||||
default:
|
||||
return fmt.Errorf("unsupported file format for lyrics embedding: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user