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:
69
mp4tag.go
Normal file
69
mp4tag.go
Normal file
@@ -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
|
||||
}
|
||||
307
objects.go
Normal file
307
objects.go
Normal file
@@ -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
|
||||
}
|
||||
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
|
||||
}
|
||||
77
utils.go
Normal file
77
utils.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user