diff --git a/mp4tag.go b/mp4tag.go deleted file mode 100644 index 1f94158..0000000 --- a/mp4tag.go +++ /dev/null @@ -1,476 +0,0 @@ -package mp4tag - -import ( - "encoding/binary" - "errors" - "io" - "os" - "path/filepath" - "reflect" - "strings" - - "github.com/abema/go-mp4" - "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 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(), "go-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 -}