tag/mp4.go
Aleksey cfaa6a7d55
Apply suggestions from code review
Co-authored-by: David Howden <dhowden@gmail.com>
2021-09-19 16:23:20 +03:00

823 lines
22 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 (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
var atomTypes = map[int]string{
0: "implicit", // automatic based on atom name
1: "text",
13: "jpeg",
14: "png",
21: "uint8",
}
// NB: atoms does not include "----", this is handled separately
var atoms = atomNames(map[string]string{
"\xa9alb": "album",
"\xa9art": "artist",
"\xa9ART": "artist",
"aART": "album_artist",
"\xa9day": "year",
"\xa9nam": "title",
"\xa9gen": "genre",
"gnre": "genre ID3v1 ID",
"geID": "genre ID",
"trkn": "track",
"\xa9wrt": "composer",
"\xa9too": "encoder",
"cprt": "copyright",
"covr": "picture",
"\xa9grp": "grouping",
"keyw": "keyword",
"\xa9lyr": "lyrics",
"\xa9cmt": "comment",
"tmpo": "tempo",
"cpil": "compilation",
"disk": "disc",
})
// Source: https://exiftool.org/TagNames/QuickTime.html
var genreIDValues = map[int]string{
2: "Blues",
3: "Comedy",
4: "Children's Music",
5: "Classical",
6: "Country",
7: "Electronic",
8: "Holiday",
9: "Classical|Opera",
10: "Singer/Songwriter",
11: "Jazz",
12: "Latino",
13: "New Age",
14: "Pop",
15: "R&B/Soul",
16: "Soundtrack",
17: "Dance",
18: "Hip-Hop/Rap",
19: "World",
20: "Alternative",
21: "Rock",
22: "Christian & Gospel",
23: "Vocal",
24: "Reggae",
25: "Easy Listening",
27: "J-Pop",
28: "Enka",
29: "Anime",
30: "Kayokyoku",
50: "Fitness & Workout",
51: "Pop|K-Pop",
52: "Karaoke",
53: "Instrumental",
1001: "Alternative|College Rock",
1002: "Alternative|Goth Rock",
1003: "Alternative|Grunge",
1004: "Alternative|Indie Rock",
1005: "Alternative|New Wave",
1006: "Alternative|Punk",
1007: "Blues|Chicago Blues",
1009: "Blues|Classic Blues",
1010: "Blues|Contemporary Blues",
1011: "Blues|Country Blues",
1012: "Blues|Delta Blues",
1013: "Blues|Electric Blues",
1014: "Children's Music|Lullabies",
1015: "Children's Music|Sing-Along",
1016: "Children's Music|Stories",
1017: "Classical|Avant-Garde",
1018: "Classical|Baroque Era",
1019: "Classical|Chamber Music",
1020: "Classical|Chant",
1021: "Classical|Choral",
1022: "Classical|Classical Crossover",
1023: "Classical|Early Music",
1024: "Classical|Impressionist",
1025: "Classical|Medieval Era",
1026: "Classical|Minimalism",
1027: "Classical|Modern Era",
1028: "Classical|Opera",
1029: "Classical|Orchestral",
1030: "Classical|Renaissance",
1031: "Classical|Romantic Era",
1032: "Classical|Wedding Music",
1033: "Country|Alternative Country",
1034: "Country|Americana",
1035: "Country|Bluegrass",
1036: "Country|Contemporary Bluegrass",
1037: "Country|Contemporary Country",
1038: "Country|Country Gospel",
1039: "Country|Honky Tonk",
1040: "Country|Outlaw Country",
1041: "Country|Traditional Bluegrass",
1042: "Country|Traditional Country",
1043: "Country|Urban Cowboy",
1044: "Dance|Breakbeat",
1045: "Dance|Exercise",
1046: "Dance|Garage",
1047: "Dance|Hardcore",
1048: "Dance|House",
1049: "Dance|Jungle/Drum'n'bass",
1050: "Dance|Techno",
1051: "Dance|Trance",
1052: "Jazz|Big Band",
1053: "Jazz|Bop",
1054: "Easy Listening|Lounge",
1055: "Easy Listening|Swing",
1056: "Electronic|Ambient",
1057: "Electronic|Downtempo",
1058: "Electronic|Electronica",
1060: "Electronic|IDM/Experimental",
1061: "Electronic|Industrial",
1062: "Singer/Songwriter|Alternative Folk",
1063: "Singer/Songwriter|Contemporary Folk",
1064: "Singer/Songwriter|Contemporary Singer/Songwriter",
1065: "Singer/Songwriter|Folk-Rock",
1066: "Singer/Songwriter|New Acoustic",
1067: "Singer/Songwriter|Traditional Folk",
1068: "Hip-Hop/Rap|Alternative Rap",
1069: "Hip-Hop/Rap|Dirty South",
1070: "Hip-Hop/Rap|East Coast Rap",
1071: "Hip-Hop/Rap|Gangsta Rap",
1072: "Hip-Hop/Rap|Hardcore Rap",
1073: "Hip-Hop/Rap|Hip-Hop",
1074: "Hip-Hop/Rap|Latin Rap",
1075: "Hip-Hop/Rap|Old School Rap",
1076: "Hip-Hop/Rap|Rap",
1077: "Hip-Hop/Rap|Underground Rap",
1078: "Hip-Hop/Rap|West Coast Rap",
1079: "Holiday|Chanukah",
1080: "Holiday|Christmas",
1081: "Holiday|Christmas: Children's",
1082: "Holiday|Christmas: Classic",
1083: "Holiday|Christmas: Classical",
1084: "Holiday|Christmas: Jazz",
1085: "Holiday|Christmas: Modern",
1086: "Holiday|Christmas: Pop",
1087: "Holiday|Christmas: R&B",
1088: "Holiday|Christmas: Religious",
1089: "Holiday|Christmas: Rock",
1090: "Holiday|Easter",
1091: "Holiday|Halloween",
1092: "Holiday|Holiday: Other",
1093: "Holiday|Thanksgiving",
1094: "Christian & Gospel|CCM",
1095: "Christian & Gospel|Christian Metal",
1096: "Christian & Gospel|Christian Pop",
1097: "Christian & Gospel|Christian Rap",
1098: "Christian & Gospel|Christian Rock",
1099: "Christian & Gospel|Classic Christian",
1100: "Christian & Gospel|Contemporary Gospel",
1101: "Christian & Gospel|Gospel",
1103: "Christian & Gospel|Praise & Worship",
1104: "Christian & Gospel|Southern Gospel",
1105: "Christian & Gospel|Traditional Gospel",
1106: "Jazz|Avant-Garde Jazz",
1107: "Jazz|Contemporary Jazz",
1108: "Jazz|Crossover Jazz",
1109: "Jazz|Dixieland",
1110: "Jazz|Fusion",
1111: "Jazz|Latin Jazz",
1112: "Jazz|Mainstream Jazz",
1113: "Jazz|Ragtime",
1114: "Jazz|Smooth Jazz",
1115: "Latino|Latin Jazz",
1116: "Latino|Contemporary Latin",
1117: "Latino|Pop Latino",
1118: "Latino|Raices",
1119: "Latino|Urbano latino",
1120: "Latino|Baladas y Boleros",
1121: "Latino|Rock y Alternativo",
1122: "Brazilian",
1123: "Latino|Musica Mexicana",
1124: "Latino|Musica tropical",
1125: "New Age|Environmental",
1126: "New Age|Healing",
1127: "New Age|Meditation",
1128: "New Age|Nature",
1129: "New Age|Relaxation",
1130: "New Age|Travel",
1131: "Pop|Adult Contemporary",
1132: "Pop|Britpop",
1133: "Pop|Pop/Rock",
1134: "Pop|Soft Rock",
1135: "Pop|Teen Pop",
1136: "R&B/Soul|Contemporary R&B",
1137: "R&B/Soul|Disco",
1138: "R&B/Soul|Doo Wop",
1139: "R&B/Soul|Funk",
1140: "R&B/Soul|Motown",
1141: "R&B/Soul|Neo-Soul",
1142: "R&B/Soul|Quiet Storm",
1143: "R&B/Soul|Soul",
1144: "Rock|Adult Alternative",
1145: "Rock|American Trad Rock",
1146: "Rock|Arena Rock",
1147: "Rock|Blues-Rock",
1148: "Rock|British Invasion",
1149: "Rock|Death Metal/Black Metal",
1150: "Rock|Glam Rock",
1151: "Rock|Hair Metal",
1152: "Rock|Hard Rock",
1153: "Rock|Metal",
1154: "Rock|Jam Bands",
1155: "Rock|Prog-Rock/Art Rock",
1156: "Rock|Psychedelic",
1157: "Rock|Rock & Roll",
1158: "Rock|Rockabilly",
1159: "Rock|Roots Rock",
1160: "Rock|Singer/Songwriter",
1161: "Rock|Southern Rock",
1162: "Rock|Surf",
1163: "Rock|Tex-Mex",
1165: "Soundtrack|Foreign Cinema",
1166: "Soundtrack|Musicals",
1167: "Comedy|Novelty",
1168: "Soundtrack|Original Score",
1169: "Soundtrack|Soundtrack",
1171: "Comedy|Standup Comedy",
1172: "Soundtrack|TV Soundtrack",
1173: "Vocal|Standards",
1174: "Vocal|Traditional Pop",
1175: "Jazz|Vocal Jazz",
1176: "Vocal|Vocal Pop",
1177: "African|Afro-Beat",
1178: "African|Afro-Pop",
1179: "World|Cajun",
1180: "World|Celtic",
1181: "World|Celtic Folk",
1182: "World|Contemporary Celtic",
1183: "Reggae|Modern Dancehall",
1184: "World|Drinking Songs",
1185: "Indian|Indian Pop",
1186: "World|Japanese Pop",
1187: "World|Klezmer",
1188: "World|Polka",
1189: "World|Traditional Celtic",
1190: "World|Worldbeat",
1191: "World|Zydeco",
1192: "Reggae|Roots Reggae",
1193: "Reggae|Dub",
1194: "Reggae|Ska",
1195: "World|Caribbean",
1196: "World|South America",
1197: "Arabic",
1198: "World|North America",
1199: "World|Hawaii",
1200: "World|Australia",
1201: "World|Japan",
1202: "World|France",
1203: "African",
1204: "World|Asia",
1205: "World|Europe",
1206: "World|South Africa",
1207: "Jazz|Hard Bop",
1208: "Jazz|Trad Jazz",
1209: "Jazz|Cool Jazz",
1210: "Blues|Acoustic Blues",
1211: "Classical|High Classical",
1220: "Brazilian|Axe",
1221: "Brazilian|Bossa Nova",
1222: "Brazilian|Choro",
1223: "Brazilian|Forro",
1224: "Brazilian|Frevo",
1225: "Brazilian|MPB",
1226: "Brazilian|Pagode",
1227: "Brazilian|Samba",
1228: "Brazilian|Sertanejo",
1229: "Brazilian|Baile Funk",
1230: "Alternative|Chinese Alt",
1231: "Alternative|Korean Indie",
1232: "Chinese",
1233: "Chinese|Chinese Classical",
1234: "Chinese|Chinese Flute",
1235: "Chinese|Chinese Opera",
1236: "Chinese|Chinese Orchestral",
1237: "Chinese|Chinese Regional Folk",
1238: "Chinese|Chinese Strings",
1239: "Chinese|Taiwanese Folk",
1240: "Chinese|Tibetan Native Music",
1241: "Hip-Hop/Rap|Chinese Hip-Hop",
1242: "Hip-Hop/Rap|Korean Hip-Hop",
1243: "Korean",
1244: "Korean|Korean Classical",
1245: "Korean|Korean Trad Song",
1246: "Korean|Korean Trad Instrumental",
1247: "Korean|Korean Trad Theater",
1248: "Rock|Chinese Rock",
1249: "Rock|Korean Rock",
1250: "Pop|C-Pop",
1251: "Pop|Cantopop/HK-Pop",
1252: "Pop|Korean Folk-Pop",
1253: "Pop|Mandopop",
1254: "Pop|Tai-Pop",
1255: "Pop|Malaysian Pop",
1256: "Pop|Pinoy Pop",
1257: "Pop|Original Pilipino Music",
1258: "Pop|Manilla Sound",
1259: "Pop|Indo Pop",
1260: "Pop|Thai Pop",
1261: "Vocal|Trot",
1262: "Indian",
1263: "Indian|Bollywood",
1264: "Indian|Regional Indian|Tamil",
1265: "Indian|Regional Indian|Telugu",
1266: "Indian|Regional Indian",
1267: "Indian|Devotional & Spiritual",
1268: "Indian|Sufi",
1269: "Indian|Indian Classical",
1270: "Russian|Russian Chanson",
1271: "World|Dini",
1272: "Turkish|Halk",
1273: "Turkish|Sanat",
1274: "World|Dangdut",
1275: "World|Indonesian Religious",
1276: "World|Calypso",
1277: "World|Soca",
1278: "Indian|Ghazals",
1279: "Indian|Indian Folk",
1280: "Turkish|Arabesque",
1281: "African|Afrikaans",
1282: "World|Farsi",
1283: "World|Israeli",
1284: "Arabic|Khaleeji",
1285: "Arabic|North African",
1286: "Arabic|Arabic Pop",
1287: "Arabic|Islamic",
1288: "Soundtrack|Sound Effects",
1289: "Folk",
1290: "Orchestral",
1291: "Marching",
1293: "Pop|Oldies",
1294: "Country|Thai Country",
1295: "World|Flamenco",
1296: "World|Tango",
1297: "World|Fado",
1298: "World|Iberia",
1299: "Russian",
1300: "Turkish",
100000: "Christian & Gospel",
100001: "Classical|Art Song",
100002: "Classical|Brass & Woodwinds",
100003: "Classical|Solo Instrumental",
100004: "Classical|Contemporary Era",
100005: "Classical|Oratorio",
100006: "Classical|Cantata",
100007: "Classical|Electronic",
100008: "Classical|Sacred",
100009: "Classical|Guitar",
100010: "Classical|Piano",
100011: "Classical|Violin",
100012: "Classical|Cello",
100013: "Classical|Percussion",
100014: "Electronic|Dubstep",
100015: "Electronic|Bass",
100016: "Hip-Hop/Rap|UK Hip-Hop",
100017: "Reggae|Lovers Rock",
100018: "Alternative|EMO",
100019: "Alternative|Pop Punk",
100020: "Alternative|Indie Pop",
100021: "New Age|Yoga",
100022: "Pop|Tribute",
100023: "Pop|Shows",
100024: "Cuban",
100025: "Cuban|Mambo",
100026: "Cuban|Chachacha",
100027: "Cuban|Guajira",
100028: "Cuban|Son",
100029: "Cuban|Bolero",
100030: "Cuban|Guaracha",
100031: "Cuban|Timba",
100032: "Soundtrack|Video Game",
100033: "Indian|Regional Indian|Punjabi|Punjabi Pop",
100034: "Indian|Regional Indian|Bengali|Rabindra Sangeet",
100035: "Indian|Regional Indian|Malayalam",
100036: "Indian|Regional Indian|Kannada",
100037: "Indian|Regional Indian|Marathi",
100038: "Indian|Regional Indian|Gujarati",
100039: "Indian|Regional Indian|Assamese",
100040: "Indian|Regional Indian|Bhojpuri",
100041: "Indian|Regional Indian|Haryanvi",
100042: "Indian|Regional Indian|Odia",
100043: "Indian|Regional Indian|Rajasthani",
100044: "Indian|Regional Indian|Urdu",
100045: "Indian|Regional Indian|Punjabi",
100046: "Indian|Regional Indian|Bengali",
100047: "Indian|Indian Classical|Carnatic Classical",
100048: "Indian|Indian Classical|Hindustani Classical",
100049: "African|Afro House",
100050: "African|Afro Soul",
100051: "African|Afrobeats",
100052: "African|Benga",
100053: "African|Bongo-Flava",
100054: "African|Coupe-Decale",
100055: "African|Gqom",
100056: "African|Highlife",
100057: "African|Kuduro",
100058: "African|Kizomba",
100059: "African|Kwaito",
100060: "African|Mbalax",
100061: "African|Ndombolo",
100062: "African|Shangaan Electro",
100063: "African|Soukous",
100064: "African|Taarab",
100065: "African|Zouglou",
100066: "Turkish|Ozgun",
100067: "Turkish|Fantezi",
100068: "Turkish|Religious",
100069: "Pop|Turkish Pop",
100070: "Rock|Turkish Rock",
100071: "Alternative|Turkish Alternative",
100072: "Hip-Hop/Rap|Turkish Hip-Hop/Rap",
100073: "African|Maskandi",
100074: "Russian|Russian Romance",
100075: "Russian|Russian Bard",
100076: "Russian|Russian Pop",
100077: "Russian|Russian Rock",
100078: "Russian|Russian Hip-Hop",
100079: "Arabic|Levant",
100080: "Arabic|Levant|Dabke",
100081: "Arabic|Maghreb Rai",
100082: "Arabic|Khaleeji|Khaleeji Jalsat",
100083: "Arabic|Khaleeji|Khaleeji Shailat",
100084: "Tarab",
100085: "Tarab|Iraqi Tarab",
100086: "Tarab|Egyptian Tarab",
100087: "Tarab|Khaleeji Tarab",
100088: "Pop|Levant Pop",
100089: "Pop|Iraqi Pop",
100090: "Pop|Egyptian Pop",
100091: "Pop|Maghreb Pop",
100092: "Pop|Khaleeji Pop",
100093: "Hip-Hop/Rap|Levant Hip-Hop",
100094: "Hip-Hop/Rap|Egyptian Hip-Hop",
100095: "Hip-Hop/Rap|Maghreb Hip-Hop",
100096: "Hip-Hop/Rap|Khaleeji Hip-Hop",
100097: "Alternative|Indie Levant",
100098: "Alternative|Indie Egyptian",
100099: "Alternative|Indie Maghreb",
100100: "Electronic|Levant Electronic",
100101: "Electronic|Electro-Cha'abi",
100102: "Electronic|Maghreb Electronic",
100103: "Folk|Iraqi Folk",
100104: "Folk|Khaleeji Folk",
100105: "Dance|Maghreb Dance",
50000061: "Spoken Word",
50000063: "Disney",
50000064: "French Pop",
50000066: "German Pop",
50000068: "German Folk",
}
// Detect PNG image if "implicit" class is used
var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
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{}
}
// 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)
return m, err
}
func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
for {
name, size, err := readAtomHeader(r)
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
case "moov", "udta", "ilst":
return m.readAtoms(r)
}
_, ok := atoms[name]
var data []string
if name == "----" {
name, data, err = readCustomAtom(r, size)
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
}
err = m.readAtomData(r, name, size-8, data)
if err != nil {
return err
}
}
}
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:]
if len(b) < 4 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 4, len(b))
}
if name == "gnre" {
m.data[name] = getInt(b[len(b)-1:])
return nil
}
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:]
}
if name == "trkn" || name == "disk" {
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":
if len(b) == 0 {
return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b))
}
data = getInt(b[len(b)-1:])
case "jpeg", "png":
data = &Picture{
Ext: contentType,
MIMEType: "image/" + contentType,
Data: b,
}
}
m.data[name] = data
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.
// We check that mean is "com.apple.iTunes" 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
}
// 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:]))
}
}
// there should remain only the header size
if size != 8 {
err := errors.New("---- atom out of bounds")
return "", nil, err
}
if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || len(data) == 0 {
return "----", nil, nil
}
return subNames["name"], data, nil
}
func (metadataMP4) Format() Format { return MP4 }
func (m metadataMP4) FileType() FileType { return m.fileType }
func (m metadataMP4) Raw() map[string]interface{} { return m.data }
func (m metadataMP4) getString(n []string) string {
for _, k := range n {
if x, ok := m.data[k]; ok {
return x.(string)
}
}
return ""
}
func (m metadataMP4) getInt(n []string) int {
for _, k := range n {
if x, ok := m.data[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 {
genre := m.getString(atoms.Name("genre"))
if len(genre) == 0 {
genreID := m.getInt(atoms.Name("genre ID"))
if genreID == 0 {
genreID := m.getInt(atoms.Name("genre ID3v1 ID")) - 1
genre = id3v1Genres[genreID]
} else {
genre = genreIDValues[genreID]
}
}
return 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 {
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 {
return x, n.(int)
}
return x, 0
}
func (m metadataMP4) Lyrics() string {
t, ok := m.data["\xa9lyr"]
if !ok {
return ""
}
return t.(string)
}
func (m metadataMP4) Comment() string {
t, ok := m.data["\xa9cmt"]
if !ok {
return ""
}
return t.(string)
}
func (m metadataMP4) Picture() *Picture {
v, ok := m.data["covr"]
if !ok {
return nil
}
p, _ := v.(*Picture)
return p
}