mirror of
https://github.com/zhaarey/go-mp4tag.git
synced 2025-10-23 15:11:07 +00:00
Complete rewrite.
Everything is done in the library now, Abema's go-mp4 lib no longer needed.
This commit is contained in:
504
read.go
Normal file
504
read.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user