Merge pull request #21 from geekmug/patch-1
[pyTivo/wmcbrine.git] / mutagen / mp4.py
blobca9e9d3a0189517c0834a13a1b790b4b218e4d9c
1 # Copyright 2006 Joe Wreschnig
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License version 2 as
5 # published by the Free Software Foundation.
7 # $Id: mp4.py 4233 2007-12-28 07:24:59Z luks $
9 """Read and write MPEG-4 audio files with iTunes metadata.
11 This module will read MPEG-4 audio information and metadata,
12 as found in Apple's MP4 (aka M4A, M4B, M4P) files.
14 There is no official specification for this format. The source code
15 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
20 consulted.
21 """
23 import struct
24 import sys
26 from mutagen import FileType, Metadata
27 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
35 # This is not an exhaustive list of container atoms, but just the
36 # ones this module needs to peek inside.
37 _CONTAINERS = ["moov", "udta", "trak", "mdia", "meta", "ilst",
38 "stbl", "minf", "moof", "traf"]
39 _SKIP_SIZE = { "meta": 4 }
41 __all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MediaKind', 'HDVideo', 'ContentRating']
43 class MP4Cover(str):
44 """A cover artwork.
46 Attributes:
47 imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG)
48 """
49 FORMAT_JPEG = 0x0D
50 FORMAT_PNG = 0x0E
52 def __new__(cls, data, imageformat=None):
53 self = str.__new__(cls, data)
54 if imageformat is None: imageformat = MP4Cover.FORMAT_JPEG
55 self.imageformat = imageformat
56 try: self.format
57 except AttributeError:
58 self.format = imageformat
59 return self
61 class Atom(object):
62 """An individual atom.
64 Attributes:
65 children -- list child atoms (or None for non-container atoms)
66 length -- length of this atom, including length and name
67 name -- four byte name of the atom, as a str
68 offset -- location in the constructor-given fileobj of this atom
70 This structure should only be used internally by Mutagen.
71 """
73 children = None
75 def __init__(self, fileobj):
76 self.offset = fileobj.tell()
77 self.length, self.name = struct.unpack(">I4s", fileobj.read(8))
78 if self.length == 1:
79 self.length, = struct.unpack(">Q", fileobj.read(8))
80 elif self.length < 8:
81 return
83 if self.name in _CONTAINERS:
84 self.children = []
85 fileobj.seek(_SKIP_SIZE.get(self.name, 0), 1)
86 while fileobj.tell() < self.offset + self.length:
87 self.children.append(Atom(fileobj))
88 else:
89 fileobj.seek(self.offset + self.length, 0)
91 def render(name, data):
92 """Render raw atom data."""
93 # this raises OverflowError if Py_ssize_t can't handle the atom data
94 size = len(data) + 8
95 if size <= 0xFFFFFFFF:
96 return struct.pack(">I4s", size, name) + data
97 else:
98 return struct.pack(">I4sQ", 1, name, size + 8) + data
99 render = staticmethod(render)
101 def findall(self, name, recursive=False):
102 """Recursively find all child atoms by specified name."""
103 if self.children is not None:
104 for child in self.children:
105 if child.name == name:
106 yield child
107 if recursive:
108 for atom in child.findall(name, True):
109 yield atom
111 def __getitem__(self, remaining):
112 """Look up a child atom, potentially recursively.
114 e.g. atom['udta', 'meta'] => <Atom name='meta' ...>
116 if not remaining:
117 return self
118 elif self.children is None:
119 raise KeyError("%r is not a container" % self.name)
120 for child in self.children:
121 if child.name == remaining[0]:
122 return child[remaining[1:]]
123 else:
124 raise KeyError, "%r not found" % remaining[0]
126 def __repr__(self):
127 klass = self.__class__.__name__
128 if self.children is None:
129 return "<%s name=%r length=%r offset=%r>" % (
130 klass, self.name, self.length, self.offset)
131 else:
132 children = "\n".join([" " + line for child in self.children
133 for line in repr(child).splitlines()])
134 return "<%s name=%r length=%r offset=%r\n%s>" % (
135 klass, self.name, self.length, self.offset, children)
137 class Atoms(object):
138 """Root atoms in a given file.
140 Attributes:
141 atoms -- a list of top-level atoms as Atom objects
143 This structure should only be used internally by Mutagen.
145 def __init__(self, fileobj):
146 self.atoms = []
147 fileobj.seek(0, 2)
148 end = fileobj.tell()
149 fileobj.seek(0)
150 while fileobj.tell() + 8 <= end:
151 self.atoms.append(Atom(fileobj))
153 def path(self, *names):
154 """Look up and return the complete path of an atom.
156 For example, atoms.path('moov', 'udta', 'meta') will return a
157 list of three atoms, corresponding to the moov, udta, and meta
158 atoms.
160 path = [self]
161 for name in names:
162 path.append(path[-1][name,])
163 return path[1:]
165 def __getitem__(self, names):
166 """Look up a child atom.
168 'names' may be a list of atoms (['moov', 'udta']) or a string
169 specifying the complete path ('moov.udta').
171 if isinstance(names, basestring):
172 names = names.split(".")
173 for child in self.atoms:
174 if child.name == names[0]:
175 return child[names[1:]]
176 else:
177 raise KeyError, "%s not found" % names[0]
179 def __repr__(self):
180 return "\n".join([repr(child) for child in self.atoms])
182 class MP4Tags(DictProxy, Metadata):
183 """Dictionary containing Apple iTunes metadata list key/values.
185 Keys are four byte identifiers, except for freeform ('----')
186 keys. Values are usually unicode strings, but some atoms have a
187 special structure:
189 Text values (multiple values per key are supported):
190 '\xa9nam' -- track title
191 '\xa9alb' -- album
192 '\xa9ART' -- artist
193 'aART' -- album artist
194 '\xa9wrt' -- composer
195 '\xa9day' -- year
196 '\xa9cmt' -- comment
197 'desc' -- description (usually used in podcasts)
198 'purd' -- purchase date
199 '\xa9grp' -- grouping
200 '\xa9gen' -- genre
201 '\xa9lyr' -- lyrics
202 'purl' -- podcast URL
203 'egid' -- podcast episode GUID
204 'catg' -- podcast category
205 'keyw' -- podcast keywords
206 '\xa9too' -- encoded by
207 'cprt' -- copyright
208 'soal' -- album sort order
209 'soaa' -- album artist sort order
210 'soar' -- artist sort order
211 'sonm' -- title sort order
212 'soco' -- composer sort order
213 'sosn' -- show sort order
214 'tvsh' -- show name
216 Boolean values:
217 'cpil' -- part of a compilation
218 'pgap' -- part of a gapless album
219 'pcst' -- podcast (iTunes reads this only on import)
221 Tuples of ints (multiple values per key are supported):
222 'trkn' -- track number, total tracks
223 'disk' -- disc number, total discs
225 Others:
226 'tmpo' -- tempo/BPM, 16 bit int
227 'covr' -- cover artwork, list of MP4Cover objects (which are
228 tagged strs)
229 'gnre' -- ID3v1 genre. Not supported, use '\xa9gen' instead.
231 The freeform '----' frames use a key in the format '----:mean:name'
232 where 'mean' is usually 'com.apple.iTunes' and 'name' is a unique
233 identifier for this frame. The value is a str, but is probably
234 text that can be decoded as UTF-8. Multiple values per key are
235 supported.
237 MP4 tag data cannot exist outside of the structure of an MP4 file,
238 so this class should not be manually instantiated.
240 Unknown non-text tags are removed.
243 def load(self, atoms, fileobj):
244 try: ilst = atoms["moov.udta.meta.ilst"]
245 except KeyError, key:
246 raise MP4MetadataError(key)
247 for atom in ilst.children:
248 fileobj.seek(atom.offset + 8)
249 data = fileobj.read(atom.length - 8)
250 info = self.__atoms.get(atom.name, (type(self).__parse_text, None))
251 info[0](self, atom, data, *info[2:])
253 def __key_sort(item1, item2):
254 (key1, v1) = item1
255 (key2, v2) = item2
256 # iTunes always writes the tags in order of "relevance", try
257 # to copy it as closely as possible.
258 order = ["\xa9nam", "\xa9ART", "\xa9wrt", "\xa9alb",
259 "\xa9gen", "gnre", "trkn", "disk",
260 "\xa9day", "cpil", "pgap", "pcst", "tmpo",
261 "\xa9too", "----", "covr", "\xa9lyr",
262 + "stik", "tvsh", "tven", "tvsn", "tves", "tvnn"]
263 order = dict(zip(order, range(len(order))))
264 last = len(order)
265 # If there's no key-based way to distinguish, order by length.
266 # If there's still no way, go by string comparison on the
267 # values, so we at least have something determinstic.
268 return (cmp(order.get(key1[:4], last), order.get(key2[:4], last)) or
269 cmp(len(v1), len(v2)) or cmp(v1, v2))
270 __key_sort = staticmethod(__key_sort)
272 def save(self, filename):
273 """Save the metadata to the given filename."""
274 values = []
275 items = self.items()
276 items.sort(self.__key_sort)
277 for key, value in items:
278 info = self.__atoms.get(key[:4], (None, type(self).__render_text))
279 try:
280 values.append(info[1](self, key, value, *info[2:]))
281 except (TypeError, ValueError), s:
282 raise MP4MetadataValueError, s, sys.exc_info()[2]
283 data = Atom.render("ilst", "".join(values))
285 # Find the old atoms.
286 fileobj = open(filename, "rb+")
287 try:
288 atoms = Atoms(fileobj)
289 try:
290 path = atoms.path("moov", "udta", "meta", "ilst")
291 except KeyError:
292 self.__save_new(fileobj, atoms, data)
293 else:
294 self.__save_existing(fileobj, atoms, path, data)
295 finally:
296 fileobj.close()
298 def __pad_ilst(self, data, length=None):
299 if length is None:
300 length = ((len(data) + 1023) & ~1023) - len(data)
301 return Atom.render("free", "\x00" * length)
303 def __save_new(self, fileobj, atoms, ilst):
304 hdlr = Atom.render("hdlr", "\x00" * 8 + "mdirappl" + "\x00" * 9)
305 meta = Atom.render(
306 "meta", "\x00\x00\x00\x00" + hdlr + ilst + self.__pad_ilst(ilst))
307 try:
308 path = atoms.path("moov", "udta")
309 except KeyError:
310 # moov.udta not found -- create one
311 path = atoms.path("moov")
312 meta = Atom.render("udta", meta)
313 offset = path[-1].offset + 8
314 insert_bytes(fileobj, len(meta), offset)
315 fileobj.seek(offset)
316 fileobj.write(meta)
317 self.__update_parents(fileobj, path, len(meta))
318 self.__update_offsets(fileobj, atoms, len(meta), offset)
320 def __save_existing(self, fileobj, atoms, path, data):
321 # Replace the old ilst atom.
322 ilst = path.pop()
323 offset = ilst.offset
324 length = ilst.length
326 # Check for padding "free" atoms
327 meta = path[-1]
328 index = meta.children.index(ilst)
329 try:
330 prev = meta.children[index-1]
331 if prev.name == "free":
332 offset = prev.offset
333 length += prev.length
334 except IndexError:
335 pass
336 try:
337 next = meta.children[index+1]
338 if next.name == "free":
339 length += next.length
340 except IndexError:
341 pass
343 delta = len(data) - length
344 if delta > 0 or (delta < 0 and delta > -8):
345 data += self.__pad_ilst(data)
346 delta = len(data) - length
347 insert_bytes(fileobj, delta, offset)
348 elif delta < 0:
349 data += self.__pad_ilst(data, -delta - 8)
350 delta = 0
352 fileobj.seek(offset)
353 fileobj.write(data)
354 self.__update_parents(fileobj, path, delta)
355 self.__update_offsets(fileobj, atoms, delta, offset)
357 def __update_parents(self, fileobj, path, delta):
358 """Update all parent atoms with the new size."""
359 for atom in path:
360 fileobj.seek(atom.offset)
361 size = cdata.uint_be(fileobj.read(4))
362 if size == 1: # 64bit
363 # skip name (4B) and read size (8B)
364 size = cdata.ulonglong_be(fileobj.read(12)[4:])
365 fileobj.seek(atom.offset + 8)
366 fileobj.write(cdata.to_ulonglong_be(size + delta))
367 else: # 32bit
368 fileobj.seek(atom.offset)
369 fileobj.write(cdata.to_uint_be(size + delta))
371 def __update_offset_table(self, fileobj, fmt, atom, delta, offset):
372 """Update offset table in the specified atom."""
373 if atom.offset > offset:
374 atom.offset += delta
375 fileobj.seek(atom.offset + 12)
376 data = fileobj.read(atom.length - 12)
377 fmt = fmt % cdata.uint_be(data[:4])
378 offsets = struct.unpack(fmt, data[4:])
379 offsets = [o + (0, delta)[offset < o] for o in offsets]
380 fileobj.seek(atom.offset + 16)
381 fileobj.write(struct.pack(fmt, *offsets))
383 def __update_tfhd(self, fileobj, atom, delta, offset):
384 if atom.offset > offset:
385 atom.offset += delta
386 fileobj.seek(atom.offset + 9)
387 data = fileobj.read(atom.length - 9)
388 flags = cdata.uint_be("\x00" + data[:3])
389 if flags & 1:
390 o = cdata.ulonglong_be(data[7:15])
391 if o > offset:
392 o += delta
393 fileobj.seek(atom.offset + 16)
394 fileobj.write(cdata.to_ulonglong_be(o))
396 def __update_offsets(self, fileobj, atoms, delta, offset):
397 """Update offset tables in all 'stco' and 'co64' atoms."""
398 if delta == 0:
399 return
400 moov = atoms["moov"]
401 for atom in moov.findall('stco', True):
402 self.__update_offset_table(fileobj, ">%dI", atom, delta, offset)
403 for atom in moov.findall('co64', True):
404 self.__update_offset_table(fileobj, ">%dQ", atom, delta, offset)
405 try:
406 for atom in atoms["moof"].findall('tfhd', True):
407 self.__update_tfhd(fileobj, atom, delta, offset)
408 except KeyError:
409 pass
411 def __parse_data(self, atom, data):
412 pos = 0
413 while pos < atom.length - 8:
414 length, name, flags = struct.unpack(">I4sI", data[pos:pos+12])
415 if name != "data":
416 raise MP4MetadataError(
417 "unexpected atom %r inside %r" % (name, atom.name))
418 yield flags, data[pos+16:pos+length]
419 pos += length
420 def __render_data(self, key, flags, value):
421 return Atom.render(key, "".join([
422 Atom.render("data", struct.pack(">2I", flags, 0) + data)
423 for data in value]))
425 def __parse_freeform(self, atom, data):
426 length = cdata.uint_be(data[:4])
427 mean = data[12:length]
428 pos = length
429 length = cdata.uint_be(data[pos:pos+4])
430 name = data[pos+12:pos+length]
431 pos += length
432 value = []
433 while pos < atom.length - 8:
434 length, atom_name = struct.unpack(">I4s", data[pos:pos+8])
435 if atom_name != "data":
436 raise MP4MetadataError(
437 "unexpected atom %r inside %r" % (atom_name, atom.name))
438 value.append(data[pos+16:pos+length])
439 pos += length
440 if value:
441 self["%s:%s:%s" % (atom.name, mean, name)] = value
442 def __render_freeform(self, key, value):
443 dummy, mean, name = key.split(":", 2)
444 mean = struct.pack(">I4sI", len(mean) + 12, "mean", 0) + mean
445 name = struct.pack(">I4sI", len(name) + 12, "name", 0) + name
446 if isinstance(value, basestring):
447 value = [value]
448 return Atom.render("----", mean + name + "".join([
449 struct.pack(">I4s2I", len(data) + 16, "data", 1, 0) + data
450 for data in value]))
452 def __parse_pair(self, atom, data):
453 self[atom.name] = [struct.unpack(">2H", data[2:6]) for
454 flags, data in self.__parse_data(atom, data)]
455 def __render_pair(self, key, value):
456 data = []
457 for (track, total) in value:
458 if 0 <= track < 1 << 16 and 0 <= total < 1 << 16:
459 data.append(struct.pack(">4H", 0, track, total, 0))
460 else:
461 raise MP4MetadataValueError(
462 "invalid numeric pair %r" % ((track, total),))
463 return self.__render_data(key, 0, data)
465 def __render_pair_no_trailing(self, key, value):
466 data = []
467 for (track, total) in value:
468 if 0 <= track < 1 << 16 and 0 <= total < 1 << 16:
469 data.append(struct.pack(">3H", 0, track, total))
470 else:
471 raise MP4MetadataValueError(
472 "invalid numeric pair %r" % ((track, total),))
473 return self.__render_data(key, 0, data)
475 def __parse_genre(self, atom, data):
476 # Translate to a freeform genre.
477 genre = cdata.short_be(data[16:18])
478 if "\xa9gen" not in self:
479 try: self["\xa9gen"] = [GENRES[genre - 1]]
480 except IndexError: pass
482 def __parse_tempo(self, atom, data):
483 self[atom.name] = [cdata.ushort_be(value[1]) for
484 value in self.__parse_data(atom, data)]
486 def __render_tempo(self, key, value):
487 try:
488 if len(value) == 0:
489 return self.__render_data(key, 0x15, "")
491 if min(value) < 0 or max(value) >= 2**16:
492 raise MP4MetadataValueError(
493 "invalid 16 bit integers: %r" % value)
494 except TypeError:
495 raise MP4MetadataValueError(
496 "tmpo must be a list of 16 bit integers")
498 values = map(cdata.to_ushort_be, value)
499 return self.__render_data(key, 0x15, values)
501 def __parse_8int(self, atom, data):
502 self[atom.name] = [cdata.uchar_be(value[1]) for
503 value in self.__parse_data(atom, data)]
505 def __render_8int(self, key, value):
506 try:
507 if len(value) == 0:
508 return self.__render_data(key, 0x07, b"")
510 if min(value) < 0 or max(value) >= 2 ** 8:
511 raise MP4MetadataValueError(
512 "invalid 8 bit integers: %r" % value)
513 except TypeError:
514 raise MP4MetadataValueError(
515 "%s must be a list of 8 bit integers" % (key))
517 values = list(map(cdata.to_uchar_be, value))
518 return self.__render_data(key, 0x07, values)
520 def __parse_32int(self, atom, data):
521 self[atom.name] = [cdata.uint_be(value[1]) for
522 value in self.__parse_data(atom, data)]
524 def __render_32int(self, key, value):
525 try:
526 if len(value) == 0:
527 return self.__render_data(key, 0x31, b"")
529 if min(value) < 0 or max(value) >= 2 ** 32:
530 raise MP4MetadataValueError(
531 "invalid 32 bit integers: %r" % value)
532 except TypeError:
533 raise MP4MetadataValueError(
534 "%s must be a list of 32 bit integers" % (key))
536 values = list(map(cdata.to_uint_be, value))
537 return self.__render_data(key, 0x31, values)
539 def __parse_bool(self, atom, data):
540 try: self[atom.name] = bool(ord(data[16:17]))
541 except TypeError: self[atom.name] = False
542 def __render_bool(self, key, value):
543 return self.__render_data(key, 0x15, [chr(bool(value))])
545 def __parse_cover(self, atom, data):
546 self[atom.name] = []
547 pos = 0
548 while pos < atom.length - 8:
549 length, name, imageformat = struct.unpack(">I4sI", data[pos:pos+12])
550 if name != "data":
551 if name == "name":
552 pos += length
553 continue
554 raise MP4MetadataError(
555 "unexpected atom %r inside 'covr'" % name)
556 if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG):
557 imageformat = MP4Cover.FORMAT_JPEG
558 cover = MP4Cover(data[pos+16:pos+length], imageformat)
559 self[atom.name].append(
560 MP4Cover(data[pos+16:pos+length], imageformat))
561 pos += length
562 def __render_cover(self, key, value):
563 atom_data = []
564 for cover in value:
565 try: imageformat = cover.imageformat
566 except AttributeError: imageformat = MP4Cover.FORMAT_JPEG
567 atom_data.append(
568 Atom.render("data", struct.pack(">2I", imageformat, 0) + cover))
569 return Atom.render(key, "".join(atom_data))
571 def __parse_text(self, atom, data, expected_flags=1):
572 value = [text.decode('utf-8', 'replace') for flags, text
573 in self.__parse_data(atom, data)
574 if flags == expected_flags]
575 if value:
576 self[atom.name] = value
577 def __render_text(self, key, value, flags=1):
578 if isinstance(value, basestring):
579 value = [value]
580 return self.__render_data(
581 key, flags, map(utf8, value))
583 def delete(self, filename):
584 self.clear()
585 self.save(filename)
587 __atoms = {
588 "----": (__parse_freeform, __render_freeform),
589 "trkn": (__parse_pair, __render_pair),
590 "disk": (__parse_pair, __render_pair_no_trailing),
591 "gnre": (__parse_genre, None),
592 "tmpo": (__parse_tempo, __render_tempo),
593 "cpil": (__parse_bool, __render_bool),
594 "pgap": (__parse_bool, __render_bool),
595 "pcst": (__parse_bool, __render_bool),
596 "covr": (__parse_cover, __render_cover),
597 "purl": (__parse_text, __render_text, 0),
598 "egid": (__parse_text, __render_text, 0),
599 "tvsn": (__parse_32int, __render_32int),
600 "tves": (__parse_32int, __render_32int),
601 "stik": (__parse_8int, __render_8int),
602 "hdvd": (__parse_8int, __render_8int),
603 "rtng": (__parse_8int, __render_8int),
606 def pprint(self):
607 values = []
608 for key, value in self.iteritems():
609 key = key.decode('latin1')
610 if key == "covr":
611 values.append("%s=%s" % (key, ", ".join(
612 ["[%d bytes of data]" % len(data) for data in value])))
613 elif isinstance(value, list):
614 values.append("%s=%s" % (key, " / ".join(map(unicode, value))))
615 else:
616 values.append("%s=%s" % (key, value))
617 return "\n".join(values)
619 class MP4Info(object):
620 """MPEG-4 stream information.
622 Attributes:
623 bitrate -- bitrate in bits per second, as an int
624 length -- file length in seconds, as a float
625 channels -- number of audio channels
626 sample_rate -- audio sampling rate in Hz
627 bits_per_sample -- bits per sample
630 bitrate = 0
631 channels = 0
632 sample_rate = 0
633 bits_per_sample = 0
635 def __init__(self, atoms, fileobj):
636 for trak in list(atoms["moov"].findall("trak")):
637 hdlr = trak["mdia", "hdlr"]
638 fileobj.seek(hdlr.offset)
639 data = fileobj.read(hdlr.length)
640 if data[16:20] == "soun":
641 break
642 else:
643 raise MP4StreamInfoError("track has no audio data")
645 mdhd = trak["mdia", "mdhd"]
646 fileobj.seek(mdhd.offset)
647 data = fileobj.read(mdhd.length)
648 if ord(data[8]) == 0:
649 offset = 20
650 fmt = ">2I"
651 else:
652 offset = 28
653 fmt = ">IQ"
654 end = offset + struct.calcsize(fmt)
655 unit, length = struct.unpack(fmt, data[offset:end])
656 self.length = float(length) / unit
658 try:
659 atom = trak["mdia", "minf", "stbl", "stsd"]
660 fileobj.seek(atom.offset)
661 data = fileobj.read(atom.length)
662 if data[20:24] == "mp4a":
663 length = cdata.uint_be(data[16:20])
664 (self.channels, self.bits_per_sample, _,
665 self.sample_rate) = struct.unpack(">3HI", data[40:50])
666 # ES descriptor type
667 if data[56:60] == "esds" and ord(data[64:65]) == 0x03:
668 pos = 65
669 # skip extended descriptor type tag, length, ES ID
670 # and stream priority
671 if data[pos:pos+3] == "\x80\x80\x80":
672 pos += 3
673 pos += 4
674 # decoder config descriptor type
675 if ord(data[pos]) == 0x04:
676 pos += 1
677 # skip extended descriptor type tag, length,
678 # object type ID, stream type, buffer size
679 # and maximum bitrate
680 if data[pos:pos+3] == "\x80\x80\x80":
681 pos += 3
682 pos += 10
683 # average bitrate
684 self.bitrate = cdata.uint_be(data[pos:pos+4])
685 except (ValueError, KeyError):
686 # stsd atoms are optional
687 pass
689 def pprint(self):
690 return "MPEG-4 audio, %.2f seconds, %d bps" % (
691 self.length, self.bitrate)
693 class MP4(FileType):
694 """An MPEG-4 audio file, probably containing AAC.
696 If more than one track is present in the file, the first is used.
697 Only audio ('soun') tracks will be read.
700 MP4Tags = MP4Tags
702 _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
704 def load(self, filename):
705 self.filename = filename
706 fileobj = open(filename, "rb")
707 try:
708 atoms = Atoms(fileobj)
709 try: self.info = MP4Info(atoms, fileobj)
710 except StandardError, err:
711 raise MP4StreamInfoError, err, sys.exc_info()[2]
712 try: self.tags = self.MP4Tags(atoms, fileobj)
713 except MP4MetadataError:
714 self.tags = None
715 except StandardError, err:
716 raise MP4MetadataError, err, sys.exc_info()[2]
717 finally:
718 fileobj.close()
720 def add_tags(self):
721 self.tags = self.MP4Tags()
723 def score(filename, fileobj, header):
724 return ("ftyp" in header) + ("mp4" in header)
725 score = staticmethod(score)
727 Open = MP4
729 def delete(filename):
730 """Remove tags from a file."""
731 MP4(filename).delete()
734 class MediaKind:
735 MUSIC = [1]
736 AUDIO_BOOK = [2]
737 MUSIC_VIDEO = [6]
738 MOVIE = [9]
739 TV_SHOW = [10]
740 BOOKLET = [11]
741 RINGTONE = [14]
743 class HDVideo:
744 STANDARD = [0]
745 P720 = [1]
746 P1080 = [2]
748 class ContentRating:
749 NONE = [0]
750 CLEAN = [2]
751 EXPLICIT = [4]