mirror of
https://git.um-react.app/um/cli.git
synced 2025-11-28 03:33:02 +00:00
feat(meta): write album art & metadata into destination file
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"unlock-music.dev/cli/algo/common"
|
||||
"unlock-music.dev/cli/internal/utils"
|
||||
)
|
||||
|
||||
func ExtractAlbumArt(ctx context.Context, rd io.Reader) (*bytes.Buffer, error) {
|
||||
@@ -33,8 +34,8 @@ func ExtractAlbumArt(ctx context.Context, rd io.Reader) (*bytes.Buffer, error) {
|
||||
}
|
||||
|
||||
type UpdateMetadataParams struct {
|
||||
Audio io.Reader // required
|
||||
AudioExt string // required
|
||||
Audio string // required
|
||||
AudioExt string // required
|
||||
|
||||
Meta common.AudioMeta // required
|
||||
|
||||
@@ -42,24 +43,15 @@ type UpdateMetadataParams struct {
|
||||
AlbumArtExt string // required if AlbumArt is not nil
|
||||
}
|
||||
|
||||
func UpdateAudioMetadata(ctx context.Context, params *UpdateMetadataParams) (*bytes.Buffer, error) {
|
||||
func UpdateAudioMetadata(ctx context.Context, outPath string, params *UpdateMetadataParams) error {
|
||||
builder := newFFmpegBuilder()
|
||||
builder.SetFlag("y") // overwrite output file
|
||||
|
||||
out := newOutputBuilder("pipe:1") // use stdout as output
|
||||
out.AddOption("f", encodeFormatFromExt(params.AudioExt)) // use mp3 muxer
|
||||
out := newOutputBuilder(outPath) // output to file
|
||||
builder.SetFlag("y") // overwrite output file
|
||||
builder.AddOutput(out)
|
||||
|
||||
// since ffmpeg doesn't support multiple input streams,
|
||||
// we need to write the audio to a temp file
|
||||
audioPath, err := writeTempFile(params.Audio, params.AudioExt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updateAudioMeta write temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(audioPath)
|
||||
|
||||
// input audio -> output audio
|
||||
builder.AddInput(newInputBuilder(audioPath)) // input 0: audio
|
||||
builder.AddInput(newInputBuilder(params.Audio)) // input 0: audio
|
||||
out.AddOption("map", "0:a")
|
||||
out.AddOption("codec:a", "copy")
|
||||
|
||||
@@ -68,9 +60,9 @@ func UpdateAudioMetadata(ctx context.Context, params *UpdateMetadataParams) (*by
|
||||
params.AudioExt != ".wav" /* wav doesn't support attached image */ {
|
||||
|
||||
// write cover to temp file
|
||||
artPath, err := writeTempFile(params.AlbumArt, params.AlbumArtExt)
|
||||
artPath, err := utils.WriteTempFile(params.AlbumArt, params.AlbumArtExt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updateAudioMeta write temp file: %w", err)
|
||||
return fmt.Errorf("updateAudioMeta write temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(artPath)
|
||||
|
||||
@@ -108,51 +100,10 @@ func UpdateAudioMetadata(ctx context.Context, params *UpdateMetadataParams) (*by
|
||||
|
||||
// execute ffmpeg
|
||||
cmd := builder.Command(ctx)
|
||||
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
cmd.Stdout, cmd.Stderr = stdout, stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("ffmpeg run: %w", err)
|
||||
if stdout, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("ffmpeg run: %w, %s", err, string(stdout))
|
||||
}
|
||||
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
func writeTempFile(rd io.Reader, ext string) (string, error) {
|
||||
audioFile, err := os.CreateTemp("", "*"+ext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ffmpeg create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(audioFile, rd); err != nil {
|
||||
return "", fmt.Errorf("ffmpeg write temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := audioFile.Close(); err != nil {
|
||||
return "", fmt.Errorf("ffmpeg close temp file: %w", err)
|
||||
}
|
||||
|
||||
return audioFile.Name(), nil
|
||||
}
|
||||
|
||||
// encodeFormatFromExt returns the file format name the recognized & supporting encoding by ffmpeg.
|
||||
func encodeFormatFromExt(ext string) string {
|
||||
switch ext {
|
||||
case ".flac":
|
||||
return "flac" // raw FLAC
|
||||
case ".mp3":
|
||||
return "mp3" // MP3 (MPEG audio layer 3)
|
||||
case ".ogg":
|
||||
return "ogg" // Ogg
|
||||
case ".m4a":
|
||||
return "ipod" // iPod H.264 MP4 (MPEG-4 Part 14)
|
||||
case ".wav":
|
||||
return "wav" // WAV / WAVE (Waveform Audio)
|
||||
case ".aac":
|
||||
return "adts" // ADTS AAC (Advanced Audio Coding)
|
||||
case ".wma":
|
||||
return "asf" // ASF (Advanced / Active Streaming Format)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
@@ -119,17 +120,20 @@ func ProbeReader(ctx context.Context, rd io.Reader) (*Result, error) {
|
||||
cmd := exec.CommandContext(ctx, "ffprobe",
|
||||
"-v", "quiet", // disable logging
|
||||
"-print_format", "json", // use json format
|
||||
"-show_format", "-show_streams", // retrieve format and streams
|
||||
"-", // input from stdin
|
||||
"-show_format", "-show_streams", "-show_error", // retrieve format and streams
|
||||
"pipe:0", // input from stdin
|
||||
)
|
||||
|
||||
cmd.Stdin = rd
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
cmd.Stdout, cmd.Stderr = stdout, stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := new(Result)
|
||||
if err := json.Unmarshal(out, ret); err != nil {
|
||||
if err := json.Unmarshal(stdout.Bytes(), ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
24
internal/utils/temp.go
Normal file
24
internal/utils/temp.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func WriteTempFile(rd io.Reader, ext string) (string, error) {
|
||||
audioFile, err := os.CreateTemp("", "*"+ext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ffmpeg create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(audioFile, rd); err != nil {
|
||||
return "", fmt.Errorf("ffmpeg write temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := audioFile.Close(); err != nil {
|
||||
return "", fmt.Errorf("ffmpeg close temp file: %w", err)
|
||||
}
|
||||
|
||||
return audioFile.Name(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user