From 406069467d3b7bcbc3a0f923f19ad7aa3551a419 Mon Sep 17 00:00:00 2001 From: Magnus Holmgren Date: Wed, 11 Oct 2006 17:42:33 +0000 Subject: More flexible MP4 file metadata parser. git-svn-id: svn://svn.rockbox.org/rockbox/trunk@11189 a1c6a512-1295-4272-9138-f99709370657 --- apps/metadata.c | 723 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 427 insertions(+), 296 deletions(-) diff --git a/apps/metadata.c b/apps/metadata.c index 1b2dde3a24..ff0cb76cbe 100644 --- a/apps/metadata.c +++ b/apps/metadata.c @@ -22,6 +22,7 @@ #include #include +#include "errno.h" #include "metadata.h" #include "mp3_playback.h" #include "logf.h" @@ -40,6 +41,37 @@ enum tagtype { TAGTYPE_APE = 1, TAGTYPE_VORBIS }; #define TAG_NAME_LENGTH 32 #define TAG_VALUE_LENGTH 128 +#define MP4_ID(a, b, c, d) (((a) << 24) | ((b) << 16) | ((c) << 8) | (d)) + +#define MP4_3gp6 MP4_ID('3', 'g', 'p', '6') +#define MP4_alac MP4_ID('a', 'l', 'a', 'c') +#define MP4_calb MP4_ID(0xa9, 'a', 'l', 'b') +#define MP4_cART MP4_ID(0xa9, 'A', 'R', 'T') +#define MP4_cnam MP4_ID(0xa9, 'n', 'a', 'm') +#define MP4_cwrt MP4_ID(0xa9, 'w', 'r', 't') +#define MP4_ftyp MP4_ID('f', 't', 'y', 'p') +#define MP4_gnre MP4_ID('g', 'n', 'r', 'e') +#define MP4_hdlr MP4_ID('h', 'd', 'l', 'r') +#define MP4_ilst MP4_ID('i', 'l', 's', 't') +#define MP4_M4A MP4_ID('M', '4', 'A', ' ') +#define MP4_mdat MP4_ID('m', 'd', 'a', 't') +#define MP4_mdia MP4_ID('m', 'd', 'i', 'a') +#define MP4_mdir MP4_ID('m', 'd', 'i', 'r') +#define MP4_meta MP4_ID('m', 'e', 't', 'a') +#define MP4_minf MP4_ID('m', 'i', 'n', 'f') +#define MP4_moov MP4_ID('m', 'o', 'o', 'v') +#define MP4_mp4a MP4_ID('m', 'p', '4', 'a') +#define MP4_mp42 MP4_ID('m', 'p', '4', '2') +#define MP4_qt MP4_ID('q', 't', ' ', ' ') +#define MP4_soun MP4_ID('s', 'o', 'u', 'n') +#define MP4_stbl MP4_ID('s', 't', 'b', 'l') +#define MP4_stsd MP4_ID('s', 't', 's', 'd') +#define MP4_stts MP4_ID('s', 't', 't', 's') +#define MP4_trak MP4_ID('t', 'r', 'a', 'k') +#define MP4_trkn MP4_ID('t', 'r', 'k', 'n') +#define MP4_udta MP4_ID('u', 'd', 't', 'a') +#define MP4_extra MP4_ID('-', '-', '-', '-') + struct apetag_header { char id[8]; @@ -190,28 +222,31 @@ static void convert_endian(void *data, const char *format) } } -/* read_uint32be() - read an unsigned integer from a big-endian - (e.g. Quicktime) file. This is used by the .m4a parser -*/ +/* Read an unsigned 16-bit integer from a big-endian file. */ #ifdef ROCKBOX_BIG_ENDIAN -#define read_uint32be(fd,buf) read((fd),(buf),4) +#define read_uint16be(fd, buf) read((fd), (buf), 2) #else -int read_uint32be(int fd, unsigned int* buf) { - char tmp; - char* p=(char*)buf; - size_t n; +int read_uint16be(int fd, unsigned short* buf) +{ + size_t n; + + n = read(fd, (char*) buf, 2); + *buf = betoh16(*buf); + return n; +} +#endif - n=read(fd,p,4); - if (n==4) { - tmp=p[0]; - p[0]=p[3]; - p[3]=tmp; - tmp=p[2]; - p[2]=p[1]; - p[1]=tmp; - } +/* Read an unsigned 32-bit integer from a big-endian file. */ +#ifdef ROCKBOX_BIG_ENDIAN +#define read_uint32be(fd,buf) read((fd), (buf), 4) +#else +int read_uint32be(int fd, unsigned int* buf) +{ + size_t n; - return(n); + n = read(fd, (char*) buf, 4); + *buf = betoh32(*buf); + return n; } #endif @@ -921,292 +956,388 @@ static bool get_wave_metadata(int fd, struct mp3entry* id3) return true; } -static bool get_m4a_metadata(int fd, struct mp3entry* id3) +/* Read the tag data from an MP4 file, storing up to buffer_size bytes in + * buffer. + */ +unsigned long read_mp4_tag(int fd, unsigned int size_left, char* buffer, + unsigned int buffer_left) { - unsigned char* buf; - unsigned long totalsamples; - int i,j,k; - size_t n; - size_t bytes_remaining; - char* id3buf; - unsigned int compressedsize; - unsigned int sample_count; - unsigned int sample_duration; - int numentries; - int entry_size; - int size_remaining; - int chunk_len; - unsigned char chunk_id[4]; - int sub_chunk_len; - unsigned char sub_chunk_id[4]; - - /* A simple parser to read vital metadata from an ALAC file. - This parser also works for AAC files - they are both stored in - a Quicktime M4A container. */ - - /* Use the trackname part of the id3 structure as a temporary buffer */ - buf=id3->path; - - lseek(fd, 0, SEEK_SET); - - totalsamples=0; - compressedsize=0; - /* read the chunks - we stop when we find the mdat chunk and set compressedsize */ - while (compressedsize==0) { - n=read_uint32be(fd,&chunk_len); - - // This means it was a 64-bit file, so we have problems. - if (chunk_len == 1) { - logf("need 64bit support\n"); - return false; - } - - n=read(fd,&chunk_id,4); - if (n < 4) - return false; - - if (memcmp(&chunk_id,"ftyp",4)==0) { - /* Check for M4A type */ - n=read(fd,&chunk_id,4); - if ((memcmp(&chunk_id,"M4A ",4)!=0) && - (memcmp(&chunk_id,"mp42",4)!=0)) { - logf("Not an M4A file, aborting\n"); - return false; - } - /* Skip rest of chunk */ - lseek(fd, chunk_len - 8 - 4, SEEK_CUR); /* FIXME not 8 */ - } else if (memcmp(&chunk_id,"moov",4)==0) { - size_remaining=chunk_len - 8; /* FIXME not 8 */ - - while (size_remaining > 0) { - n=read_uint32be(fd,&sub_chunk_len); - if ((sub_chunk_len < 1) || (sub_chunk_len > size_remaining)) { - logf("Strange sub_chunk_len value inside moov: %d (remaining: %d)\n",sub_chunk_len,size_remaining); - return false; + unsigned int bytes_read = 0; + + if (buffer_left == 0) + { + lseek(fd, size_left, SEEK_CUR); /* Skip everything */ + } + else + { + /* Skip the data tag header - maybe we should parse it properly? */ + lseek(fd, 16, SEEK_CUR); + size_left -= 16; + + if (size_left > buffer_left) + { + read(fd, buffer, buffer_left); + lseek(fd, size_left - buffer_left, SEEK_CUR); + bytes_read = buffer_left; + } + else + { + read(fd, buffer, size_left); + bytes_read = size_left; + } + } + + return bytes_read; +} + +/* Read a string tag from an MP4 file */ +unsigned int read_mp4_tag_string(int fd, int size_left, char** buffer, + unsigned int* buffer_left, char** dest) +{ + unsigned int bytes_read = read_mp4_tag(fd, size_left, *buffer, + *buffer_left - 1); + unsigned int length = 0; + + if (bytes_read) + { + (*buffer)[bytes_read] = 0; + *dest = *buffer; + length = strlen(*buffer) + 1; + *buffer_left -= length; + *buffer += length; + } + else + { + *dest = NULL; + } + + return length; +} + +static unsigned int read_mp4_atom(int fd, unsigned int* size, + unsigned int* type, unsigned int size_left) +{ + read_uint32be(fd, size); + read_uint32be(fd, type); + + if (*size == 1) + { + /* FAT32 doesn't support files this big, so something seems to + * be wrong. (64-bit sizes should only be used when required.) + */ + errno = EFBIG; + *type = 0; + return 0; + } + + if (*size > 0) + { + if (*size > size_left) + { + size_left = 0; } - n=read(fd,&sub_chunk_id,4); - size_remaining-=8; - - if (memcmp(&sub_chunk_id,"mvhd",4)==0) { - /* We don't need anything from here - skip */ - lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */ - size_remaining-=(sub_chunk_len-8); - } else if (memcmp(&sub_chunk_id,"udta",4)==0) { - /* The udta chunk contains the metadata - track, artist, album etc. - The format appears to be: - udta - meta - hdlr - ilst - .nam - [rest of tags] - free - - NOTE: This code was written by examination of some .m4a files - produced by iTunes v4.9 - it may not therefore be 100% - compliant with all streams. But it should fail gracefully. - */ - j=(sub_chunk_len-8); - size_remaining-=j; - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - j-=8; - if (memcmp(&sub_chunk_id,"meta",4)==0) { - lseek(fd, 4, SEEK_CUR); - j-=4; - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - j-=8; - if (memcmp(&sub_chunk_id,"hdlr",4)==0) { - lseek(fd, sub_chunk_len - 8, SEEK_CUR); - j-=(sub_chunk_len - 8); - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - j-=8; - if (memcmp(&sub_chunk_id,"ilst",4)==0) { - /* Here are the actual tags. We use the id3v2 300-byte buffer - to store the string data */ - bytes_remaining=sizeof(id3->id3v2buf); - id3->genre=255; /* Not every track is the Blues */ - id3buf=id3->id3v2buf; - k=sub_chunk_len-8; - j-=k; - while (k > 0) { - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - k-=8; - if (memcmp(sub_chunk_id,"\251nam",4)==0) { - read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->title); - } else if (memcmp(sub_chunk_id,"\251ART",4)==0) { - read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->artist); - } else if (memcmp(sub_chunk_id,"\251alb",4)==0) { - read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->album); - } else if (memcmp(sub_chunk_id,"\251gen",4)==0) { - read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->genre_string); - } else if (memcmp(sub_chunk_id,"\251day",4)==0) { - read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->year_string); - } else if (memcmp(sub_chunk_id,"trkn",4)==0) { - if (sub_chunk_len==0x20) { - read(fd,buf,sub_chunk_len-8); - id3->tracknum=buf[19]; - } else { - lseek(fd, sub_chunk_len-8,SEEK_CUR); + else + { + size_left -= *size; + } + + *size -= 8; + } + else + { + *size = size_left; + size_left = 0; + } + + return size_left; +} + +static bool read_mp4_tags(int fd, struct mp3entry* id3, + unsigned int size_left) +{ + unsigned int size; + unsigned int type; + unsigned int buffer_left = sizeof(id3->id3v2buf) + sizeof(id3->id3v1buf); + char* buffer = id3->id3v2buf; + bool cwrt = false; + + do + { + size_left = read_mp4_atom(fd, &size, &type, size_left); + + /* DEBUGF("Tag atom: '%c%c%c%c' (%d bytes left)\n", type >> 24 & 0xff, + type >> 16 & 0xff, type >> 8 & 0xff, type & 0xff, size); */ + + switch (type) + { + case MP4_cnam: + read_mp4_tag_string(fd, size, &buffer, &buffer_left, + &id3->title); + break; + + case MP4_cART: + read_mp4_tag_string(fd, size, &buffer, &buffer_left, + &id3->artist); + break; + + case MP4_calb: + read_mp4_tag_string(fd, size, &buffer, &buffer_left, + &id3->album); + break; + + case MP4_cwrt: + read_mp4_tag_string(fd, size, &buffer, &buffer_left, + &id3->composer); + cwrt = false; + break; + + case MP4_gnre: + { + unsigned short genre; + + read_mp4_tag(fd, size, (char*) &genre, sizeof(genre)); + id3->genre = betoh16(genre); + } + break; + + case MP4_trkn: + { + unsigned short n[2]; + + read_mp4_tag(fd, size, (char*) &n, sizeof(n)); + id3->tracknum = betoh16(n[1]); + } + break; + + case MP4_extra: + { + char tag_name[TAG_NAME_LENGTH]; + unsigned int sub_size; + + /* "mean" atom */ + read_uint32be(fd, &sub_size); + size -= sub_size; + lseek(fd, sub_size - 4, SEEK_CUR); + /* "name" atom */ + read_uint32be(fd, &sub_size); + size -= sub_size; + lseek(fd, 8, SEEK_CUR); + sub_size -= 12; + + if (sub_size > sizeof(tag_name) - 1) + { + read(fd, tag_name, sizeof(tag_name) - 1); + lseek(fd, sub_size - sizeof(tag_name) - 1, SEEK_CUR); + tag_name[sizeof(tag_name) - 1] = 0; + } + else + { + read(fd, tag_name, sub_size); + tag_name[sub_size] = 0; + } + + if ((strcasecmp(tag_name, "composer") == 0) && !cwrt) + { + read_mp4_tag_string(fd, size, &buffer, &buffer_left, + &id3->composer); + } + else + { + char* any; + unsigned int length = read_mp4_tag_string(fd, size, + &buffer, &buffer_left, &any); + + if (length > 0) + { + /* Re-use the read buffer as the dest buffer... */ + buffer -= length; + buffer_left += length; + + parse_replaygain(tag_name, buffer, id3, buffer, + buffer_left); } - } else { - lseek(fd, sub_chunk_len-8,SEEK_CUR); - } - k-=(sub_chunk_len-8); } - } } - } - /* Skip any remaining data in udta chunk */ - lseek(fd, j, SEEK_CUR); - } else if (memcmp(&sub_chunk_id,"trak",4)==0) { - /* Format of trak chunk: - tkhd - mdia - mdhd - hdlr - minf - smhd - dinf - stbl - stsd - Samplerate, Samplesize, Numchannels - stts - time_to_sample array - RLE'd table containing duration of each block - stsz - sample_byte_size array - ?Size in bytes of each compressed block - stsc - Seek table related? - stco - Seek table related? - */ - - /* Skip tkhd - not needed */ - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - if (memcmp(&sub_chunk_id,"tkhd",4)!=0) { - logf("Expecting tkhd\n"); - return false; - } - lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */ - size_remaining-=sub_chunk_len; - - /* Process mdia - skipping possible edts */ - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - if (memcmp(&sub_chunk_id,"edts",4)==0) { - lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */ - size_remaining-=sub_chunk_len; - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - } - - if (memcmp(&sub_chunk_id,"mdia",4)!=0) { - logf("Expecting mdia\n"); - return false; - } - size_remaining-=sub_chunk_len; - j=sub_chunk_len-8; - - while (j > 0) { - n=read_uint32be(fd,&sub_chunk_len); - n=read(fd,&sub_chunk_id,4); - j-=4; - if (memcmp(&sub_chunk_id,"minf",4)==0) { - j=sub_chunk_len-8; - } else if (memcmp(&sub_chunk_id,"stbl",4)==0) { - j=sub_chunk_len-8; - } else if (memcmp(&sub_chunk_id,"stsd",4)==0) { - n=read(fd,buf,sub_chunk_len-8); - j-=sub_chunk_len; - i=0; - /* Skip version and flags */ - i+=4; - - numentries=(buf[i]<<24)|(buf[i+1]<<16)|(buf[i+2]<<8)|buf[i+3]; - i+=4; - if (numentries!=1) { - logf("ERROR: Expecting only one entry in stsd\n"); - } - - entry_size=(buf[i]<<24)|(buf[i+1]<<16)|(buf[i+2]<<8)|buf[i+3]; - i+=4; - - if (memcmp(&buf[i],"alac",4)==0) { - id3->codectype=AFMT_ALAC; - } else if (memcmp(&buf[i],"mp4a",4)==0) { - id3->codectype=AFMT_AAC; - } else { - logf("Not an ALAC or AAC file\n"); - return false; - } - - //numchannels=(buf[i+20]<<8)|buf[i+21]; /* Not used - assume Stereo */ - //samplesize=(buf[i+22]<<8)|buf[i+23]; /* Not used - assume 16-bit */ - - /* Samplerate is 32-bit fixed point, but this works for < 65536 Hz */ - id3->frequency=(buf[i+28]<<8)|buf[i+29]; - } else if (memcmp(&sub_chunk_id,"stts",4)==0) { - j-=sub_chunk_len; - i=8; - n=read(fd,buf,8); - i+=8; - numentries=(buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7]; - for (k=0;k 0) lseek(fd, sub_chunk_len - i, SEEK_CUR); - } else if (memcmp(&sub_chunk_id,"stsz",4)==0) { - j-=sub_chunk_len; - i=8; - n=read(fd,buf,8); - i+=8; - numentries=(buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7]; - for (k=0;k 0) lseek(fd, sub_chunk_len - i, SEEK_CUR); - } else { - lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */ - j-=sub_chunk_len; - } - } - } else { - logf("Unexpected sub_chunk_id inside moov: %c%c%c%c\n", - sub_chunk_id[0],sub_chunk_id[1],sub_chunk_id[2],sub_chunk_id[3]); - return false; + break; + + default: + lseek(fd, size, SEEK_CUR); + break; } - } - } else if (memcmp(&chunk_id,"mdat",4)==0) { - /* once we hit mdat we stop reading and return. - * this is on the assumption that there is no furhter interesting - * stuff in the stream. if there is stuff will fail (:()). - * But we need the read pointer to be at the mdat stuff - * for the decoder. And we don't want to rely on fseek/ftell, - * as they may not always be avilable */ - lseek(fd, chunk_len - 8, SEEK_CUR); /* FIXME not 8 */ - compressedsize=chunk_len-8; - } else if (memcmp(&chunk_id,"free",4)==0) { - /* these following atoms can be skipped !!!! */ - lseek(fd, chunk_len - 8, SEEK_CUR); /* FIXME not 8 */ - } else { - logf("(top) unknown chunk id: %c%c%c%c\n", chunk_id[0],chunk_id[1],chunk_id[2],chunk_id[3]); - return false; } - } + while ((size_left > 0) && (errno == 0)); - id3->vbr=true; /* All ALAC files are VBR */ - id3->filesize=filesize(fd); - id3->samples=totalsamples; - id3->length=(10*totalsamples)/(id3->frequency/100); - id3->bitrate=(compressedsize*8)/id3->length;; + return true; +} - return true; -} +static bool read_mp4_container(int fd, struct mp3entry* id3, + unsigned int size_left) +{ + unsigned int size; + unsigned int type; + unsigned int handler = 0; + bool rc = true; + + do + { + size_left = read_mp4_atom(fd, &size, &type, size_left); + + /* DEBUGF("Atom: '%c%c%c%c' (0x%08x, %d bytes left)\n", + (type >> 24) & 0xff, (type >> 16) & 0xff, (type >> 8) & 0xff, + type & 0xff, type, size); */ + + switch (type) + { + case MP4_ftyp: + { + unsigned int id; + + read_uint32be(fd, &id); + size -= 4; + + if ((id != MP4_M4A) && (id != MP4_mp42) && (id != MP4_qt) + && (id != MP4_3gp6)) + { + DEBUGF("Unknown MP4 file type: '%c%c%c%c'\n", + id >> 24 & 0xff, id >> 16 & 0xff, id >> 8 & 0xff, + id & 0xff); + return false; + } + } + break; + + case MP4_meta: + lseek(fd, 4, SEEK_CUR); /* Skip version */ + size -= 4; + /* Fall through */ + + case MP4_moov: + case MP4_udta: + case MP4_mdia: + case MP4_stbl: + case MP4_trak: + rc = read_mp4_container(fd, id3, size); + size = 0; + break; + + case MP4_ilst: + if (handler == MP4_mdir) + { + rc = read_mp4_tags(fd, id3, size); + size = 0; + } + break; + + case MP4_minf: + if (handler == MP4_soun) + { + rc = read_mp4_container(fd, id3, size); + size = 0; + } + break; + + case MP4_stsd: + lseek(fd, 8, SEEK_CUR); + size -= 8; + rc = read_mp4_container(fd, id3, size); + size = 0; + break; + + case MP4_hdlr: + lseek(fd, 8, SEEK_CUR); + read_uint32be(fd, &handler); + size -= 12; + /* DEBUGF(" Handler '%c%c%c%c'\n", handler >> 24 & 0xff, + handler >> 16 & 0xff, handler >> 8 & 0xff,handler & 0xff); */ + break; + + case MP4_stts: + { + unsigned int entries; + unsigned int i; + + lseek(fd, 4, SEEK_CUR); + read_uint32be(fd, &entries); + id3->samples = 0; + + for (i = 0; i < entries; i++) + { + unsigned int n; + unsigned int l; + + read_uint32be(fd, &n); + read_uint32be(fd, &l); + id3->samples += n * l; + } + + size = 0; + } + break; + + case MP4_mp4a: + case MP4_alac: + { + unsigned int frequency; + + id3->codectype = (type == MP4_mp4a) ? AFMT_AAC : AFMT_ALAC; + lseek(fd, 22, SEEK_CUR); + read_uint32be(fd, &frequency); + size -= 26; + id3->frequency = frequency; + /* There're some child atoms here, but we don't need them */ + } + break; + + case MP4_mdat: + id3->filesize = size; + break; + + default: + break; + } + + lseek(fd, size, SEEK_CUR); + } + while (rc && (size_left > 0) && (errno == 0) && (id3->filesize == 0)); + /* Break on non-zero filesize, since Rockbox currently doesn't support + * metadata after the mdat atom (which sets the filesize field). + */ + + return rc; +} + +static bool get_mp4_metadata(int fd, struct mp3entry* id3) +{ + id3->codectype = AFMT_UNKNOWN; + id3->genre = 255; + id3->filesize = 0; + errno = 0; + + if (read_mp4_container(fd, id3, filesize(fd)) && (errno == 0) + && (id3->samples > 0) && (id3->frequency > 0) + && (id3->filesize > 0)) + { + if (id3->codectype == AFMT_UNKNOWN) + { + logf("Not an ALAC or AAC file"); + return false; + } + + id3->length = (id3->samples / id3->frequency) * 1000; + id3->bitrate = ((int64_t) id3->filesize * 8) / id3->length; + DEBUGF("MP4 bitrate %d, frequency %d Hz, length %d ms\n", + id3->bitrate, id3->frequency, id3->length); + } + else + { + logf("MP4 metadata error"); + DEBUGF("MP4 metadata error. errno %d, length %d, frequency %d, filesize %d\n", + errno, id3->length, id3->frequency, id3->filesize); + return false; + } + + return true; +} static bool get_musepack_metadata(int fd, struct mp3entry *id3) { @@ -1718,7 +1849,7 @@ bool get_metadata(struct track_info* track, int fd, const char* trackname, case AFMT_ALAC: case AFMT_AAC: - if (!get_m4a_metadata(fd, &(track->id3))) + if (!get_mp4_metadata(fd, &(track->id3))) { return false; } -- cgit v1.2.3