This recipe yields the atoms contained in an MP4 file. Mostly used for extracting the tags contained in it (artist, title etc) using a convenience class (M4ATags). Implemented as an generator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | import struct
FLAGS= CONTAINER, SKIPPER, TAGITEM, IGNORE, NOVERN, XTAGITEM= [2**_ for _ in xrange(6)]
# CONTAINER: datum contains other boxes
# SKIPPER: ignore first 4 bytes of datum
# TAGITEM: "official" tag item
# NOVERN: datum is 8 bytes (2 4-bytes BE integers)
# XTAGITEM: datum is a triplet (I believe) of "mean", "name", "data" items
CALLBACK= TAGITEM | XTAGITEM
FLAGS.append(CALLBACK)
TAGTYPES= (
('ftyp', 0),
('moov', CONTAINER),
('mdat', 0),
('udta', CONTAINER),
('meta', CONTAINER|SKIPPER),
('ilst', CONTAINER),
('\xa9ART', TAGITEM),
('\xa9nam', TAGITEM),
('\xa9too', TAGITEM),
('\xa9alb', TAGITEM),
('\xa9day', TAGITEM),
('\xa9gen', TAGITEM),
('\xa9wrt', TAGITEM),
('trkn', TAGITEM|NOVERN),
('\xa9cmt', TAGITEM),
('trak', CONTAINER),
('----', XTAGITEM),
('mdia', CONTAINER),
('minf', CONTAINER),
)
flagged= {}
for flag in FLAGS:
flagged[flag]= frozenset(_[0] for _ in TAGTYPES if _[1] & flag)
def _xtra(s):
"Convert '----' atom data into dictionaries"
offset= 0
result= {}
while offset < len(s):
atomsize= struct.unpack("!i", s[offset:offset+4])[0]
atomtype= s[offset+4:offset+8]
if atomtype == "data":
result[atomtype]= s[offset+16:offset+atomsize]
else:
result[atomtype]= s[offset+12:offset+atomsize]
offset+= atomsize
return result
def _analyse(fp, offset0, offset1):
"Walk the atom tree in a mp4 file"
offset= offset0
while offset < offset1:
fp.seek(offset)
atomsize= struct.unpack("!i", fp.read(4))[0]
atomtype= fp.read(4)
if atomtype in flagged[CONTAINER]:
data= ''
for reply in _analyse(fp, offset+(atomtype in flagged[SKIPPER] and 12 or 8),
offset+atomsize):
yield reply
else:
fp.seek(offset+8)
if atomtype in flagged[TAGITEM]:
data=fp.read(atomsize-8)[16:]
if atomtype in flagged[NOVERN]:
data= struct.unpack("!ii", data)
elif atomtype in flagged[XTAGITEM]:
data= _xtra(fp.read(atomsize-8))
else:
data= fp.read(min(atomsize-8, 32))
if not atomtype in flagged[IGNORE]: yield atomtype, atomsize, data
offset+= atomsize
def mp4_atoms(pathname):
fp= open(pathname, "rb")
fp.seek(0,2)
size=fp.tell()
for atom in _analyse(fp, 0, size):
yield atom
fp.close()
class M4ATags(dict):
"An example class reading .m4a tags"
cvt= {
'trkn': 'Track',
'\xa9ART': 'Artist',
'\xa9nam': 'Title',
'\xa9alb': 'Album',
'\xa9day': 'Year',
'\xa9gen': 'Genre',
'\xa9cmt': 'Comment',
'\xa9wrt': 'Writer',
'\xa9too': 'Tool',
}
def __init__(self, pathname=None):
super(dict, self).__init__()
if pathname is None: return
for atomtype, atomsize, atomdata in mp4_atoms(pathname):
self.atom2tag(atomtype, atomdata)
def atom2tag(self, atomtype, atomdata):
"Insert items using descriptive key instead of atomtype"
if atomtype == "----":
key= atomdata['name'].title()
value= atomdata['data'].decode("utf-8")
else:
try: key= self.cvt[atomtype]
except KeyError: return
if atomtype == "trkn":
value= atomdata[0]
else:
try: value= atomdata.decode("utf-8")
except AttributeError:
print `atomtype`, `atomdata`
raise
self[key]= value
if __name__=="__main__":
import sys, pprint
r= M4ATag(sys.argv[1]) # pathname of an .mp4/.m4a file as first argument
pprint.pprint(r)
|
This is the result of a lot of trial and error, and it is not guaranteed to be of industrial strength; it's working fine though for tag extraction on all the .m4a files I have in my possession.
I couldn't find another Python implementation or a straightforward reference to the format, so I gathered info from here and there in order to do this; if you are more knowledgeable about MP4 items and their roles, you are very welcome updating the TAGTYPES tuple or any of the code. I assume there must be some options for multiple values in a tag, for example, since that would explain some bytes I tend to completely ignore in the code above, but since I had no such cases or a way to produce multiple values, I wouldn't know.
Hope it helps you.
anyone know what's up with Genre tag? none of the open source tag libs I've found are able to correctly parse the genre tag in my aac files. iTunes is able to read it, as are other tools, so it is in there somehow, but the gen tag in this script (and every other I've tried) always comes up empty. I've finally got access to every tag i need, except for genre, so this is important.
For genre: You need to modify this script slightly. First, add ('gnre', TAGITEM) to the TAGTYPES list at the top. In the class attribute M4ATags.cvt, add 'gnre': 'Genre'
This will add the genre value to the dictionary. However, iTunes does not use a plain text string to encode this. It is a binary packed index value of a hard-coded set of defined genres. To decode this value:
struct.unpack('!h', genre)
This yields an integer. Now take a look at http://kobesearch.cpan.org/htdocs/Audio-M4P/Audio/M4P/QuickTime.pm.html Search for "our @genre_strings", and there is the big list of them. For example, if your genre value is 21, it is Alternative.
Custom genres are stored in the iTunes XML.
Cheers,
The script doesn't deal with the special size values. In particular, if the atom size is 1 then the true size is a uint64_t that follows (took me a bit to figure out while playing with this script).
On line 78 (in _analyse()) replace:
with: