be3da62488
1) correct a bug with the mdat atom. Sometimes, the metadata can be at the end, after the audio Demo : get any music file and strip the metadata convert it to m4a with ffmpeg analyse it with MusicBrainz Picard et voila, you have the metadata at the end. Example of structure of a m4a created that way; https://www.dropbox.com/s/602yivdstan7506/atom.txt?dl=0 2) add support for the "----" tag See http://atomicparsley.sourceforge.net/mpeg-4files.html It gives access to the tags created by MusicBrainz Picard
316 lines
6.2 KiB
Go
316 lines
6.2 KiB
Go
// Copyright 2015, David Howden
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package tag
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
var atomTypes = map[int]string{
|
|
0: "uint8",
|
|
1: "text",
|
|
13: "jpeg",
|
|
14: "png",
|
|
21: "uint8",
|
|
}
|
|
|
|
var atoms = atomNames(map[string]string{
|
|
"\xa9alb": "album",
|
|
"\xa9art": "artist",
|
|
"\xa9ART": "artist",
|
|
"aART": "album_artist",
|
|
"\xa9day": "year",
|
|
"\xa9nam": "title",
|
|
"\xa9gen": "genre",
|
|
"trkn": "track",
|
|
"\xa9wrt": "composer",
|
|
"\xa9too": "encoder",
|
|
"cprt": "copyright",
|
|
"covr": "picture",
|
|
"\xa9grp": "grouping",
|
|
"keyw": "keyword",
|
|
"\xa9lyr": "lyrics",
|
|
"\xa9cmt": "comment",
|
|
"tmpo": "tempo",
|
|
"cpil": "compilation",
|
|
"disk": "disc",
|
|
})
|
|
|
|
type atomNames map[string]string
|
|
|
|
func (f atomNames) Name(n string) []string {
|
|
res := make([]string, 1)
|
|
for k, v := range f {
|
|
if v == n {
|
|
res = append(res, k)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data.
|
|
type metadataMP4 map[string]interface{}
|
|
|
|
// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning
|
|
// non-nil error if there was a problem.
|
|
func ReadAtoms(r io.ReadSeeker) (Metadata, error) {
|
|
_, err := r.Seek(0, os.SEEK_SET)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m := make(metadataMP4)
|
|
err = m.readAtoms(r)
|
|
return m, err
|
|
}
|
|
|
|
func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
|
|
for {
|
|
var size uint32
|
|
var subname string
|
|
err := binary.Read(r, binary.BigEndian, &size)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
name, err := readString(r, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch name {
|
|
case "meta":
|
|
// next_item_id (int32)
|
|
_, err := readBytes(r, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fallthrough
|
|
case "moov", "udta", "ilst":
|
|
return m.readAtoms(r)
|
|
case "free":
|
|
_, err := r.Seek(int64(size-8), os.SEEK_CUR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
case "mdat": // skip the data, the metadata can be at the end
|
|
_, err := r.Seek(int64(size-8), os.SEEK_CUR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
case "----":
|
|
/* Generic atom.
|
|
Should have 3 sub atoms : mean, name and data.
|
|
We check that mean=="com.apple.iTunes" and we use the subname as
|
|
the name, and move to the data atom if anything goes wrong, we jump
|
|
at the end of the "----" atom. */
|
|
|
|
// let's read the mean atom
|
|
var subsize uint32
|
|
err := binary.Read(r, binary.BigEndian, &subsize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sub, err := readString(r, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if sub != "mean" {
|
|
// Something's wrong. Remove 8 read bytes from the size counter
|
|
// since "----" is not a known atom name, the whole data will
|
|
// be skipped
|
|
size -= 8
|
|
break
|
|
}
|
|
|
|
mean, err := readBytes(r, int(subsize-8))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Remove the size of the mean atom from the size counter
|
|
size -= subsize
|
|
|
|
if string(mean[4:]) != "com.apple.iTunes" {
|
|
// Something's wrong, skip this atom
|
|
break
|
|
}
|
|
|
|
// Let's read the name atom
|
|
err = binary.Read(r, binary.BigEndian, &subsize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sub, err = readString(r, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if sub != "name" {
|
|
// Something's wrong
|
|
size -= 8
|
|
break
|
|
}
|
|
|
|
b, err := readBytes(r, int(subsize-8))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
/* Remove the size of the name atom from the size counter.
|
|
We should now be at the start of the data subatom and size should
|
|
be equal to the size of the data atom and its header */
|
|
size -= subsize
|
|
|
|
subname = string(b[4:])
|
|
}
|
|
|
|
b, err := readBytes(r, int(size-8))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Allow all known atoms and the valid "----" atoms
|
|
_, ok := atoms[name]
|
|
switch {
|
|
case name == "----" && subname == "":
|
|
continue
|
|
case name == "----":
|
|
name = subname
|
|
case !ok:
|
|
continue
|
|
}
|
|
|
|
// 16: name + size + "data" + size (4 bytes each), have already read 8
|
|
b = b[8:]
|
|
class := getInt(b[1:4])
|
|
contentType, ok := atomTypes[class]
|
|
if !ok {
|
|
return fmt.Errorf("invalid content type: %v", class)
|
|
}
|
|
|
|
b = b[8:]
|
|
switch name {
|
|
case "trkn", "disk":
|
|
m[name] = int(b[3])
|
|
m[name+"_count"] = int(b[5])
|
|
default:
|
|
var data interface{}
|
|
// 4: atom version (1 byte) + atom flags (3 bytes)
|
|
// 4: NULL (usually locale indicator)
|
|
switch contentType {
|
|
case "text":
|
|
data = string(b)
|
|
|
|
case "uint8":
|
|
data = getInt(b[:1])
|
|
|
|
case "jpeg", "png":
|
|
data = &Picture{
|
|
Ext: contentType,
|
|
MIMEType: "image/" + contentType,
|
|
Data: b,
|
|
}
|
|
}
|
|
m[name] = data
|
|
}
|
|
}
|
|
}
|
|
|
|
func (metadataMP4) Format() Format { return MP4 }
|
|
func (metadataMP4) FileType() FileType { return AAC }
|
|
|
|
func (m metadataMP4) Raw() map[string]interface{} { return m }
|
|
|
|
func (m metadataMP4) getString(n []string) string {
|
|
for _, k := range n {
|
|
if x, ok := m[k]; ok {
|
|
return x.(string)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m metadataMP4) getInt(n []string) int {
|
|
for _, k := range n {
|
|
if x, ok := m[k]; ok {
|
|
return x.(int)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m metadataMP4) Title() string {
|
|
return m.getString(atoms.Name("title"))
|
|
}
|
|
|
|
func (m metadataMP4) Artist() string {
|
|
return m.getString(atoms.Name("artist"))
|
|
}
|
|
|
|
func (m metadataMP4) Album() string {
|
|
return m.getString(atoms.Name("album"))
|
|
}
|
|
|
|
func (m metadataMP4) AlbumArtist() string {
|
|
return m.getString(atoms.Name("album_artist"))
|
|
}
|
|
|
|
func (m metadataMP4) Composer() string {
|
|
return m.getString(atoms.Name("composer"))
|
|
}
|
|
|
|
func (m metadataMP4) Genre() string {
|
|
return m.getString(atoms.Name("genre"))
|
|
}
|
|
|
|
func (m metadataMP4) Year() int {
|
|
date := m.getString(atoms.Name("year"))
|
|
if len(date) >= 4 {
|
|
year, _ := strconv.Atoi(date[:4])
|
|
return year
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m metadataMP4) Track() (int, int) {
|
|
x := m.getInt([]string{"trkn"})
|
|
if n, ok := m["trkn_count"]; ok {
|
|
return x, n.(int)
|
|
}
|
|
return x, 0
|
|
}
|
|
|
|
func (m metadataMP4) Disc() (int, int) {
|
|
x := m.getInt([]string{"disk"})
|
|
if n, ok := m["disk_count"]; ok {
|
|
return x, n.(int)
|
|
}
|
|
return x, 0
|
|
}
|
|
|
|
func (m metadataMP4) Lyrics() string {
|
|
t, ok := m["\xa9lyr"]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return t.(string)
|
|
}
|
|
|
|
func (m metadataMP4) Picture() *Picture {
|
|
v, ok := m["covr"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return v.(*Picture)
|
|
}
|