127 lines
3.4 KiB
Go
127 lines
3.4 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"go.uber.org/zap"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"unlock-music.dev/cli/algo/common"
|
|
"unlock-music.dev/cli/internal/utils"
|
|
)
|
|
|
|
func ExtractAlbumArt(ctx context.Context, rd io.Reader) (*bytes.Buffer, error) {
|
|
cmd := exec.CommandContext(ctx, "ffmpeg",
|
|
"-i", "pipe:0", // input from stdin
|
|
"-an", // disable audio
|
|
"-codec:v", "copy", // copy video(image) codec
|
|
"-f", "image2", // use image2 muxer
|
|
"pipe:1", // output to stdout
|
|
)
|
|
|
|
cmd.Stdin = rd
|
|
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)
|
|
}
|
|
|
|
return stdout, nil
|
|
}
|
|
|
|
type UpdateMetadataParams struct {
|
|
Audio string // required
|
|
AudioExt string // required
|
|
|
|
Meta common.AudioMeta // required
|
|
|
|
AlbumArt []byte // optional
|
|
AlbumArtExt string // required if AlbumArt is not nil
|
|
}
|
|
|
|
func UpdateMeta(ctx context.Context, outPath string, params *UpdateMetadataParams, logger *zap.Logger) error {
|
|
if params.AudioExt == ".flac" {
|
|
return updateMetaFlac(ctx, outPath, params, logger.With(zap.String("module", "updateMetaFlac")))
|
|
} else {
|
|
return updateMetaFFmpeg(ctx, outPath, params)
|
|
}
|
|
}
|
|
func updateMetaFFmpeg(ctx context.Context, outPath string, params *UpdateMetadataParams) error {
|
|
builder := newFFmpegBuilder()
|
|
|
|
out := newOutputBuilder(outPath) // output to file
|
|
builder.SetFlag("y") // overwrite output file
|
|
builder.AddOutput(out)
|
|
|
|
// input audio -> output audio
|
|
builder.AddInput(newInputBuilder(params.Audio)) // input 0: audio
|
|
out.AddOption("map", "0:a")
|
|
out.AddOption("codec:a", "copy")
|
|
|
|
// input cover -> output cover
|
|
if params.AlbumArt != nil &&
|
|
params.AudioExt != ".wav" /* wav doesn't support attached image */ {
|
|
|
|
// write cover to temp file
|
|
artPath, err := utils.WriteTempFile(bytes.NewReader(params.AlbumArt), params.AlbumArtExt)
|
|
if err != nil {
|
|
return fmt.Errorf("updateAudioMeta write temp file: %w", err)
|
|
}
|
|
defer os.Remove(artPath)
|
|
|
|
builder.AddInput(newInputBuilder(artPath)) // input 1: cover
|
|
out.AddOption("map", "1:v")
|
|
|
|
switch params.AudioExt {
|
|
case ".ogg": // ogg only supports theora codec
|
|
out.AddOption("codec:v", "libtheora")
|
|
case ".m4a": // .m4a(mp4) requires set codec, disposition, stream metadata
|
|
out.AddOption("codec:v", "mjpeg")
|
|
out.AddOption("disposition:v", "attached_pic")
|
|
out.AddMetadata("s:v", "title", "Album cover")
|
|
out.AddMetadata("s:v", "comment", "Cover (front)")
|
|
case ".mp3":
|
|
out.AddOption("codec:v", "mjpeg")
|
|
out.AddMetadata("s:v", "title", "Album cover")
|
|
out.AddMetadata("s:v", "comment", "Cover (front)")
|
|
default: // other formats use default behavior
|
|
}
|
|
}
|
|
|
|
// set file metadata
|
|
album := params.Meta.GetAlbum()
|
|
if album != "" {
|
|
out.AddMetadata("", "album", album)
|
|
}
|
|
|
|
title := params.Meta.GetTitle()
|
|
if album != "" {
|
|
out.AddMetadata("", "title", title)
|
|
}
|
|
|
|
artists := params.Meta.GetArtists()
|
|
if len(artists) != 0 {
|
|
// TODO: it seems that ffmpeg doesn't support multiple artists
|
|
out.AddMetadata("", "artist", strings.Join(artists, " / "))
|
|
}
|
|
|
|
if params.AudioExt == ".mp3" {
|
|
out.AddOption("write_id3v1", "true")
|
|
out.AddOption("id3v2_version", "3")
|
|
}
|
|
|
|
// execute ffmpeg
|
|
cmd := builder.Command(ctx)
|
|
|
|
if stdout, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("ffmpeg run: %w, %s", err, string(stdout))
|
|
}
|
|
|
|
return nil
|
|
}
|