diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a611185 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Sorrow446/go-mp4tag + +go 1.21.5 \ No newline at end of file diff --git a/mp4tag.go b/mp4tag.go new file mode 100644 index 0000000..95571d9 --- /dev/null +++ b/mp4tag.go @@ -0,0 +1,69 @@ +package mp4tag + +import ( + "bytes" + "fmt" + "io" + "os" +) + +func (mp4 *MP4) Close() error { + return mp4.f.Close() +} + +func (mp4 *MP4) Read() (*MP4Tags, error) { + tags, _, err := mp4.actualRead() + return tags, err +} + +func (mp4 *MP4) Write(tags *MP4Tags, delStrings []string) error { + if tags == nil && len(delStrings) == 0 { + return nil + } + err := mp4.actualWrite(tags, delStrings) + return err +} + +func (mp4 *MP4) checkHeader() error { + buf := make([]byte, 8) + _, err := mp4.f.Seek(4, io.SeekStart) + if err != nil { + return err + } + _, err = io.ReadFull(mp4.f, buf) + if err != nil { + return err + } + + if !bytes.Equal(buf[:4], []byte{0x66, 0x74, 0x79, 0x70}) { + return &ErrInvalidMagic{} + } + for _, ftyp := range ftyps { + if bytes.Equal(buf[4:], ftyp) { + return nil + } + } + return &ErrUnsupportedFtyp{ + Msg: "unsupported ftyp: " + fmt.Sprintf("%x", buf[4:]), + } +} + +func Open(trackPath string) (*MP4, error) { + f, err := os.Open(trackPath) + if err != nil { + return nil, err + } + stat, err := f.Stat() + if err != nil { + f.Close() + return nil, err + } + + mp4 := &MP4{f: f, size : stat.Size(), path: trackPath} + err = mp4.checkHeader() + if err != nil { + f.Close() + return nil, err + } + return mp4, nil +} \ No newline at end of file diff --git a/objects.go b/objects.go new file mode 100644 index 0000000..9152e18 --- /dev/null +++ b/objects.go @@ -0,0 +1,307 @@ +package mp4tag + +import "os" + +type ErrBoxNotPresent struct { + Msg string +} + +type ErrUnsupportedFtyp struct { + Msg string +} + +type ErrInvalidStcoSize struct {} + +type ErrInvalidMagic struct {} + + +func (e *ErrBoxNotPresent) Error() string { + return e.Msg +} + +func (e *ErrUnsupportedFtyp) Error() string { + return e.Msg +} + +func (_ *ErrInvalidStcoSize) Error() string { + return "stco size is invalid" +} + +func (_ *ErrInvalidMagic) Error() string { + return "file header is corrupted or not an mp4 file" +} + +var ftyps = [8][]byte{ + {0x4D, 0x34, 0x41, 0x20}, // M4A + {0x4D, 0x34, 0x42, 0x20}, // M4B + {0x64, 0x61, 0x73, 0x68}, // dash + {0x6D, 0x70, 0x34, 0x31}, // mp41 + {0x6D, 0x70, 0x34, 0x32}, // mp42 + {0x69, 0x73, 0x6F, 0x6D}, // isom + {0x69, 0x73, 0x6F, 0x32}, // iso2 + {0x61, 0x76, 0x63, 0x31}, // avc1 +} + +var containers = []string{ + "moov", "udta", "meta", "ilst", "----", "(c)alb", + "aART", "(c)art", "(c)nam", "(c)cmt", "(c)gen", "gnre", + "(c)wrt", "(c)con", "cprt", "desc", "(c)lyr", "(c)nrt", + "(c)pub", "trkn", "covr", "(c)day", "disk", "(c)too", + "trak", "mdia", "minf", "stbl", "rtng", "plID", + "atID", "tmpo", "sonm", "soal", "soar", "soco", + "soaa", +} + +// 0-9 +var numbers = []rune{ + 0x30, 0x31, 0x32, 0x33, 0x34, + 0x35, 0x36, 0x37, 0x38, 0x39, +} + +type MP4 struct { + f *os.File + path string + size int64 +} + +type MP4Box struct { + StartOffset int64 + EndOffset int64 + BoxSize int64 + Path string +} + +type MP4Boxes struct { + Boxes []*MP4Box +} + +type ImageType int8 +const ( + ImageTypeJPEG ImageType = iota + 13 + ImageTypePNG + ImageTypeAuto +) + +var resolveImageType = map[uint8]ImageType{ + 13: ImageTypeJPEG, + 14: ImageTypePNG, +} + +type ItunesAdvisory int8 +const ( + ItunesAdvisoryNone ItunesAdvisory = iota + ItunesAdvisoryExplicit + ItunesAdvisoryClean +) + +var resolveItunesAdvisory = map[uint8]ItunesAdvisory{ + 1: ItunesAdvisoryExplicit, + 2: ItunesAdvisoryClean, +} + +// GenreNone +type Genre int8 +const ( + GenreNone Genre = iota + GenreBlues + GenreClassicRock + GenreCountry + GenreDance + GenreDisco + GenreFunk + GenreGrunge + GenreHipHop + GenreJazz + GenreMetal + GenreNewAge + GenreOldies + GenreOther + GenrePop + GenreRhythmAndBlues + GenreRap + GenreReggae + GenreRock + GenreTechno + GenreIndustrial + GenreAlternative + GenreSka + GenreDeathMetal + GenrePranks + GenreSoundtrack + GenreEurotechno + GenreAmbient + GenreTripHop + GenreVocal + GenreJassAndFunk + GenreFusion + GenreTrance + GenreClassical + GenreInstrumental + GenreAcid + GenreHouse + GenreGame + GenreSoundClip + GenreGospel + GenreNoise + GenreAlternativeRock + GenreBass + GenreSoul + GenrePunk + GenreSpace + GenreMeditative + GenreInstrumentalPop + GenreInstrumentalRock + GenreEthnic + GenreGothic + GenreDarkwave + GenreTechnoindustrial + GenreElectronic + GenrePopFolk + GenreEurodance + GenreSouthernRock + GenreComedy + GenreCull + GenreGangsta + GenreTop40 + GenreChristianRap + GenrePopSlashFunk + GenreJungleMusic + GenreNativeUS + GenreCabaret + GenreNewWave + GenrePsychedelic + GenreRave + GenreShowtunes + GenreTrailer + GenreLofi + GenreTribal + GenreAcidPunk + GenreAcidJazz + GenrePolka + GenreRetro + GenreMusical + GenreRockNRoll + GenreHardRock +) + +var resolveGenre = map[uint8]Genre{ + 1: GenreBlues, + 2: GenreClassicRock, + 3: GenreCountry, + 4: GenreDance, + 5: GenreDisco, + 6: GenreFunk, + 7: GenreGrunge, + 8: GenreHipHop, + 9: GenreJazz, + 10: GenreMetal, + 11: GenreNewAge, + 12: GenreOldies, + 13: GenreOther, + 14: GenrePop, + 15: GenreRhythmAndBlues, + 16: GenreRap, + 17: GenreReggae, + 18: GenreRock, + 19: GenreTechno, + 20: GenreIndustrial, + 21: GenreAlternative, + 22: GenreSka, + 23: GenreDeathMetal, + 24: GenrePranks, + 25: GenreSoundtrack, + 26: GenreEurotechno, + 27: GenreAmbient, + 28: GenreTripHop, + 29: GenreVocal, + 30: GenreJassAndFunk, + 31: GenreFusion, + 32: GenreTrance, + 33: GenreClassical, + 34: GenreInstrumental, + 35: GenreAcid, + 36: GenreHouse, + 37: GenreGame, + 38: GenreSoundClip, + 39: GenreGospel, + 40: GenreNoise, + 41: GenreAlternativeRock, + 42: GenreBass, + 43: GenreSoul, + 44: GenrePunk, + 45: GenreSpace, + 46: GenreMeditative, + 47: GenreInstrumentalPop, + 48: GenreInstrumentalRock, + 49: GenreEthnic, + 50: GenreGothic, + 51: GenreDarkwave, + 52: GenreTechnoindustrial, + 53: GenreElectronic, + 54: GenrePopFolk, + 55: GenreEurodance, + 56: GenreSouthernRock, + 57: GenreComedy, + 58: GenreCull, + 59: GenreGangsta, + 60: GenreTop40, + 61: GenreChristianRap, + 62: GenrePopSlashFunk, + 63: GenreJungleMusic, + 64: GenreNativeUS, + 65: GenreCabaret, + 66: GenreNewWave, + 67: GenrePsychedelic, + 68: GenreRave, + 69: GenreShowtunes, + 70: GenreTrailer, + 71: GenreLofi, + 72: GenreTribal, + 73: GenreAcidPunk, + 74: GenreAcidJazz, + 75: GenrePolka, + 76: GenreRetro, + 77: GenreMusical, + 78: GenreRockNRoll, + 79: GenreHardRock, +} + +type MP4Picture struct { + Format ImageType + Data []byte +} + +type MP4Tags struct { + Album string + AlbumSort string + AlbumArtist string + AlbumArtistSort string + Artist string + ArtistSort string + BPM int16 + Comment string + Composer string + ComposerSort string + Conductor string + Copyright string + Custom map[string]string + CustomGenre string + Date string + Description string + Director string + DiscNumber int16 + DiscTotal int16 + Genre Genre + ItunesAdvisory ItunesAdvisory + ItunesAlbumID int32 + ItunesArtistID int32 + Lyrics string + Narrator string + Pictures []*MP4Picture + Publisher string + Title string + TitleSort string + TrackNumber int16 + TrackTotal int16 + Year int32 +} \ No newline at end of file diff --git a/read.go b/read.go new file mode 100644 index 0000000..44700a8 --- /dev/null +++ b/read.go @@ -0,0 +1,504 @@ +package mp4tag + +import ( + "encoding/binary" + "fmt" + "io" + "strconv" + "strings" +) + +func (boxes MP4Boxes) getBoxByPath(boxPath string) *MP4Box { + for _, box := range boxes.Boxes { + if box.Path == boxPath { + return box + } + } + return nil +} + +func (boxes MP4Boxes) getBoxesByPath(boxPath string) []*MP4Box { + var outBoxes []*MP4Box + for _, box := range boxes.Boxes { + if box.Path == boxPath { + outBoxes = append(outBoxes, box) + } + } + return outBoxes +} + +func (mp4 MP4) readString(size int64) (string, error) { + buf := make([]byte, size) + _, err := io.ReadFull(mp4.f, buf) + if err != nil { + return "", err + } + return string(buf), nil +} + +func (mp4 MP4) readBoxName() (string, error) { + buf := make([]byte, 4) + _, err := io.ReadFull(mp4.f, buf) + if err != nil { + return "", err + } + boxName := string(buf) + if buf[0] == 0xA9 { + boxName = "(c)" + strings.ToLower(boxName[1:]) + } + return boxName, nil +} + +func (mp4 MP4) readI16BE() (int16, error) { + buf := make([]byte, 2) + _, err := io.ReadFull(mp4.f, buf) + if err != nil { + return -1, err + } + num := binary.BigEndian.Uint16(buf) + return int16(num), nil +} + +func (mp4 MP4) readI32BE() (int32, error) { + buf := make([]byte, 4) + _, err := io.ReadFull(mp4.f, buf) + if err != nil { + return -1, err + } + num := binary.BigEndian.Uint32(buf) + return int32(num), nil +} + +func (mp4 MP4) readBoxes(boxes MP4Boxes, parentEndsAt, level int64, p string) (MP4Boxes, error) { + empty := MP4Boxes{} + pos, err := getPos(mp4.f) + if err != nil { + return empty, err + } + if pos >= parentEndsAt { + return boxes, err + } + boxSizeI32, err := mp4.readI32BE() + if err != nil { + return empty, err + } + boxName, err := mp4.readBoxName() + if err != nil { + return empty, err + } + boxSize := int64(boxSizeI32) + endsAt := pos + boxSize + if boxName == "meta" { + _, err = mp4.f.Seek(4, io.SeekCurrent) + if err != nil { + return empty, err + } + } + p += "." + boxName + box := &MP4Box{ + StartOffset: pos, + EndOffset: endsAt, + BoxSize: boxSize, + Path: p[1:], + } + boxes.Boxes = append(boxes.Boxes, box) + if containsStr(containers, boxName) { + boxes, err = mp4.readBoxes(boxes, endsAt, level+1, p) + if err != nil { + return empty, err + } + } + p = p[:len(p)-len(boxName)-1] + _, err = mp4.f.Seek(pos + boxSize, io.SeekStart) + if err != nil { + return empty, err + } + boxes, err = mp4.readBoxes(boxes, parentEndsAt, level, p) + return boxes, err +} + +func checkBoxes(boxes MP4Boxes) error { + paths := [5]string{ + "moov", "mdat", "moov.udta", "moov.udta.meta", + "moov.trak.mdia.minf.stbl.stco", + } + // "moov.udta.meta.ilst" + for _, path := range paths { + if boxes.getBoxByPath(path) == nil { + return &ErrBoxNotPresent{Msg: path + " box not present"} + } + } + return nil +} + +func (mp4 MP4) readTag(boxes MP4Boxes, boxName string) (string, error) { + path := fmt.Sprintf("moov.udta.meta.ilst.%s.data", boxName) + box := boxes.getBoxByPath(path) + if box == nil { + return "", nil + } + _, err := mp4.f.Seek(box.StartOffset+16, io.SeekStart) + if err != nil { + return "", err + } + tag, err := mp4.readString(box.BoxSize-16) + return tag, err +} + +func (mp4 MP4) readByte() (byte, error) { + buf := make([]byte, 1) + _, err := mp4.f.Read(buf) + if err != nil { + return 0x0, err + } + return buf[0], nil +} + +func (mp4 MP4) readBPM(boxes MP4Boxes) (int16, error) { + box := boxes.getBoxByPath("moov.udta.meta.ilst.tmpo.data") + if box == nil { + return -1, nil + } + _, err := mp4.f.Seek(box.StartOffset+16, io.SeekStart) + if err != nil { + return -1, err + } + bpm, err := mp4.readI16BE() + return bpm, err +} + +func (mp4 MP4) readPics(_boxes MP4Boxes) ([]*MP4Picture, error) { + var outPics []*MP4Picture + boxes := _boxes.getBoxesByPath("moov.udta.meta.ilst.covr.data") + if boxes == nil { + return nil, nil + } + for _, box := range boxes { + var pic MP4Picture + _, err := mp4.f.Seek(box.StartOffset+11, io.SeekStart) + if err != nil { + return nil, err + } + b, err := mp4.readByte() + if err != nil { + return nil, err + } + + imageType, ok := resolveImageType[uint8(b)] + if ok { + if imageType == ImageTypeJPEG { + pic.Format = ImageTypeJPEG + } else { + pic.Format = ImageTypePNG + } + } + _, err = mp4.f.Seek(4, io.SeekCurrent) + if err != nil { + return nil, err + } + buf := make([]byte, box.BoxSize-16) + _, err = io.ReadFull(mp4.f, buf) + if err != nil { + return nil, err + } + pic.Data = buf + outPics = append(outPics, &pic) + } + return outPics, nil +} + +func (mp4 MP4) readTrknDisk(boxes MP4Boxes, boxName string) (int16, int16, error) { + path := fmt.Sprintf("moov.udta.meta.ilst.%s.data", boxName) + box := boxes.getBoxByPath(path) + if box == nil { + return -1, -1, nil + } + _, err := mp4.f.Seek(box.StartOffset+18, io.SeekStart) + if err != nil { + return -1, -1, nil + } + + num, err := mp4.readI16BE() + if err != nil { + return -1, -1, nil + } + total, err := mp4.readI16BE() + if err != nil { + return -1, -1, nil + } + return num, total, nil +} + +func (mp4 MP4) readCustom(boxes MP4Boxes) (map[string]string, error) { + var ( + names []string + values []string + ) + path := "moov.udta.meta.ilst.----" + nameBoxes := boxes.getBoxesByPath(path+".name") + if nameBoxes == nil { + return nil, nil + } + for _, box := range nameBoxes { + _, err := mp4.f.Seek(box.StartOffset+12, io.SeekStart) + if err != nil { + return nil, err + } + name, err := mp4.readString(box.BoxSize-12) + if err != nil { + return nil, err + } + names = append(names, name) + } + + dataBoxes := boxes.getBoxesByPath(path+".data") + for _, box := range dataBoxes { + _, err := mp4.f.Seek(box.StartOffset+16, io.SeekStart) + if err != nil { + return nil, err + } + value, err := mp4.readString(box.BoxSize-16) + if err != nil { + return nil, err + } + values = append(values, value) + } + + custom := map[string]string{} + for idx, name := range names { + custom[name] = values[idx] + } + return custom, nil +} + +func (mp4 MP4) readITAlbumID(boxes MP4Boxes) (int32, error) { + box := boxes.getBoxByPath("moov.udta.meta.ilst.plID.data") + if box == nil { + return -1, nil + } + _, err := mp4.f.Seek(box.StartOffset+20, io.SeekStart) + if err != nil { + return -1, err + } + id, err := mp4.readI32BE() + return id, err +} + +func (mp4 MP4) readITArtistID(boxes MP4Boxes) (int32, error) { + box := boxes.getBoxByPath("moov.udta.meta.ilst.atID.data") + if box == nil { + return -1, nil + } + _, err := mp4.f.Seek(box.StartOffset+16, io.SeekStart) + if err != nil { + return -1, err + } + id, err := mp4.readI32BE() + return id, err +} + +func (mp4 MP4) readAdvisory(boxes MP4Boxes) (ItunesAdvisory, error) { + none := ItunesAdvisoryNone + box := boxes.getBoxByPath("moov.udta.meta.ilst.rtng.data") + if box == nil { + return none, nil + } + _, err := mp4.f.Seek(box.StartOffset+16, io.SeekStart) + if err != nil { + return none, err + } + b, err := mp4.readByte() + if err != nil { + return none, err + } + advisory, ok := resolveItunesAdvisory[uint8(b)] + if !ok { + return none, nil + } + return advisory, nil +} + +func (mp4 MP4) readGenre(boxes MP4Boxes) (Genre, error) { + none := GenreNone + box := boxes.getBoxByPath("moov.udta.meta.ilst.gnre.data") + if box == nil { + return none, nil + } + _, err := mp4.f.Seek(box.StartOffset+17, io.SeekStart) + if err != nil { + return none, err + } + b, err := mp4.readByte() + if err != nil { + return none, err + } + genre, ok := resolveGenre[uint8(b)] + if !ok { + return none, nil + } + return genre, nil +} + +func (mp4 MP4) readTags(boxes MP4Boxes) (*MP4Tags, error) { + album, err := mp4.readTag(boxes, "(c)alb") + if err != nil { + return nil, err + } + albumArtist, err := mp4.readTag(boxes, "aART") + if err != nil { + return nil, err + } + artist, err := mp4.readTag(boxes, "(c)art") + if err != nil { + return nil, err + } + bpm, err := mp4.readBPM(boxes) + if err != nil { + return nil, err + } + + comment, err := mp4.readTag(boxes, "(c)cmt") + if err != nil { + return nil, err + } + composer, err := mp4.readTag(boxes, "(c)wrt") + if err != nil { + return nil, err + } + conductor, err := mp4.readTag(boxes, "(c)con") + if err != nil { + return nil, err + } + copyright, err := mp4.readTag(boxes, "cprt") + if err != nil { + return nil, err + } + custom, err := mp4.readCustom(boxes) + if err != nil { + return nil, err + } + customGenre, err := mp4.readTag(boxes, "(c)gen") + if err != nil { + return nil, err + } + description, err := mp4.readTag(boxes, "desc") + if err != nil { + return nil, err + } + lyrics, err := mp4.readTag(boxes, "(c)lyr") + if err != nil { + return nil, err + } + narrator, err := mp4.readTag(boxes, "(c)nrt") + if err != nil { + return nil, err + } + publisher, err := mp4.readTag(boxes, "(c)pub") + if err != nil { + return nil, err + } + title, err := mp4.readTag(boxes, "(c)nam") + if err != nil { + return nil, err + } + + pics, err := mp4.readPics(boxes) + if err != nil { + return nil, err + } + trackNum, trackTotal, err := mp4.readTrknDisk(boxes, "trkn") + if err != nil { + return nil, err + } + discNum, discTotal, err := mp4.readTrknDisk(boxes, "disk") + if err != nil { + return nil, err + } + + genre, err := mp4.readGenre(boxes) + if err != nil { + return nil, err + } + + advisory, err := mp4.readAdvisory(boxes) + if err != nil { + return nil, err + } + + albumID, err := mp4.readITAlbumID(boxes) + if err != nil { + return nil, err + } + + artistID, err := mp4.readITArtistID(boxes) + if err != nil { + return nil, err + } + + tags := &MP4Tags{ + Album: album, + AlbumArtist: albumArtist, + Artist: artist, + BPM: bpm, + Comment: comment, + Composer: composer, + Conductor: conductor, + Copyright: copyright, + Custom: custom, + CustomGenre: customGenre, + Description: description, + DiscNumber: discNum, + DiscTotal: discTotal, + Genre: genre, + ItunesAdvisory: advisory, + ItunesAlbumID: albumID, + ItunesArtistID: artistID, + Lyrics: lyrics, + Narrator: narrator, + Pictures: pics, + Publisher: publisher, + Title: title, + TrackNumber: trackNum, + TrackTotal: trackTotal, + } + + year, err := mp4.readTag(boxes, "(c)day") + if err != nil { + return nil, err + } + + if year != "" { + if containsOnlyNums(year) { + yearInt, err := strconv.ParseInt(year, 10, 32) + if err != nil { + return nil, err + } + tags.Year = int32(yearInt) + } else { + tags.Date = year + } + } + + return tags, nil +} + +func (mp4 MP4) actualRead() (*MP4Tags, MP4Boxes, error) { + var boxes MP4Boxes + _, err := mp4.f.Seek(0, io.SeekStart) + if err != nil { + return nil, boxes, err + } + boxes, err = mp4.readBoxes(boxes, mp4.size, 0, "") + if err != nil { + return nil, boxes, err + } + err = checkBoxes(boxes) + if err != nil { + return nil, boxes, err + } + if boxes.getBoxByPath("moov.udta.meta.ilst") == nil { + return &MP4Tags{}, boxes, nil + } + tags, err := mp4.readTags(boxes) + return tags, boxes, err +} \ No newline at end of file diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..4342e50 --- /dev/null +++ b/utils.go @@ -0,0 +1,77 @@ +package mp4tag + +import ( + "path/filepath" + "time" + "os" + "io" + "strings" + "fmt" +) + +func containsRune(items []rune, value rune) bool { + for _, item := range items { + if item == value { + return true + } + } + return false +} + +func containsOnlyNums(str string) bool { + for _, r := range str { + if !containsRune(numbers, r) { + return false + } + } + return true +} + +func strArrToLower(arr []string) []string { + var lowerArr []string + for _, str := range arr { + lowerArr = append(lowerArr, strings.ToLower(str)) + } + return lowerArr +} + +func containsStr(arr []string, val string) bool { + for _, str := range arr { + if str == val { + return true + } + } + return false +} + +func getTempPath(path string) string { + fname := filepath.Base(path) + unix := time.Now().UnixMilli() + tempPath := filepath.Join( + os.TempDir(), fmt.Sprintf("%s_tmp_%d", fname, unix)) + return tempPath +} + +func getPos(f *os.File) (int64, error) { + return f.Seek(0, io.SeekCurrent) +} + +func moveMP4(srcPath, destPath string) error { + inFile, err := os.Open(srcPath) + if err != nil { + return err + } + outFile, err := os.Create(destPath) + if err != nil { + inFile.Close() + return err + } + defer outFile.Close() + _, err = io.Copy(outFile, inFile) + if err != nil { + return err + } + inFile.Close() + err = os.Remove(srcPath) + return err +} \ No newline at end of file diff --git a/write.go b/write.go new file mode 100644 index 0000000..2593feb --- /dev/null +++ b/write.go @@ -0,0 +1,1072 @@ +package mp4tag + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +const BufSize = 4096*1024 + +func overwriteTags(mergedTags, tags *MP4Tags, delStrings []string) *MP4Tags{ + if containsStr(delStrings, "alltags") { + mergedPics := mergedTags.Pictures + mergedTags = &MP4Tags{} + mergedTags.Pictures = mergedPics + } else if containsStr(delStrings, "allcustom") { + mergedTags.Custom = map[string]string{} + } + + if containsStr(delStrings, "album") { + mergedTags.Album = "" + } + + if containsStr(delStrings, "albumartist") { + mergedTags.AlbumArtist = "" + } + + if containsStr(delStrings, "albumartistsort") { + mergedTags.AlbumArtistSort = "" + } + + if containsStr(delStrings, "albumsort") { + mergedTags.AlbumSort = "" + } + + if containsStr(delStrings, "artist") { + mergedTags.Artist = "" + } + + if containsStr(delStrings, "artistsort") { + mergedTags.ArtistSort = "" + } + + if containsStr(delStrings, "bpm") { + mergedTags.BPM = 0 + } + + if containsStr(delStrings, "comment") { + mergedTags.Comment = "" + } + + if containsStr(delStrings, "composer") { + mergedTags.Composer = "" + } + + if containsStr(delStrings, "composersort") { + mergedTags.ComposerSort = "" + } + + if containsStr(delStrings, "conductor") { + mergedTags.Conductor = "" + } + + if containsStr(delStrings, "copyright") { + mergedTags.Copyright = "" + } + + if containsStr(delStrings, "customgenre") { + mergedTags.CustomGenre = "" + } + + if containsStr(delStrings, "date") { + mergedTags.Date = "" + } + + if containsStr(delStrings, "description") { + mergedTags.Description = "" + } + + if containsStr(delStrings, "director") { + mergedTags.Director = "" + } + + if containsStr(delStrings, "discnumber") || containsStr(delStrings, "disknumber") { + mergedTags.DiscNumber = 0 + } + + if containsStr(delStrings, "disctotal") || containsStr(delStrings, "disktotal") { + mergedTags.DiscTotal = 0 + } + + if containsStr(delStrings, "genre") { + mergedTags.Genre = GenreNone + } + + if containsStr(delStrings, "itunesadvisory") { + mergedTags.ItunesAdvisory = ItunesAdvisoryNone + } + + if containsStr(delStrings, "itunesalbumid") { + mergedTags.ItunesAlbumID = 0 + } + + if containsStr(delStrings, "itunesartistid") { + mergedTags.ItunesArtistID = 0 + } + + if containsStr(delStrings, "lyrics") { + mergedTags.Lyrics = "" + } + + if containsStr(delStrings, "narrator") { + mergedTags.Narrator = "" + } + + if containsStr(delStrings, "publisher") { + mergedTags.Publisher = "" + } + + if containsStr(delStrings, "title") { + mergedTags.Title = "" + } + + if containsStr(delStrings, "titlesort") { + mergedTags.TitleSort = "" + } + + if containsStr(delStrings, "tracknumber") { + mergedTags.TrackNumber = 0 + } + + if containsStr(delStrings, "tracktotal") { + mergedTags.TrackTotal = 0 + } + + if containsStr(delStrings, "year") { + mergedTags.Year = 0 + } + + if containsStr(delStrings, "allpictures") { + mergedTags.Pictures = []*MP4Picture{} + } + + if tags.Album != "" { + mergedTags.Album = tags.Album + } + + if tags.AlbumSort != "" { + mergedTags.AlbumSort = tags.AlbumSort + } + + if tags.AlbumArtist != "" { + mergedTags.AlbumArtist = tags.AlbumArtist + } + + if tags.AlbumArtistSort != "" { + mergedTags.AlbumArtistSort = tags.AlbumArtistSort + } + + if tags.Artist != "" { + mergedTags.Artist = tags.Artist + } + + if tags.ArtistSort != "" { + mergedTags.ArtistSort = tags.ArtistSort + } + + if tags.BPM > 0 { + mergedTags.BPM = tags.BPM + } + + if tags.Comment != "" { + mergedTags.Comment = tags.Comment + } + + if tags.Composer != "" { + mergedTags.Composer = tags.Composer + } + + if tags.ComposerSort != "" { + mergedTags.ComposerSort = tags.ComposerSort + } + if tags.Conductor != "" { + mergedTags.Conductor = tags.Conductor + } + + if tags.Copyright != "" { + mergedTags.Copyright = tags.Copyright + } + + if tags.CustomGenre != "" { + mergedTags.CustomGenre = tags.CustomGenre + } + + if tags.Date != "" { + mergedTags.Date = tags.Date + } + + if tags.Description != "" { + mergedTags.Description = tags.Description + } + + if tags.Director != "" { + mergedTags.Director = tags.Director + } + + if tags.DiscNumber > 0 { + mergedTags.DiscNumber = tags.DiscNumber + } + + if tags.DiscTotal > 0 { + mergedTags.DiscTotal = tags.DiscTotal + } + + if tags.ItunesAdvisory != ItunesAdvisoryNone { + mergedTags.ItunesAdvisory = tags.ItunesAdvisory + } + + if tags.ItunesAlbumID > 0 { + mergedTags.ItunesAlbumID = tags.ItunesAlbumID + } + + if tags.ItunesArtistID > 0 { + mergedTags.ItunesArtistID = tags.ItunesArtistID + } + + if tags.Lyrics != "" { + mergedTags.Lyrics = tags.Lyrics + } + + if tags.Narrator != "" { + mergedTags.Narrator = tags.Narrator + } + + if tags.Publisher != "" { + mergedTags.Publisher = tags.Publisher + } + + if tags.Title != "" { + mergedTags.Title = tags.Title + } + + if tags.TitleSort != "" { + mergedTags.TitleSort = tags.TitleSort + } + + if tags.TrackNumber > 0 { + mergedTags.TrackNumber = tags.TrackNumber + } + + if tags.TrackTotal > 0 { + mergedTags.TrackTotal = tags.TrackTotal + } + + if tags.Year > 0 { + mergedTags.Year = tags.Year + } + + if tags.Genre != GenreNone { + mergedTags.Genre = tags.Genre + } + + for k, v := range tags.Custom { + if v != "" { + mergedTags.Custom[k] = v + } + } + + var filteredPics []*MP4Picture + + for idx, p := range mergedTags.Pictures { + if !containsStr(delStrings, fmt.Sprintf("picture:%d", idx+1)) { + filteredPics = append(filteredPics, p) + } + } + + for _, p := range tags.Pictures { + filteredPics = append(filteredPics, p) + } + + mergedTags.Pictures = filteredPics + return mergedTags +} + +func putI16BE(n int16) []byte { + buf := make([]byte, 2) + + binary.BigEndian.PutUint16(buf, uint16(n)) + return buf +} + +func putI32BE(n int32) []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, uint32(n)) + return buf +} + +func (mp4 MP4) updateChunkOffsets(outF *os.File, boxes MP4Boxes, oldIlistSize, newIlistSize int64) error { + stco := boxes.getBoxByPath("moov.trak.mdia.minf.stbl.stco") + _, err := mp4.f.Seek(stco.StartOffset+12, io.SeekStart) + if err != nil { + return err + } + _, err = outF.Seek(stco.StartOffset+16, io.SeekStart) + if err != nil { + return err + } + count, err := mp4.readI32BE() + if err != nil { + return err + } + if stco.BoxSize != int64(count) * 4 + 16 { + return &ErrInvalidStcoSize{} + } + + for i := int32(1); i<=count; i++ { + offset, err := mp4.readI32BE() + if err != nil { + return err + } + offsetBytes := putI32BE(offset-int32(oldIlistSize)+int32(newIlistSize)) + _, err = outF.Write(offsetBytes) + if err != nil { + return err + } + } + + return nil +} + +func (mp4 MP4) readToOffset(f *os.File, startOffset int64) error { + _, err := mp4.f.Seek(0, io.SeekStart) + if err != nil { + return err + } + buf := make([]byte, BufSize) + var totalRead int64 + + for { + read, err := mp4.f.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return err + } + readI64 := int64(read) + totalRead += readI64 + if totalRead > startOffset { + // Okay? + _, err = f.Write(buf[:totalRead-readI64+startOffset]) + if err != nil { + return err + } + break + } + _, err = f.Write(buf) + if err != nil { + return err + } + } + return nil +} + + +func writeRegular(f *os.File, boxName, val string, prefix bool) error { + // boxSize := utf8.RuneCountInString(val) + 24 + valBytes := []byte(val) + boxSize := len(valBytes) + 24 + boxSizeI32 := int32(boxSize) + boxSizeBytes := putI32BE(boxSizeI32) + _, err := f.Write(boxSizeBytes) + if err != nil { + return err + } + if prefix { + _, err = f.Write([]byte{0xA9}) + if err != nil { + return err + } + } + _, err = f.WriteString(boxName) + if err != nil { + return err + } + boxSizeBytes = putI32BE(boxSizeI32-8) + _, err = f.Write(boxSizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + + _, err = f.Write( + []byte{0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + + _, err = f.Write(valBytes) + return err +} + +func writeGenre(f *os.File, genre Genre) error { + _, err := f.Write([]byte{0x0, 0x0, 0x0, 0x1A}) + if err != nil { + return err + } + _, err = f.WriteString("gnre") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x12}) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write(bytes.Repeat([]byte{0x0}, 9)) + if err != nil { + return err + } + _, err = f.Write([]byte{byte(genre)}) + return err +} + +func writeTrknDisc(f *os.File, n, total int16, isTrkn bool) error { + var boxSize int32 = 30 + if n < 0 { + n = 0 + } + if total < 0 { + total = 0 + } + if isTrkn { + boxSize += 2 + } + boxSizeBytes := putI32BE(boxSize) + _, err := f.Write(boxSizeBytes) + if err != nil { + return err + } + if isTrkn { + _, err = f.WriteString("trkn") + } else { + _, err = f.WriteString("disk") + } + if err != nil { + return err + } + boxSizeBytes = putI32BE(boxSize-8) + if err != nil { + return err + } + _, err = f.Write(boxSizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write(bytes.Repeat([]byte{0x0}, 10)) + if err != nil { + return err + } + + nBytes := putI16BE(n) + _, err = f.Write(nBytes) + if err != nil { + return err + } + totalBytes := putI16BE(total) + _, err = f.Write(totalBytes) + if err != nil { + return err + } + if isTrkn { + _, err = f.Write([]byte{0x0, 0x0}) + return err + } + return nil +} + +func writeBPM(f *os.File, bpm int16) error { + _, err := f.Write([]byte{0x0, 0x0, 0x0, 0x1A}) + if err != nil { + return err + } + _, err = f.WriteString("tmpo") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x12}) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write( + []byte{0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + bpmBytes := putI16BE(bpm) + _, err = f.Write(bpmBytes) + return err +} + +func writeAdvisory(f *os.File, advisory ItunesAdvisory) error { + _, err := f.Write([]byte{0x0, 0x0, 0x0, 0x19}) + if err != nil { + return err + } + _, err = f.WriteString("rtng") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x11}) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + _, err = f.Write([]byte{byte(advisory)}) + return err +} + +func writeItunesAlbumID(f *os.File, albumID int32) error { + _, err := f.Write([]byte{0x0, 0x0, 0x0, 0x20}) + if err != nil { + return err + } + _, err = f.WriteString("plID") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x18}) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write( + []byte{0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + albumIDBytes := putI32BE(albumID) + _, err = f.Write(albumIDBytes) + return err +} + +func writeItunesArtistID(f *os.File, artistID int32) error { + _, err := f.Write([]byte{0x0, 0x0, 0x0, 0x1C}) + if err != nil { + return err + } + _, err = f.WriteString("atID") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x14}) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write( + []byte{0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + artistIDBytes := putI32BE(artistID) + _, err = f.Write(artistIDBytes) + return err +} + +func writeCustom(f *os.File, name, value string) error { + nameUpperBytes := []byte(strings.ToUpper(name)) + valueBytes := []byte(value) + nameSize := len(nameUpperBytes) + valueSize := len(valueBytes) + + sizeBytes := putI32BE(int32(nameSize+valueSize)+64) + _, err := f.Write(sizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("----") + if err != nil { + return err + } + _, err = f.Write([]byte{0x0, 0x0, 0x0, 0x1C}) + if err != nil { + return err + } + _, err = f.WriteString("mean") + if err != nil { + return err + } + _, err = f.Write(bytes.Repeat([]byte{0x0}, 4)) + if err != nil { + return err + } + _, err = f.WriteString("com.apple.iTunes") + if err != nil { + return err + } + sizeBytes = putI32BE(int32(nameSize)+12) + _, err = f.Write(sizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("name") + if err != nil { + return err + } + _, err = f.Write(bytes.Repeat([]byte{0x0}, 4)) + if err != nil { + return err + } + _, err = f.Write(nameUpperBytes) + if err != nil { + return err + } + sizeBytes = putI32BE(int32(valueSize)+16) + _, err = f.Write(sizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + _, err = f.Write( + []byte{0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + _, err = f.Write(valueBytes) + return err +} + +func getPicFormat(imageType ImageType, magic []byte) uint8 { + if imageType == ImageTypeAuto { + if bytes.Equal(magic, []byte{0x89, 0x50, 0x4E, 0x47}) { + return 0xE + } + } + if imageType == ImageTypePNG { + return 0xE + } + return 0x0D +} + +func writePics(f *os.File, pics []*MP4Picture) error { + var boxSize int32 = 8 + for _, pic := range pics { + dataSize := len(pic.Data) + if dataSize < 1 { + continue + } + boxSize += int32(dataSize + 16) + } + + boxSizeBytes := putI32BE(boxSize) + _, err := f.Write(boxSizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("covr") + if err != nil { + return err + } + + for _, pic := range pics { + dataSize := len(pic.Data) + if dataSize < 1 { + continue + } + boxSizeBytes = putI32BE(int32(dataSize+16)) + _, err = f.Write(boxSizeBytes) + if err != nil { + return err + } + _, err = f.WriteString("data") + if err != nil { + return err + } + + format := getPicFormat(pic.Format, pic.Data[:4]) + _, err = f.Write([]byte{0x0, 0x0, 0x0, format, 0x0, 0x0, 0x0, 0x0}) + if err != nil { + return err + } + _, err = f.Write(pic.Data) + if err != nil { + return err + } + } + + return nil +} + +func resizeBoxes(f *os.File, boxes MP4Boxes, oldIlstSize, newIlistSize int64) error { + moov := boxes.getBoxByPath("moov") + udta := boxes.getBoxByPath("moov.udta") + meta := boxes.getBoxByPath("moov.udta.meta") + + sizeBytes := putI32BE(int32(newIlistSize)) + _, err := f.Write(sizeBytes) + if err != nil { + return err + } + + _, err = f.Seek(moov.StartOffset, io.SeekStart) + if err != nil { + return err + } + newMoovSize := moov.BoxSize - oldIlstSize + newIlistSize + sizeBytes = putI32BE(int32(newMoovSize)) + _, err = f.Write(sizeBytes) + if err != nil { + return err + } + + _, err = f.Seek(udta.StartOffset, io.SeekStart) + if err != nil { + return err + } + newUdtaSize := udta.BoxSize - oldIlstSize + newIlistSize + sizeBytes = putI32BE(int32(newUdtaSize)) + _, err = f.Write(sizeBytes) + if err != nil { + return err + } + + _, err = f.Seek(meta.StartOffset, io.SeekStart) + if err != nil { + return err + } + newMetaSize := meta.BoxSize - oldIlstSize + newIlistSize + sizeBytes = putI32BE(int32(newMetaSize)) + _, err = f.Write(sizeBytes) + return err +} + +func (mp4 MP4) writeRemaining(f *os.File) error { + buf := make([]byte, BufSize) + for { + read, err := mp4.f.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return err + } + if read < BufSize { + _, err := f.Write(buf[:read]) + if err != nil { + return err + } + } else { + _, err = f.Write(buf) + if err != nil { + return err + } + } + } + return nil +} + +func (mp4 MP4) writeTags(boxes MP4Boxes, tags *MP4Tags, tempPath string) error { + ilst := boxes.getBoxByPath("moov.udta.meta.ilst") + oldIlstSize := ilst.BoxSize + f, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + err = mp4.readToOffset(f, ilst.StartOffset) + if err != nil { + return err + } + ilstStartOffset, err := getPos(f) + if err != nil { + return err + } + + _, err = f.Write(bytes.Repeat([]byte{0x0}, 4)) + if err != nil { + return err + } + _, err = f.WriteString("ilst") + if err != nil { + return err + } + if tags.Title != "" { + err = writeRegular(f, "nam", tags.Title, true) + if err != nil { + return err + } + } + if tags.TitleSort != "" { + err = writeRegular(f, "sonm", tags.TitleSort, false) + if err != nil { + return err + } + } + if tags.Album != "" { + err = writeRegular(f, "alb", tags.Album, true) + if err != nil { + return err + } + } + if tags.AlbumSort != "" { + err = writeRegular(f, "soal", tags.AlbumSort, false) + if err != nil { + return err + } + } + + if tags.AlbumArtist != "" { + err = writeRegular(f, "aART", tags.AlbumArtist, false) + if err != nil { + return err + } + } + + if tags.AlbumArtistSort != "" { + err = writeRegular(f, "soaa", tags.AlbumArtistSort, false) + if err != nil { + return err + } + } + + if tags.Artist != "" { + err = writeRegular(f, "ART", tags.Artist, true) + if err != nil { + return err + } + } + + if tags.ArtistSort != "" { + err = writeRegular(f, "soar", tags.ArtistSort, false) + if err != nil { + return err + } + } + + if tags.Comment != "" { + err = writeRegular(f, "cmt", tags.Comment, true) + if err != nil { + return err + } + } + + if tags.Composer != "" { + err = writeRegular(f, "wrt", tags.Composer, true) + if err != nil { + return err + } + } + + if tags.ComposerSort != "" { + err = writeRegular(f, "soco", tags.ComposerSort, false) + if err != nil { + return err + } + } + + if tags.Copyright != "" { + err = writeRegular(f, "cprt", tags.Copyright, false) + if err != nil { + return err + } + } + + if tags.Lyrics != "" { + err = writeRegular(f, "lyr", tags.Lyrics, true) + if err != nil { + return err + } + } + + if tags.CustomGenre != "" { + err = writeRegular(f, "gen", tags.CustomGenre, true) + if err != nil { + return err + } + } + + if tags.Description != "" { + err = writeRegular(f, "desc", tags.Description, false) + if err != nil { + return err + } + } + + if tags.Publisher != "" { + err = writeRegular(f, "pub", tags.Publisher, true) + if err != nil { + return err + } + } + + if tags.Conductor != "" { + err = writeRegular(f, "con", tags.Conductor, true) + if err != nil { + return err + } + } + + if tags.ItunesAdvisory != ItunesAdvisoryNone { + err = writeAdvisory(f, tags.ItunesAdvisory) + if err != nil { + return err + } + } + + if tags.ItunesAlbumID > 0 { + err = writeItunesAlbumID(f, tags.ItunesAlbumID) + if err != nil { + return err + } + } + + if tags.ItunesArtistID > 0 { + err = writeItunesArtistID(f, tags.ItunesArtistID) + if err != nil { + return err + } + } + + if tags.TrackNumber > 0 || tags.TrackTotal > 0 { + err = writeTrknDisc(f, tags.TrackNumber, tags.TrackTotal, true) + if err != nil { + return err + } + } + + if tags.DiscNumber > 0 || tags.DiscTotal > 0 { + err = writeTrknDisc(f, tags.DiscNumber, tags.DiscTotal, false) + if err != nil { + return err + } + } + + if tags.BPM > 0 { + err = writeBPM(f, tags.BPM) + if err != nil { + return err + } + } + + if tags.Year > 0 { + err = writeRegular(f, "day", strconv.Itoa(int(tags.Year)), true) + if err != nil { + return err + } + } else if tags.Date == "" { + err = writeRegular(f, "day", tags.Date, true) + if err != nil { + return err + } + } + + if tags.Genre != GenreNone { + err = writeGenre(f, tags.Genre) + if err != nil { + return err + } + } + + for k, v := range tags.Custom { + err = writeCustom(f, k, v) + if err != nil { + return err + } + } + + err = writePics(f, tags.Pictures) + if err != nil { + return err + } + + newIlstEndOffset, err := getPos(f) + if err != nil { + return err + } + newIlstSize := newIlstEndOffset - ilstStartOffset + _, err = f.Seek(ilstStartOffset, io.SeekStart) + if err != nil { + return err + } + + err = resizeBoxes(f, boxes, oldIlstSize, newIlstSize) + if err != nil { + return err + } + + mdat := boxes.getBoxByPath("mdat") + if mdat.StartOffset > ilstStartOffset && oldIlstSize != newIlstSize { + err = mp4.updateChunkOffsets(f, boxes, oldIlstSize, newIlstSize) + if err != nil { + return err + } + } + + _, err = f.Seek(newIlstEndOffset, io.SeekStart) + if err != nil { + return err + } + _, err = mp4.f.Seek(ilst.EndOffset, io.SeekStart) + if err != nil { + return err + } + err = mp4.writeRemaining(f) + return err +} + +func (mp4 *MP4) actualWrite(tags *MP4Tags, _delStrings []string) error { + delStrings := strArrToLower(_delStrings) + + mergedTags, boxes, err := mp4.actualRead() + if err != nil { + return err + } + if boxes.getBoxByPath("moov.udta.meta.ilst") == nil { + return &ErrBoxNotPresent{Msg: "ilst box not present, implement me"} + } + mergedTags = overwriteTags(mergedTags, tags, delStrings) + tempPath := getTempPath(mp4.path) + err = mp4.writeTags(boxes, mergedTags, tempPath) + if err != nil { + return err + } + mp4.Close() + err = moveMP4(tempPath, mp4.path) + if err != nil { + return err + } + + m, err := Open(mp4.path) + if err != nil { + return err + } + mp4.f = m.f + mp4.size = m.size + return nil +} \ No newline at end of file