mirror of
https://github.com/zhaarey/go-mp4tag.git
synced 2025-10-23 15:11:07 +00:00
477 lines
12 KiB
Go
477 lines
12 KiB
Go
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())
|
|
}
|
|
tempPath = filepath.Join(tempPath, "tmp.m4a")
|
|
defer os.RemoveAll(tempPath)
|
|
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
|
|
}
|