diff --git a/cli/go.mod b/cli/go.mod deleted file mode 100644 index e3cce99..0000000 --- a/cli/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -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 -) diff --git a/cli/go.sum b/cli/go.sum deleted file mode 100644 index 17d4142..0000000 --- a/cli/go.sum +++ /dev/null @@ -1,39 +0,0 @@ -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= diff --git a/cli/main.go b/cli/main.go deleted file mode 100644 index 825d049..0000000 --- a/cli/main.go +++ /dev/null @@ -1,537 +0,0 @@ -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, - Copyright: args.Copyright, - 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()) - } -} diff --git a/cli/structs.go b/cli/structs.go deleted file mode 100644 index eca8392..0000000 --- a/cli/structs.go +++ /dev/null @@ -1,42 +0,0 @@ -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."` - Copyright string `arg:"--copyright" help:"Write copyright 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, copyright, 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 - Copyright 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 -}