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 }