tag/mp4.go

380 lines
7.9 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 (
"bytes"
2015-03-19 13:21:53 +01:00
"encoding/binary"
"errors"
2015-03-19 13:21:53 +01:00
"fmt"
"io"
"strconv"
"strings"
2015-03-19 13:21:53 +01:00
)
var atomTypes = map[int]string{
0: "implicit", // automatic based on atom name
2015-03-19 13:21:53 +01:00
1: "text",
13: "jpeg",
14: "png",
21: "uint8",
}
// NB: atoms does not include "----", this is handled separately
2015-03-19 13:21:53 +01:00
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",
})
2024-01-22 22:42:04 +01:00
var means = map[string]bool{
"com.apple.iTunes": true,
"com.mixedinkey.mixedinkey": true,
"com.serato.dj": true,
}
// Detect PNG image if "implicit" class is used
var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
2015-03-19 13:21:53 +01:00
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 struct {
fileType FileType
data map[string]interface{}
}
2015-03-19 13:21:53 +01:00
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) {
m := metadataMP4{
data: make(map[string]interface{}),
fileType: UnknownFileType,
}
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 {
name, size, err := readAtomHeader(r)
2015-03-19 13:21:53 +01:00
if err != nil {
if err == io.EOF {
return nil
}
return err
}
switch name {
case "meta":
// next_item_id (int32)
_, err := readBytes(r, 4)
if err != nil {
return err
}
fallthrough
2015-03-19 13:21:53 +01:00
case "moov", "udta", "ilst":
return m.readAtoms(r)
}
_, ok := atoms[name]
var data []string
if name == "----" {
name, data, err = readCustomAtom(r, size)
2015-04-14 16:08:53 +02:00
if err != nil {
return err
}
if name != "----" {
ok = true
size = 0 // already read data
}
}
if !ok {
_, err := r.Seek(int64(size-8), io.SeekCurrent)
if err != nil {
return err
}
continue
2015-03-19 13:21:53 +01:00
}
err = m.readAtomData(r, name, size-8, data)
2015-03-19 13:21:53 +01:00
if err != nil {
return err
}
}
}
2015-03-19 13:21:53 +01:00
func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error {
var b []byte
var err error
var contentType string
if len(processedData) > 0 {
b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields
contentType = "text"
} else {
// read the data
b, err = readBytes(r, uint(size))
if err != nil {
return err
}
if len(b) < 8 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 8, len(b))
}
// "data" + size (4 bytes each)
b = b[8:]
2020-11-19 20:28:13 +01:00
if len(b) < 4 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 4, len(b))
}
class := getInt(b[1:4])
var ok bool
contentType, ok = atomTypes[class]
if !ok {
return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b)
}
// 4: atom version (1 byte) + atom flags (3 bytes)
// 4: NULL (usually locale indicator)
if len(b) < 8 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for atom version and flags, got %d", 8, len(b))
}
b = b[8:]
2018-02-17 03:07:03 +01:00
}
if name == "trkn" || name == "disk" {
2018-02-17 03:07:03 +01:00
if len(b) < 6 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for track and disk numbers, got %d", 6, len(b))
}
m.data[name] = int(b[3])
m.data[name+"_count"] = int(b[5])
return nil
}
if contentType == "implicit" {
if name == "covr" {
if bytes.HasPrefix(b, pngHeader) {
contentType = "png"
}
// TODO(dhowden): Detect JPEG formats too (harder).
}
}
var data interface{}
switch contentType {
case "implicit":
if _, ok := atoms[name]; ok {
return fmt.Errorf("unhandled implicit content type for required atom: %q", name)
}
return nil
case "text":
data = string(b)
case "uint8":
2018-02-17 03:07:03 +01:00
if len(b) < 1 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b))
}
data = getInt(b[:1])
case "jpeg", "png":
data = &Picture{
Ext: contentType,
MIMEType: "image/" + contentType,
Data: b,
2015-03-19 13:21:53 +01:00
}
}
m.data[name] = data
2015-03-19 13:21:53 +01:00
return nil
}
func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
err = binary.Read(r, binary.BigEndian, &size)
if err != nil {
return
}
name, err = readString(r, 4)
return
}
// Generic atom.
// Should have 3 sub atoms : mean, name and data.
2024-01-22 22:42:04 +01:00
// We check that mean is "com.apple.iTunes" or others and we use the subname as
// the name, and move to the data atom.
// Data atom could have multiple data values, each with a header.
// If anything goes wrong, we jump at the end of the "----" atom.
func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ error) {
subNames := make(map[string]string)
for size > 8 {
subName, subSize, err := readAtomHeader(r)
if err != nil {
return "", nil, err
2015-03-19 13:21:53 +01:00
}
// Remove the size of the atom from the size counter
if size >= subSize {
size -= subSize
} else {
return "", nil, errors.New("--- invalid size")
}
b, err := readBytes(r, uint(subSize-8))
if err != nil {
return "", nil, err
}
if len(b) < 4 {
return "", nil, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b))
}
switch subName {
case "mean", "name":
subNames[subName] = string(b[4:])
case "data":
data = append(data, string(b[4:]))
2015-03-19 13:21:53 +01:00
}
}
// there should remain only the header size
if size != 8 {
err := errors.New("---- atom out of bounds")
return "", nil, err
}
2024-01-22 22:42:04 +01:00
if !means[subNames["mean"]] || subNames["name"] == "" || len(data) == 0 {
return "----", nil, nil
}
2024-01-22 22:42:04 +01:00
return subNames["name"], data, nil
2015-03-19 13:21:53 +01:00
}
func (metadataMP4) Format() Format { return MP4 }
func (m metadataMP4) FileType() FileType { return m.fileType }
2015-03-19 13:21:53 +01:00
func (m metadataMP4) Raw() map[string]interface{} { return m.data }
2015-03-19 13:21:53 +01:00
func (m metadataMP4) getString(n []string) string {
for _, k := range n {
if x, ok := m.data[k]; ok {
2015-03-19 13:21:53 +01:00
return x.(string)
}
}
return ""
}
func (m metadataMP4) getInt(n []string) int {
for _, k := range n {
if x, ok := m.data[k]; ok {
2015-03-19 13:21:53 +01:00
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.data["trkn_count"]; ok {
2015-03-19 13:21:53 +01:00
return x, n.(int)
}
return x, 0
}
func (m metadataMP4) Disc() (int, int) {
x := m.getInt([]string{"disk"})
if n, ok := m.data["disk_count"]; ok {
2015-03-19 13:21:53 +01:00
return x, n.(int)
}
return x, 0
}
func (m metadataMP4) Lyrics() string {
t, ok := m.data["\xa9lyr"]
if !ok {
return ""
}
return t.(string)
}
2018-11-04 21:56:00 +01:00
func (m metadataMP4) Comment() string {
t, ok := m.data["\xa9cmt"]
if !ok {
return ""
}
return t.(string)
}
2015-03-19 13:21:53 +01:00
func (m metadataMP4) Picture() *Picture {
v, ok := m.data["covr"]
2015-03-19 13:21:53 +01:00
if !ok {
return nil
}
p, _ := v.(*Picture)
return p
2015-03-19 13:21:53 +01:00
}