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
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']
47 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
55 self
.imageformat
= imageformat
57 except AttributeError:
58 self
.format
= imageformat
62 """An individual atom.
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.
75 def __init__(self
, fileobj
):
76 self
.offset
= fileobj
.tell()
77 self
.length
, self
.name
= struct
.unpack(">I4s", fileobj
.read(8))
79 self
.length
, = struct
.unpack(">Q", fileobj
.read(8))
83 if self
.name
in _CONTAINERS
:
85 fileobj
.seek(_SKIP_SIZE
.get(self
.name
, 0), 1)
86 while fileobj
.tell() < self
.offset
+ self
.length
:
87 self
.children
.append(Atom(fileobj
))
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
95 if size
<= 0xFFFFFFFF:
96 return struct
.pack(">I4s", size
, name
) + data
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
:
108 for atom
in child
.findall(name
, True):
111 def __getitem__(self
, remaining
):
112 """Look up a child atom, potentially recursively.
114 e.g. atom['udta', 'meta'] => <Atom name='meta' ...>
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:]]
124 raise KeyError, "%r not found" % remaining
[0]
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
)
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
)
138 """Root atoms in a given file.
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
):
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
162 path
.append(path
[-1][name
,])
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:]]
177 raise KeyError, "%s not found" % names
[0]
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
189 Text values (multiple values per key are supported):
190 '\xa9nam' -- track title
193 'aART' -- album artist
194 '\xa9wrt' -- composer
197 'desc' -- description (usually used in podcasts)
198 'purd' -- purchase date
199 '\xa9grp' -- grouping
202 'purl' -- podcast URL
203 'egid' -- podcast episode GUID
204 'catg' -- podcast category
205 'keyw' -- podcast keywords
206 '\xa9too' -- encoded by
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
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
226 'tmpo' -- tempo/BPM, 16 bit int
227 'covr' -- cover artwork, list of MP4Cover objects (which are
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
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
):
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
))))
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."""
276 items
.sort(self
.__key
_sort
)
277 for key
, value
in items
:
278 info
= self
.__atoms
.get(key
[:4], (None, type(self
).__render
_text
))
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+")
288 atoms
= Atoms(fileobj
)
290 path
= atoms
.path("moov", "udta", "meta", "ilst")
292 self
.__save
_new
(fileobj
, atoms
, data
)
294 self
.__save
_existing
(fileobj
, atoms
, path
, data
)
298 def __pad_ilst(self
, data
, length
=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)
306 "meta", "\x00\x00\x00\x00" + hdlr
+ ilst
+ self
.__pad
_ilst
(ilst
))
308 path
= atoms
.path("moov", "udta")
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
)
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.
326 # Check for padding "free" atoms
328 index
= meta
.children
.index(ilst
)
330 prev
= meta
.children
[index
-1]
331 if prev
.name
== "free":
333 length
+= prev
.length
337 next
= meta
.children
[index
+1]
338 if next
.name
== "free":
339 length
+= next
.length
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
)
349 data
+= self
.__pad
_ilst
(data
, -delta
- 8)
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."""
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
))
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
:
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
:
386 fileobj
.seek(atom
.offset
+ 9)
387 data
= fileobj
.read(atom
.length
- 9)
388 flags
= cdata
.uint_be("\x00" + data
[:3])
390 o
= cdata
.ulonglong_be(data
[7:15])
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."""
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
)
406 for atom
in atoms
["moof"].findall('tfhd', True):
407 self
.__update
_tfhd
(fileobj
, atom
, delta
, offset
)
411 def __parse_data(self
, atom
, data
):
413 while pos
< atom
.length
- 8:
414 length
, name
, flags
= struct
.unpack(">I4sI", data
[pos
:pos
+12])
416 raise MP4MetadataError(
417 "unexpected atom %r inside %r" % (name
, atom
.name
))
418 yield flags
, data
[pos
+16: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
)
425 def __parse_freeform(self
, atom
, data
):
426 length
= cdata
.uint_be(data
[:4])
427 mean
= data
[12:length
]
429 length
= cdata
.uint_be(data
[pos
:pos
+4])
430 name
= data
[pos
+12:pos
+length
]
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
])
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
):
448 return Atom
.render("----", mean
+ name
+ "".join([
449 struct
.pack(">I4s2I", len(data
) + 16, "data", 1, 0) + data
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
):
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))
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
):
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
))
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
):
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
)
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
):
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
)
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
):
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
)
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
):
548 while pos
< atom
.length
- 8:
549 length
, name
, imageformat
= struct
.unpack(">I4sI", data
[pos
:pos
+12])
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
))
562 def __render_cover(self
, key
, value
):
565 try: imageformat
= cover
.imageformat
566 except AttributeError: imageformat
= MP4Cover
.FORMAT_JPEG
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
]
576 self
[atom
.name
] = value
577 def __render_text(self
, key
, value
, flags
=1):
578 if isinstance(value
, basestring
):
580 return self
.__render
_data
(
581 key
, flags
, map(utf8
, value
))
583 def delete(self
, filename
):
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
),
608 for key
, value
in self
.iteritems():
609 key
= key
.decode('latin1')
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
))))
616 values
.append("%s=%s" % (key
, value
))
617 return "\n".join(values
)
619 class MP4Info(object):
620 """MPEG-4 stream information.
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
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":
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:
654 end
= offset
+ struct
.calcsize(fmt
)
655 unit
, length
= struct
.unpack(fmt
, data
[offset
:end
])
656 self
.length
= float(length
) / unit
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])
667 if data
[56:60] == "esds" and ord(data
[64:65]) == 0x03:
669 # skip extended descriptor type tag, length, ES ID
670 # and stream priority
671 if data
[pos
:pos
+3] == "\x80\x80\x80":
674 # decoder config descriptor type
675 if ord(data
[pos
]) == 0x04:
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":
684 self
.bitrate
= cdata
.uint_be(data
[pos
:pos
+4])
685 except (ValueError, KeyError):
686 # stsd atoms are optional
690 return "MPEG-4 audio, %.2f seconds, %d bps" % (
691 self
.length
, self
.bitrate
)
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.
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")
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
:
715 except StandardError, err
:
716 raise MP4MetadataError
, err
, sys
.exc_info()[2]
721 self
.tags
= self
.MP4Tags()
723 def score(filename
, fileobj
, header
):
724 return ("ftyp" in header
) + ("mp4" in header
)
725 score
= staticmethod(score
)
729 def delete(filename
):
730 """Remove tags from a file."""
731 MP4(filename
).delete()