14
12
There is no official specification for this format. The source code
15
13
for TagLib, FAAD, and various MPEG specifications at
16
http://developer.apple.com/documentation/QuickTime/QTFF/,
17
http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt,
18
http://standards.iso.org/ittf/PubliclyAvailableStandards/c041828_ISO_IEC_14496-12_2005(E).zip,
19
and http://wiki.multimedia.cx/index.php?title=Apple_QuickTime were all
15
* http://developer.apple.com/documentation/QuickTime/QTFF/
16
* http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt
17
* http://standards.iso.org/ittf/PubliclyAvailableStandards/\
18
c041828_ISO_IEC_14496-12_2005(E).zip
19
* http://wiki.multimedia.cx/index.php?title=Apple_QuickTime
26
27
from mutagen import FileType, Metadata
27
28
from mutagen._constants import GENRES
28
from mutagen._util import cdata, insert_bytes, delete_bytes, DictProxy, utf8
30
class error(IOError): pass
31
class MP4MetadataError(error): pass
32
class MP4StreamInfoError(error): pass
33
class MP4MetadataValueError(ValueError, MP4MetadataError): pass
29
from mutagen._util import cdata, insert_bytes, DictProxy, utf8
36
class MP4MetadataError(error):
40
class MP4StreamInfoError(error):
44
class MP4MetadataValueError(ValueError, MP4MetadataError):
35
48
# This is not an exhaustive list of container atoms, but just the
36
49
# ones this module needs to peek inside.
37
50
_CONTAINERS = ["moov", "udta", "trak", "mdia", "meta", "ilst",
38
51
"stbl", "minf", "moof", "traf"]
39
_SKIP_SIZE = { "meta": 4 }
52
_SKIP_SIZE = {"meta": 4}
41
54
__all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MP4FreeForm']
43
57
class MP4Cover(str):
44
58
"""A cover artwork.
47
imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG)
62
* imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG)
52
def __new__(cls, data, imageformat=None):
53
self = str.__new__(cls, data)
54
if imageformat is None: imageformat = MP4Cover.FORMAT_JPEG
67
def __new__(cls, data, *args, **kwargs):
68
return str.__new__(cls, data)
70
def __init__(self, data, imageformat=FORMAT_JPEG):
55
71
self.imageformat = imageformat
57
74
except AttributeError:
58
75
self.format = imageformat
62
78
class MP4FreeForm(str):
63
79
"""A freeform value.
66
dataformat -- format of the data (either FORMAT_TEXT or FORMAT_DATA)
83
* dataformat -- format of the data (either FORMAT_TEXT or FORMAT_DATA)
71
def __new__(cls, data, dataformat=None):
72
self = str.__new__(cls, data)
73
if dataformat is None:
74
dataformat = MP4FreeForm.FORMAT_TEXT
89
def __new__(cls, data, *args, **kwargs):
90
return str.__new__(cls, data)
92
def __init__(self, data, dataformat=FORMAT_TEXT):
75
93
self.dataformat = dataformat
79
96
class Atom(object):
185
207
list of three atoms, corresponding to the moov, udta, and meta
189
212
for name in names:
190
path.append(path[-1][name,])
213
path.append(path[-1][name, ])
216
def __contains__(self, names):
193
223
def __getitem__(self, names):
194
224
"""Look up a child atom.
196
226
'names' may be a list of atoms (['moov', 'udta']) or a string
197
227
specifying the complete path ('moov.udta').
199
230
if isinstance(names, basestring):
200
231
names = names.split(".")
201
232
for child in self.atoms:
202
233
if child.name == names[0]:
203
234
return child[names[1:]]
205
raise KeyError, "%s not found" % names[0]
236
raise KeyError("%s not found" % names[0])
207
238
def __repr__(self):
208
239
return "\n".join([repr(child) for child in self.atoms])
210
242
class MP4Tags(DictProxy, Metadata):
211
"""Dictionary containing Apple iTunes metadata list key/values.
243
r"""Dictionary containing Apple iTunes metadata list key/values.
213
245
Keys are four byte identifiers, except for freeform ('----')
214
246
keys. Values are usually unicode strings, but some atoms have a
215
247
special structure:
217
249
Text values (multiple values per key are supported):
218
'\xa9nam' -- track title
221
'aART' -- album artist
222
'\xa9wrt' -- composer
225
'desc' -- description (usually used in podcasts)
226
'purd' -- purchase date
227
'\xa9grp' -- grouping
230
'purl' -- podcast URL
231
'egid' -- podcast episode GUID
232
'catg' -- podcast category
233
'keyw' -- podcast keywords
234
'\xa9too' -- encoded by
236
'soal' -- album sort order
237
'soaa' -- album artist sort order
238
'soar' -- artist sort order
239
'sonm' -- title sort order
240
'soco' -- composer sort order
241
'sosn' -- show sort order
251
* '\\xa9nam' -- track title
252
* '\\xa9alb' -- album
253
* '\\xa9ART' -- artist
254
* 'aART' -- album artist
255
* '\\xa9wrt' -- composer
257
* '\\xa9cmt' -- comment
258
* 'desc' -- description (usually used in podcasts)
259
* 'purd' -- purchase date
260
* '\\xa9grp' -- grouping
261
* '\\xa9gen' -- genre
262
* '\\xa9lyr' -- lyrics
263
* 'purl' -- podcast URL
264
* 'egid' -- podcast episode GUID
265
* 'catg' -- podcast category
266
* 'keyw' -- podcast keywords
267
* '\\xa9too' -- encoded by
268
* 'cprt' -- copyright
269
* 'soal' -- album sort order
270
* 'soaa' -- album artist sort order
271
* 'soar' -- artist sort order
272
* 'sonm' -- title sort order
273
* 'soco' -- composer sort order
274
* 'sosn' -- show sort order
275
* 'tvsh' -- show name
245
'cpil' -- part of a compilation
246
'pgap' -- part of a gapless album
247
'pcst' -- podcast (iTunes reads this only on import)
279
* 'cpil' -- part of a compilation
280
* 'pgap' -- part of a gapless album
281
* 'pcst' -- podcast (iTunes reads this only on import)
249
283
Tuples of ints (multiple values per key are supported):
250
'trkn' -- track number, total tracks
251
'disk' -- disc number, total discs
285
* 'trkn' -- track number, total tracks
286
* 'disk' -- disc number, total discs
254
'tmpo' -- tempo/BPM, 16 bit int
255
'covr' -- cover artwork, list of MP4Cover objects (which are
257
'gnre' -- ID3v1 genre. Not supported, use '\xa9gen' instead.
290
* 'tmpo' -- tempo/BPM, 16 bit int
291
* 'covr' -- cover artwork, list of MP4Cover objects (which are
293
* 'gnre' -- ID3v1 genre. Not supported, use '\\xa9gen' instead.
259
295
The freeform '----' frames use a key in the format '----:mean:name'
260
296
where 'mean' is usually 'com.apple.iTunes' and 'name' is a unique
271
307
def load(self, atoms, fileobj):
272
try: ilst = atoms["moov.udta.meta.ilst"]
309
ilst = atoms["moov.udta.meta.ilst"]
273
310
except KeyError, key:
274
311
raise MP4MetadataError(key)
275
312
for atom in ilst.children:
276
313
fileobj.seek(atom.offset + 8)
277
314
data = fileobj.read(atom.length - 8)
278
info = self.__atoms.get(atom.name, (type(self).__parse_text, None))
279
info[0](self, atom, data, *info[2:])
315
if len(data) != atom.length - 8:
316
raise MP4MetadataError("Not enough data")
318
if atom.name in self.__atoms:
319
info = self.__atoms[atom.name]
320
info[0](self, atom, data, *info[2:])
322
# unknown atom, try as text and skip if it fails
323
# FIXME: keep them somehow
325
self.__parse_text(atom, data)
326
except MP4MetadataError:
330
def _can_load(cls, atoms):
331
return "moov.udta.meta.ilst" in atoms
281
334
def __key_sort(item1, item2):
282
335
(key1, v1) = item1
283
336
(key2, v2) = item2
490
543
return Atom.render("----", mean + name + data)
492
545
def __parse_pair(self, atom, data):
493
self[atom.name] = [struct.unpack(">2H", data[2:6]) for
494
flags, data in self.__parse_data(atom, data)]
546
self[atom.name] = [struct.unpack(">2H", d[2:6]) for
547
flags, d in self.__parse_data(atom, data)]
495
549
def __render_pair(self, key, value):
497
551
for (track, total) in value:
558
618
if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG):
559
619
imageformat = MP4Cover.FORMAT_JPEG
560
620
cover = MP4Cover(data[pos+16:pos+length], imageformat)
561
self[atom.name].append(
562
MP4Cover(data[pos+16:pos+length], imageformat))
621
self[atom.name].append(cover)
564
624
def __render_cover(self, key, value):
566
626
for cover in value:
567
try: imageformat = cover.imageformat
568
except AttributeError: imageformat = MP4Cover.FORMAT_JPEG
570
Atom.render("data", struct.pack(">2I", imageformat, 0) + cover))
628
imageformat = cover.imageformat
629
except AttributeError:
630
imageformat = MP4Cover.FORMAT_JPEG
631
atom_data.append(Atom.render(
632
"data", struct.pack(">2I", imageformat, 0) + cover))
571
633
return Atom.render(key, "".join(atom_data))
573
635
def __parse_text(self, atom, data, expected_flags=1):
598
663
"covr": (__parse_cover, __render_cover),
599
664
"purl": (__parse_text, __render_text, 0),
600
665
"egid": (__parse_text, __render_text, 0),
668
# the text atoms we know about which should make loading fail if parsing
670
for name in ["\xa9nam", "\xa9alb", "\xa9ART", "aART", "\xa9wrt", "\xa9day",
671
"\xa9cmt", "desc", "purd", "\xa9grp", "\xa9gen", "\xa9lyr",
672
"catg", "keyw", "\xa9too", "cprt", "soal", "soaa", "soar",
673
"sonm", "soco", "sosn", "tvsh"]:
674
__atoms[name] = (__parse_text, __render_text)
603
676
def pprint(self):
613
686
values.append("%s=%s" % (key, value))
614
687
return "\n".join(values)
616
690
class MP4Info(object):
617
691
"""MPEG-4 stream information.
620
bitrate -- bitrate in bits per second, as an int
621
length -- file length in seconds, as a float
622
channels -- number of audio channels
623
sample_rate -- audio sampling rate in Hz
624
bits_per_sample -- bits per sample
695
* bitrate -- bitrate in bits per second, as an int
696
* length -- file length in seconds, as a float
697
* channels -- number of audio channels
698
* sample_rate -- audio sampling rate in Hz
699
* bits_per_sample -- bits per sample
687
762
return "MPEG-4 audio, %.2f seconds, %d bps" % (
688
763
self.length, self.bitrate)
690
766
class MP4(FileType):
691
767
"""An MPEG-4 audio file, probably containing AAC.
693
769
If more than one track is present in the file, the first is used.
694
770
Only audio ('soun') tracks will be read.
772
:ivar info: :class:`MP4Info`
773
:ivar tags: :class:`MP4Tags`
697
776
MP4Tags = MP4Tags
699
778
_mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
701
780
def load(self, filename):
703
782
fileobj = open(filename, "rb")
705
784
atoms = Atoms(fileobj)
706
try: self.info = MP4Info(atoms, fileobj)
786
# ftyp is always the first atom in a valid MP4 file
787
if not atoms.atoms or atoms.atoms[0].name != "ftyp":
788
raise error("Not a MP4 file")
791
self.info = MP4Info(atoms, fileobj)
707
792
except StandardError, err:
708
793
raise MP4StreamInfoError, err, sys.exc_info()[2]
709
try: self.tags = self.MP4Tags(atoms, fileobj)
710
except MP4MetadataError:
795
if not MP4Tags._can_load(atoms):
712
except StandardError, err:
713
raise MP4MetadataError, err, sys.exc_info()[2]
799
self.tags = self.MP4Tags(atoms, fileobj)
800
except StandardError, err:
801
raise MP4MetadataError, err, sys.exc_info()[2]
717
805
def add_tags(self):
718
self.tags = self.MP4Tags()
806
if self.tags is None:
807
self.tags = self.MP4Tags()
809
raise error("an MP4 tag already exists")
720
812
def score(filename, fileobj, header):
721
813
return ("ftyp" in header) + ("mp4" in header)
722
score = staticmethod(score)
726
819
def delete(filename):
727
820
"""Remove tags from a file."""
728
822
MP4(filename).delete()