tag/mp4.go

316 lines
6.2 KiB
Go
Raw Normal View History

2015-03-19 13:21:53 +01:00
// 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"
2015-04-14 16:06:32 +02:00
"os"
2015-03-19 13:21:53 +01:00
"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{}
2015-04-14 16:06:32 +02:00
// 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
}
2015-03-19 13:21:53 +01:00
m := make(metadataMP4)
2015-04-14 16:06:32 +02:00
err = m.readAtoms(r)
2015-03-19 13:21:53 +01:00
return m, err
}
2015-04-14 16:08:53 +02:00
func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
2015-03-19 13:21:53 +01:00
for {
var size uint32
var subname string
2015-03-19 13:21:53 +01:00
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":
2015-04-14 16:08:53 +02:00
_, err := r.Seek(int64(size-8), os.SEEK_CUR)
if err != nil {
return err
}
2015-03-19 13:21:53 +01:00
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:])
2015-03-19 13:21:53 +01:00
}
b, err := readBytes(r, int(size-8))
if err != nil {
return err
}
// Allow all known atoms and the valid "----" atoms
2015-03-19 13:21:53 +01:00
_, ok := atoms[name]
switch {
case name == "----" && subname == "":
continue
case name == "----":
name = subname
case !ok:
2015-03-19 13:21:53 +01:00
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 }
2015-03-19 13:21:53 +01:00
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)
}
2015-03-19 13:21:53 +01:00
func (m metadataMP4) Picture() *Picture {
v, ok := m["covr"]
if !ok {
return nil
}
return v.(*Picture)
}