mirror of
https://github.com/zhaarey/go-mp4tag.git
synced 2025-10-23 15:11:07 +00:00
Add files via upload
This commit is contained in:
14
cli/go.mod
Normal file
14
cli/go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module main
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/abema/go-mp4 v0.7.2
|
||||
github.com/alexflint/go-arg v1.4.3
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.1.0 // indirect
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
)
|
||||
39
cli/go.sum
Normal file
39
cli/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg=
|
||||
github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||
github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
|
||||
github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
|
||||
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
|
||||
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
536
cli/main.go
Normal file
536
cli/main.go
Normal file
@@ -0,0 +1,536 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/abema/go-mp4"
|
||||
"github.com/alexflint/go-arg"
|
||||
"github.com/sunfish-shogi/bufseekio"
|
||||
)
|
||||
|
||||
var atomsMap = map[string]mp4.BoxType{
|
||||
"Album": {'\251', 'a', 'l', 'b'},
|
||||
"AlbumArtist": {'a', 'A', 'R', 'T'},
|
||||
"Artist": {'\251', 'A', 'R', 'T'},
|
||||
"Comment": {'\251', 'c', 'm', 't'},
|
||||
"Composer": {'\251', 'w', 'r', 't'},
|
||||
"Copyright": {'c', 'p', 'r', 't'},
|
||||
"Cover": {'c', 'o', 'v', 'r'},
|
||||
"Disk": {'d', 'i', 's', 'k'},
|
||||
"Genre": {'\251', 'g', 'e', 'n'},
|
||||
"Label": {'\251', 'l', 'a', 'b'},
|
||||
"Title": {'\251', 'n', 'a', 'm'},
|
||||
"Track": {'t', 'r', 'k', 'n'},
|
||||
"Year": {'\251', 'd', 'a', 'y'},
|
||||
}
|
||||
|
||||
func parseArgs() *Args {
|
||||
var args Args
|
||||
arg.MustParse(&args)
|
||||
return &args
|
||||
}
|
||||
|
||||
func parseTags(args *Args) (*Tags, error) {
|
||||
_tags := &Tags{
|
||||
Album: args.Album,
|
||||
AlbumArtist: args.AlbumArtist,
|
||||
Artist: args.Artist,
|
||||
Comment: args.Comment,
|
||||
Composer: args.Composer,
|
||||
Custom: args.Custom,
|
||||
Delete: args.Delete,
|
||||
DiskNumber: args.DiskNumber,
|
||||
DiskTotal: args.DiskTotal,
|
||||
Genre: args.Genre,
|
||||
Label: args.Label,
|
||||
Title: args.Title,
|
||||
TrackNumber: args.TrackNumber,
|
||||
TrackTotal: args.TrackTotal,
|
||||
Year: args.Year,
|
||||
}
|
||||
if args.Cover != "" {
|
||||
coverBytes, err := ioutil.ReadFile(args.Cover)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_tags.Cover = coverBytes
|
||||
}
|
||||
return _tags, nil
|
||||
}
|
||||
|
||||
// Icky, but don't think there's any other better way.
|
||||
func tagsEmpty(_tags *Tags) bool {
|
||||
emptyOne := &Tags{DiskNumber: 0, DiskTotal: 0, TrackNumber: 0, TrackTotal: 0}
|
||||
emptyTwo := &Tags{DiskNumber: 0, DiskTotal: 0, TrackNumber: 0, TrackTotal: 0, Custom: map[string]string{}}
|
||||
return reflect.DeepEqual(_tags, emptyOne) || reflect.DeepEqual(_tags, emptyTwo)
|
||||
}
|
||||
|
||||
func copy(w *mp4.Writer, h *mp4.ReadHandle) error {
|
||||
_, err := w.StartBox(&h.BoxInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
box, _, err := h.ReadPayload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mp4.Marshal(w, box, h.BoxInfo.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.Expand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
return err
|
||||
}
|
||||
|
||||
// See which atoms don't already exist and will need creating.
|
||||
func populateAtoms(f *os.File, _tags *Tags) (map[string]bool, error) {
|
||||
ilst, err := mp4.ExtractBox(
|
||||
f, nil, mp4.BoxPath{mp4.BoxTypeMoov(), mp4.BoxTypeUdta(), mp4.BoxTypeMeta(), mp4.BoxTypeIlst()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ilst) == 0 {
|
||||
return nil, errors.New("Ilst atom is missing. Implement me.")
|
||||
}
|
||||
atoms := map[string]bool{}
|
||||
fields := reflect.VisibleFields(reflect.TypeOf(*_tags))
|
||||
for _, field := range fields {
|
||||
fieldName := field.Name
|
||||
if fieldName == "Custom" || fieldName == "TrackTotal" || fieldName == "DiskTotal" {
|
||||
continue
|
||||
}
|
||||
if fieldName == "TrackNumber" {
|
||||
fieldName = "Track"
|
||||
} else if fieldName == "DiskNumber" {
|
||||
fieldName = "Disk"
|
||||
}
|
||||
boxType, ok := atomsMap[fieldName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
boxes, err := mp4.ExtractBox(
|
||||
f, nil, mp4.BoxPath{mp4.BoxTypeMoov(), mp4.BoxTypeUdta(), mp4.BoxTypeMeta(), mp4.BoxTypeIlst(), boxType})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
atoms[fieldName] = len(boxes) == 0
|
||||
}
|
||||
return atoms, nil
|
||||
}
|
||||
|
||||
func marshalData(w *mp4.Writer, ctx mp4.Context, val interface{}) error {
|
||||
_, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var boxData mp4.Data
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
boxData.DataType = mp4.DataTypeStringUTF8
|
||||
boxData.Data = []byte(v)
|
||||
case []byte:
|
||||
boxData.DataType = mp4.DataTypeBinary
|
||||
boxData.Data = v
|
||||
}
|
||||
_, err = mp4.Marshal(w, &boxData, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
return err
|
||||
}
|
||||
|
||||
func writeMeta(w *mp4.Writer, tag mp4.BoxType, ctx mp4.Context, val interface{}) error {
|
||||
_, err := w.StartBox(&mp4.BoxInfo{Type: tag})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = marshalData(w, ctx, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
return err
|
||||
}
|
||||
|
||||
func writeCustomMeta(w *mp4.Writer, ctx mp4.Context, field string, val interface{}) error {
|
||||
_, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte{'\x00', '\x00', '\x00', '\x00'})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, "com.apple.iTunes")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte{'\x00', '\x00', '\x00', '\x00'})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = marshalData(w, ctx, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
return err
|
||||
}
|
||||
|
||||
// func writeCover(h *mp4.ReadHandle, w *mp4.Writer, ctx mp4.Context, coverData []byte) error {
|
||||
// _, err := w.StartBox(&h.BoxInfo)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// box, _, err := h.ReadPayload()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// _, err = mp4.Marshal(w, box, h.BoxInfo.Context)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// err = writeMeta(w, h.BoxInfo.Type, ctx, coverData)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// _, err = w.EndBox()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Make new atoms and write to.
|
||||
func createAndWrite(h *mp4.ReadHandle, w *mp4.Writer, ctx mp4.Context, _tags *Tags, atoms map[string]bool) error {
|
||||
_, err := w.StartBox(&h.BoxInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
box, _, err := h.ReadPayload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mp4.Marshal(w, box, h.BoxInfo.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _tags.Cover != nil {
|
||||
err := writeMeta(w, atomsMap["Cover"], ctx, _tags.Cover)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _tags.TrackNumber > 0 {
|
||||
trkn := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(trkn, uint32(_tags.TrackNumber))
|
||||
if _tags.TrackTotal > 0 {
|
||||
binary.BigEndian.PutUint16(trkn[4:], uint16(_tags.TrackTotal))
|
||||
}
|
||||
err = writeMeta(w, atomsMap["Track"], ctx, trkn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _tags.DiskNumber > 0 {
|
||||
disk := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(disk, uint32(_tags.DiskNumber))
|
||||
if _tags.DiskTotal > 0 {
|
||||
binary.BigEndian.PutUint16(disk[4:], uint16(_tags.DiskTotal))
|
||||
}
|
||||
err = writeMeta(w, atomsMap["Disk"], ctx, disk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for tagName, needCreate := range atoms {
|
||||
if tagName == "Cover" || tagName == "Track" || tagName == "Disk" {
|
||||
continue
|
||||
}
|
||||
val := reflect.ValueOf(*_tags).FieldByName(tagName).String()
|
||||
if !needCreate || val == "" {
|
||||
continue
|
||||
}
|
||||
boxType := atomsMap[tagName]
|
||||
err = writeMeta(w, boxType, ctx, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for field, value := range _tags.Custom {
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
err = writeCustomMeta(w, ctx, field, strings.ToUpper(value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = h.Expand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
return err
|
||||
}
|
||||
|
||||
func writeExisting(h *mp4.ReadHandle, w *mp4.Writer, _tags *Tags, currentKey string, ctx mp4.Context) (bool, error) {
|
||||
if currentKey == "Cover" && _tags.Cover == nil {
|
||||
return true, nil
|
||||
}
|
||||
if currentKey == "Cover" && _tags.Cover != nil {
|
||||
// err := writeCover(h, w, ctx, _tags.Cover)
|
||||
// if err != nil {
|
||||
// return false, nil
|
||||
// }
|
||||
//err := writeMeta(w, atomsMap["Cover"], ctx, _tags.Cover)
|
||||
_, err := w.StartBox(&h.BoxInfo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
box, _, err := h.ReadPayload()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
data := box.(*mp4.Data)
|
||||
data.DataType = mp4.DataTypeBinary
|
||||
data.Data = []byte(_tags.Cover)
|
||||
_, err = mp4.Marshal(w, data, h.BoxInfo.Context)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// err := writeMeta(w, h.BoxInfo.Type, ctx, _tags.Cover)
|
||||
// if err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
} else if currentKey == "Disk" {
|
||||
if _tags.DiskNumber < 1 {
|
||||
return true, nil
|
||||
}
|
||||
// disk := make([]byte, 8)
|
||||
// binary.BigEndian.PutUint32(disk, uint32(_tags.DiskNumber))
|
||||
// if _tags.DiskTotal > 0 {
|
||||
// binary.BigEndian.PutUint16(disk[4:], uint16(_tags.DiskTotal))
|
||||
// }
|
||||
// err := writeMeta(w, h.BoxInfo.Type, ctx, disk)
|
||||
// if err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
} else if currentKey == "Track" {
|
||||
if _tags.TrackNumber < 1 {
|
||||
return true, nil
|
||||
}
|
||||
// trkn := make([]byte, 8)
|
||||
// binary.BigEndian.PutUint32(trkn, uint32(_tags.TrackNumber))
|
||||
// if _tags.TrackTotal > 0 {
|
||||
// binary.BigEndian.PutUint16(trkn[4:], uint16(_tags.TrackTotal))
|
||||
// }
|
||||
// // err := writeMeta(w, h.BoxInfo.Type, ctx, trkn)
|
||||
} else {
|
||||
toWrite := reflect.ValueOf(*_tags).FieldByName(currentKey).String()
|
||||
if toWrite == "" {
|
||||
return true, nil
|
||||
}
|
||||
// Not working here.
|
||||
// err := writeMeta(w, h.BoxInfo.Type, ctx, toWrite)
|
||||
// if err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
_, err := w.StartBox(&h.BoxInfo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
box, _, err := h.ReadPayload()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
data := box.(*mp4.Data)
|
||||
data.DataType = mp4.DataTypeStringUTF8
|
||||
data.Data = []byte(toWrite)
|
||||
_, err = mp4.Marshal(w, data, h.BoxInfo.Context)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, err = w.EndBox()
|
||||
return false, err
|
||||
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func containsAtom(boxType mp4.BoxType, boxes []mp4.BoxType) mp4.BoxType {
|
||||
for _, _boxType := range boxes {
|
||||
if boxType == _boxType {
|
||||
return boxType
|
||||
}
|
||||
}
|
||||
return mp4.BoxType{}
|
||||
}
|
||||
|
||||
func containsTag(delete []string, currentTag string) bool {
|
||||
for _, tag := range delete {
|
||||
if strings.EqualFold(tag, currentTag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getTag(boxType mp4.BoxType) string {
|
||||
for k, v := range atomsMap {
|
||||
if v == boxType {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getAtomsList() []mp4.BoxType {
|
||||
var atomsList []mp4.BoxType
|
||||
for _, atom := range atomsMap {
|
||||
atomsList = append(atomsList, atom)
|
||||
}
|
||||
return atomsList
|
||||
}
|
||||
|
||||
func copyTrack(srcPath, destPath string) error {
|
||||
srcFile, err := os.OpenFile(srcPath, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
outFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
_, err = io.Copy(outFile, srcFile)
|
||||
return err
|
||||
}
|
||||
|
||||
func write(trackPath string, _tags *Tags) error {
|
||||
var currentKey string
|
||||
ctx := mp4.Context{UnderIlstMeta: true}
|
||||
tempPath, err := os.MkdirTemp(os.TempDir(), "mp4tag")
|
||||
if err != nil {
|
||||
return errors.New(
|
||||
"Failed to make temp directory.\n" + err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempPath)
|
||||
tempPath = filepath.Join(tempPath, "tmp.m4a")
|
||||
atomsList := getAtomsList()
|
||||
outFile, err := os.OpenFile(trackPath, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile, err := os.OpenFile(tempPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
r := bufseekio.NewReadSeeker(outFile, 128*1024, 4)
|
||||
atoms, err := populateAtoms(outFile, _tags)
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
tempFile.Close()
|
||||
return err
|
||||
}
|
||||
w := mp4.NewWriter(tempFile)
|
||||
_, err = mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
|
||||
switch h.BoxInfo.Type {
|
||||
case mp4.BoxTypeMoov(), mp4.BoxTypeUdta(), mp4.BoxTypeMeta():
|
||||
err := copy(w, h)
|
||||
return nil, err
|
||||
case mp4.BoxTypeIlst():
|
||||
err := createAndWrite(h, w, ctx, _tags, atoms)
|
||||
return nil, err
|
||||
case containsAtom(h.BoxInfo.Type, atomsList):
|
||||
if h.BoxInfo.Type == atomsMap["Cover"] && _tags.Cover != nil {
|
||||
return nil, nil
|
||||
}
|
||||
currentKey = getTag(h.BoxInfo.Type)
|
||||
if containsTag(_tags.Delete, currentKey) {
|
||||
return nil, nil
|
||||
}
|
||||
err = copy(w, h)
|
||||
return nil, err
|
||||
case mp4.BoxTypeData():
|
||||
if currentKey == "" {
|
||||
return nil, w.CopyBox(r, &h.BoxInfo)
|
||||
}
|
||||
needCreate := atoms[currentKey]
|
||||
if !needCreate {
|
||||
valEmpty, err := writeExisting(h, w, _tags, currentKey, ctx)
|
||||
currentKey = ""
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if valEmpty {
|
||||
return nil, w.CopyBox(r, &h.BoxInfo)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, w.CopyBox(r, &h.BoxInfo)
|
||||
}
|
||||
})
|
||||
outFile.Close()
|
||||
tempFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyTrack(tempPath, trackPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
args := parseArgs()
|
||||
_tags, err := parseTags(args)
|
||||
if err != nil {
|
||||
panic("Failed to parse tags.\n" + err.Error())
|
||||
}
|
||||
if tagsEmpty(_tags) {
|
||||
fmt.Println("Nothing to write. Exiting...")
|
||||
os.Exit(0)
|
||||
}
|
||||
err = write(args.FilePath, _tags)
|
||||
if err != nil {
|
||||
panic("Failed to write tags.\n" + err.Error())
|
||||
}
|
||||
}
|
||||
40
cli/structs.go
Normal file
40
cli/structs.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
type Args struct {
|
||||
Album string `arg:"--album" help:"Write album tag."`
|
||||
AlbumArtist string `arg:"--albumArtist" help:"Write album artist tag."`
|
||||
Artist string `arg:"--artist" help:"Write artist tag."`
|
||||
Comment string `arg:"--comment" help:"Write comment tag."`
|
||||
Composer string `arg:"--composer" help:"Write composer tag."`
|
||||
Cover string `arg:"--cover" help:"Path of cover to write. JPEG is recommended."`
|
||||
Custom map[string]string `arg:"--custom" help:"Write custom tags. Multiple tags with the same field name can be written.\n\t\t\t Example: \"--custom MYCUSTOMFIELD1=value1 MYCUSTOMFIELD2=value2\""`
|
||||
Delete []string `arg:"-d, --delete" help:"Tags to delete.\n\t\t\t Options: album, albumartist, artist, comment, composer, cover, disk, genre, label, title, track, year.\n\t\t\t Example: \"-d album albumartist\""`
|
||||
DiskNumber int `arg:"--diskNumber" help:"Write disk number tag."`
|
||||
DiskTotal int `arg:"--diskTotal" help:"Write disk total tag. Can't be written without disk number tag."`
|
||||
FilePath string `arg:"positional, required" help:"Path of file to write to."`
|
||||
Genre string `arg:"--genre" help:"Write genre tag."`
|
||||
Label string `arg:"--label" help:"Write label tag."`
|
||||
Title string `arg:"--title" help:"Write title tag."`
|
||||
TrackNumber int `arg:"--trackNumber" help:"Write track number tag."`
|
||||
TrackTotal int `arg:"--trackTotal" help:"Write track total tag. Can't be written without track number tag."`
|
||||
Year string `arg:"--year" help:"Write year tag."`
|
||||
}
|
||||
|
||||
type Tags struct {
|
||||
Album string
|
||||
AlbumArtist string
|
||||
Artist string
|
||||
Comment string
|
||||
Composer string
|
||||
Cover []byte
|
||||
Custom map[string]string
|
||||
Delete []string
|
||||
DiskNumber int
|
||||
DiskTotal int
|
||||
Genre string
|
||||
Label string
|
||||
Title string
|
||||
TrackNumber int
|
||||
TrackTotal int
|
||||
Year string
|
||||
}
|
||||
Reference in New Issue
Block a user