ffmpeg.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. package ffmpeg
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "go.uber.org/zap"
  7. "io"
  8. "os"
  9. "os/exec"
  10. "strings"
  11. "unlock-music.dev/cli/algo/common"
  12. "unlock-music.dev/cli/internal/utils"
  13. )
  14. func ExtractAlbumArt(ctx context.Context, rd io.Reader) (*bytes.Buffer, error) {
  15. cmd := exec.CommandContext(ctx, "ffmpeg",
  16. "-i", "pipe:0", // input from stdin
  17. "-an", // disable audio
  18. "-codec:v", "copy", // copy video(image) codec
  19. "-f", "image2", // use image2 muxer
  20. "pipe:1", // output to stdout
  21. )
  22. cmd.Stdin = rd
  23. stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
  24. cmd.Stdout, cmd.Stderr = stdout, stderr
  25. if err := cmd.Run(); err != nil {
  26. return nil, fmt.Errorf("ffmpeg run: %w", err)
  27. }
  28. return stdout, nil
  29. }
  30. type UpdateMetadataParams struct {
  31. Audio string // required
  32. AudioExt string // required
  33. Meta common.AudioMeta // required
  34. AlbumArt []byte // optional
  35. AlbumArtExt string // required if AlbumArt is not nil
  36. }
  37. func UpdateMeta(ctx context.Context, outPath string, params *UpdateMetadataParams, logger *zap.Logger) error {
  38. if params.AudioExt == ".flac" {
  39. return updateMetaFlac(ctx, outPath, params, logger.With(zap.String("module", "updateMetaFlac")))
  40. } else {
  41. return updateMetaFFmpeg(ctx, outPath, params)
  42. }
  43. }
  44. func updateMetaFFmpeg(ctx context.Context, outPath string, params *UpdateMetadataParams) error {
  45. builder := newFFmpegBuilder()
  46. out := newOutputBuilder(outPath) // output to file
  47. builder.SetFlag("y") // overwrite output file
  48. builder.AddOutput(out)
  49. // input audio -> output audio
  50. builder.AddInput(newInputBuilder(params.Audio)) // input 0: audio
  51. out.AddOption("map", "0:a")
  52. out.AddOption("codec:a", "copy")
  53. // input cover -> output cover
  54. if params.AlbumArt != nil &&
  55. params.AudioExt != ".wav" /* wav doesn't support attached image */ {
  56. // write cover to temp file
  57. artPath, err := utils.WriteTempFile(bytes.NewReader(params.AlbumArt), params.AlbumArtExt)
  58. if err != nil {
  59. return fmt.Errorf("updateAudioMeta write temp file: %w", err)
  60. }
  61. defer os.Remove(artPath)
  62. builder.AddInput(newInputBuilder(artPath)) // input 1: cover
  63. out.AddOption("map", "1:v")
  64. switch params.AudioExt {
  65. case ".ogg": // ogg only supports theora codec
  66. out.AddOption("codec:v", "libtheora")
  67. case ".m4a": // .m4a(mp4) requires set codec, disposition, stream metadata
  68. out.AddOption("codec:v", "mjpeg")
  69. out.AddOption("disposition:v", "attached_pic")
  70. out.AddMetadata("s:v", "title", "Album cover")
  71. out.AddMetadata("s:v", "comment", "Cover (front)")
  72. case ".mp3":
  73. out.AddOption("codec:v", "mjpeg")
  74. out.AddMetadata("s:v", "title", "Album cover")
  75. out.AddMetadata("s:v", "comment", "Cover (front)")
  76. default: // other formats use default behavior
  77. }
  78. }
  79. // set file metadata
  80. album := params.Meta.GetAlbum()
  81. if album != "" {
  82. out.AddMetadata("", "album", album)
  83. }
  84. title := params.Meta.GetTitle()
  85. if album != "" {
  86. out.AddMetadata("", "title", title)
  87. }
  88. artists := params.Meta.GetArtists()
  89. if len(artists) != 0 {
  90. // TODO: it seems that ffmpeg doesn't support multiple artists
  91. out.AddMetadata("", "artist", strings.Join(artists, " / "))
  92. }
  93. if params.AudioExt == ".mp3" {
  94. out.AddOption("write_id3v1", "true")
  95. out.AddOption("id3v2_version", "3")
  96. }
  97. // execute ffmpeg
  98. cmd := builder.Command(ctx)
  99. if stdout, err := cmd.CombinedOutput(); err != nil {
  100. return fmt.Errorf("ffmpeg run: %w, %s", err, string(stdout))
  101. }
  102. return nil
  103. }