Files
go-mp4tag/read.go
Sorrow446 0d364af5e4 Complete rewrite.
Everything is done in the library now, Abema's go-mp4 lib no longer needed.
2023-12-16 22:11:29 +00:00

504 lines
11 KiB
Go

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
}