~ubuntu-branches/ubuntu/utopic/mutagen/utopic-proposed

« back to all changes in this revision

Viewing changes to mutagen/mp4.py

  • Committer: Package Import Robot
  • Author(s): Daniel T Chen
  • Date: 2013-11-27 22:10:48 UTC
  • mfrom: (8.1.17 sid)
  • Revision ID: package-import@ubuntu.com-20131127221048-ae2f5j42ak2ox3kw
Tags: 1.22-1ubuntu1
* Merge from Debian unstable.  Remaining changes:
  - debian/control: Drop faad and oggz-tools build dependencies (in
    universe).

Show diffs side-by-side

added added

removed removed

Lines of Context:
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License version 2 as
5
5
# published by the Free Software Foundation.
6
 
#
7
 
# $Id: mp4.py 4233 2007-12-28 07:24:59Z luks $
8
6
 
9
7
"""Read and write MPEG-4 audio files with iTunes metadata.
10
8
 
13
11
 
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
20
 
consulted.
 
14
 
 
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
 
20
 
 
21
were all consulted.
21
22
"""
22
23
 
23
24
import struct
25
26
 
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
29
 
 
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
 
30
 
 
31
 
 
32
class error(IOError):
 
33
    pass
 
34
 
 
35
 
 
36
class MP4MetadataError(error):
 
37
    pass
 
38
 
 
39
 
 
40
class MP4StreamInfoError(error):
 
41
    pass
 
42
 
 
43
 
 
44
class MP4MetadataValueError(ValueError, MP4MetadataError):
 
45
    pass
 
46
 
34
47
 
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}
40
53
 
41
54
__all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MP4FreeForm']
42
55
 
 
56
 
43
57
class MP4Cover(str):
44
58
    """A cover artwork.
45
 
    
 
59
 
46
60
    Attributes:
47
 
    imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG)
 
61
 
 
62
    * imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG)
48
63
    """
49
64
    FORMAT_JPEG = 0x0D
50
65
    FORMAT_PNG = 0x0E
51
66
 
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)
 
69
 
 
70
    def __init__(self, data, imageformat=FORMAT_JPEG):
55
71
        self.imageformat = imageformat
56
 
        try: self.format
 
72
        try:
 
73
            self.format
57
74
        except AttributeError:
58
75
            self.format = imageformat
59
 
        return self
60
76
 
61
77
 
62
78
class MP4FreeForm(str):
63
79
    """A freeform value.
64
 
    
 
80
 
65
81
    Attributes:
66
 
    dataformat -- format of the data (either FORMAT_TEXT or FORMAT_DATA)
 
82
 
 
83
    * dataformat -- format of the data (either FORMAT_TEXT or FORMAT_DATA)
67
84
    """
 
85
 
68
86
    FORMAT_DATA = 0x0
69
87
    FORMAT_TEXT = 0x1
70
88
 
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)
 
91
 
 
92
    def __init__(self, data, dataformat=FORMAT_TEXT):
75
93
        self.dataformat = dataformat
76
 
        return self
77
94
 
78
95
 
79
96
class Atom(object):
95
112
        self.length, self.name = struct.unpack(">I4s", fileobj.read(8))
96
113
        if self.length == 1:
97
114
            self.length, = struct.unpack(">Q", fileobj.read(8))
 
115
            if self.length < 16:
 
116
                raise MP4MetadataError(
 
117
                    "64 bit atom length can only be 16 and higher")
98
118
        elif self.length == 0:
99
119
            if level != 0:
100
120
                raise MP4MetadataError(
116
136
        else:
117
137
            fileobj.seek(self.offset + self.length, 0)
118
138
 
 
139
    @staticmethod
119
140
    def render(name, data):
120
141
        """Render raw atom data."""
121
142
        # this raises OverflowError if Py_ssize_t can't handle the atom data
124
145
            return struct.pack(">I4s", size, name) + data
125
146
        else:
126
147
            return struct.pack(">I4sQ", 1, name, size + 8) + data
127
 
    render = staticmethod(render)
128
148
 
129
149
    def findall(self, name, recursive=False):
130
150
        """Recursively find all child atoms by specified name."""
149
169
            if child.name == remaining[0]:
150
170
                return child[remaining[1:]]
151
171
        else:
152
 
            raise KeyError, "%r not found" % remaining[0]
 
172
            raise KeyError("%r not found" % remaining[0])
153
173
 
154
174
    def __repr__(self):
155
175
        klass = self.__class__.__name__
162
182
            return "<%s name=%r length=%r offset=%r\n%s>" % (
163
183
                klass, self.name, self.length, self.offset, children)
164
184
 
 
185
 
165
186
class Atoms(object):
166
187
    """Root atoms in a given file.
167
188
 
170
191
 
171
192
    This structure should only be used internally by Mutagen.
172
193
    """
 
194
 
173
195
    def __init__(self, fileobj):
174
196
        self.atoms = []
175
197
        fileobj.seek(0, 2)
185
207
        list of three atoms, corresponding to the moov, udta, and meta
186
208
        atoms.
187
209
        """
 
210
 
188
211
        path = [self]
189
212
        for name in names:
190
 
            path.append(path[-1][name,])
 
213
            path.append(path[-1][name, ])
191
214
        return path[1:]
192
215
 
 
216
    def __contains__(self, names):
 
217
        try:
 
218
            self[names]
 
219
        except KeyError:
 
220
            return False
 
221
        return True
 
222
 
193
223
    def __getitem__(self, names):
194
224
        """Look up a child atom.
195
225
 
196
226
        'names' may be a list of atoms (['moov', 'udta']) or a string
197
227
        specifying the complete path ('moov.udta').
198
228
        """
 
229
 
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:]]
204
235
        else:
205
 
            raise KeyError, "%s not found" % names[0]
 
236
            raise KeyError("%s not found" % names[0])
206
237
 
207
238
    def __repr__(self):
208
239
        return "\n".join([repr(child) for child in self.atoms])
209
240
 
 
241
 
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.
212
244
 
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:
216
248
 
217
249
    Text values (multiple values per key are supported):
218
 
        '\xa9nam' -- track title
219
 
        '\xa9alb' -- album
220
 
        '\xa9ART' -- artist
221
 
        'aART' -- album artist
222
 
        '\xa9wrt' -- composer
223
 
        '\xa9day' -- year
224
 
        '\xa9cmt' -- comment
225
 
        'desc' -- description (usually used in podcasts)
226
 
        'purd' -- purchase date
227
 
        '\xa9grp' -- grouping
228
 
        '\xa9gen' -- genre
229
 
        '\xa9lyr' -- lyrics
230
 
        'purl' -- podcast URL
231
 
        'egid' -- podcast episode GUID
232
 
        'catg' -- podcast category
233
 
        'keyw' -- podcast keywords
234
 
        '\xa9too' -- encoded by
235
 
        'cprt' -- copyright
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
242
 
        'tvsh' -- show name
 
250
 
 
251
    * '\\xa9nam' -- track title
 
252
    * '\\xa9alb' -- album
 
253
    * '\\xa9ART' -- artist
 
254
    * 'aART' -- album artist
 
255
    * '\\xa9wrt' -- composer
 
256
    * '\\xa9day' -- year
 
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
243
276
 
244
277
    Boolean values:
245
 
        'cpil' -- part of a compilation
246
 
        'pgap' -- part of a gapless album
247
 
        'pcst' -- podcast (iTunes reads this only on import)
 
278
 
 
279
    * 'cpil' -- part of a compilation
 
280
    * 'pgap' -- part of a gapless album
 
281
    * 'pcst' -- podcast (iTunes reads this only on import)
248
282
 
249
283
    Tuples of ints (multiple values per key are supported):
250
 
        'trkn' -- track number, total tracks
251
 
        'disk' -- disc number, total discs
 
284
 
 
285
    * 'trkn' -- track number, total tracks
 
286
    * 'disk' -- disc number, total discs
252
287
 
253
288
    Others:
254
 
        'tmpo' -- tempo/BPM, 16 bit int
255
 
        'covr' -- cover artwork, list of MP4Cover objects (which are
256
 
                  tagged strs)
257
 
        'gnre' -- ID3v1 genre. Not supported, use '\xa9gen' instead.
 
289
 
 
290
    * 'tmpo' -- tempo/BPM, 16 bit int
 
291
    * 'covr' -- cover artwork, list of MP4Cover objects (which are
 
292
      tagged strs)
 
293
    * 'gnre' -- ID3v1 genre. Not supported, use '\\xa9gen' instead.
258
294
 
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
269
305
    """
270
306
 
271
307
    def load(self, atoms, fileobj):
272
 
        try: ilst = atoms["moov.udta.meta.ilst"]
 
308
        try:
 
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:])
280
 
 
 
315
            if len(data) != atom.length - 8:
 
316
                raise MP4MetadataError("Not enough data")
 
317
 
 
318
            if atom.name in self.__atoms:
 
319
                info = self.__atoms[atom.name]
 
320
                info[0](self, atom, data, *info[2:])
 
321
            else:
 
322
                # unknown atom, try as text and skip if it fails
 
323
                # FIXME: keep them somehow
 
324
                try:
 
325
                    self.__parse_text(atom, data)
 
326
                except MP4MetadataError:
 
327
                    continue
 
328
 
 
329
    @classmethod
 
330
    def _can_load(cls, atoms):
 
331
        return "moov.udta.meta.ilst" in atoms
 
332
 
 
333
    @staticmethod
281
334
    def __key_sort(item1, item2):
282
335
        (key1, v1) = item1
283
336
        (key2, v2) = item2
294
347
        # values, so we at least have something determinstic.
295
348
        return (cmp(order.get(key1[:4], last), order.get(key2[:4], last)) or
296
349
                cmp(len(v1), len(v2)) or cmp(v1, v2))
297
 
    __key_sort = staticmethod(__key_sort)
298
350
 
299
351
    def save(self, filename):
300
352
        """Save the metadata to the given filename."""
386
438
        for atom in path:
387
439
            fileobj.seek(atom.offset)
388
440
            size = cdata.uint_be(fileobj.read(4))
389
 
            if size == 1: # 64bit
 
441
            if size == 1:  # 64bit
390
442
                # skip name (4B) and read size (8B)
391
443
                size = cdata.ulonglong_be(fileobj.read(12)[4:])
392
444
                fileobj.seek(atom.offset + 8)
393
445
                fileobj.write(cdata.to_ulonglong_be(size + delta))
394
 
            else: # 32bit
 
446
            else:  # 32bit
395
447
                fileobj.seek(atom.offset)
396
448
                fileobj.write(cdata.to_uint_be(size + delta))
397
449
 
444
496
                    "unexpected atom %r inside %r" % (name, atom.name))
445
497
            yield flags, data[pos+16:pos+length]
446
498
            pos += length
 
499
 
447
500
    def __render_data(self, key, flags, value):
448
501
        return Atom.render(key, "".join([
449
502
            Atom.render("data", struct.pack(">2I", flags, 0) + data)
490
543
        return Atom.render("----", mean + name + data)
491
544
 
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)]
 
548
 
495
549
    def __render_pair(self, key, value):
496
550
        data = []
497
551
        for (track, total) in value:
516
570
        # Translate to a freeform genre.
517
571
        genre = cdata.short_be(data[16:18])
518
572
        if "\xa9gen" not in self:
519
 
            try: self["\xa9gen"] = [GENRES[genre - 1]]
520
 
            except IndexError: pass
 
573
            try:
 
574
                self["\xa9gen"] = [GENRES[genre - 1]]
 
575
            except IndexError:
 
576
                pass
521
577
 
522
578
    def __parse_tempo(self, atom, data):
523
579
        self[atom.name] = [cdata.ushort_be(value[1]) for
539
595
        return self.__render_data(key, 0x15, values)
540
596
 
541
597
    def __parse_bool(self, atom, data):
542
 
        try: self[atom.name] = bool(ord(data[16:17]))
543
 
        except TypeError: self[atom.name] = False
 
598
        try:
 
599
            self[atom.name] = bool(ord(data[16:17]))
 
600
        except TypeError:
 
601
            self[atom.name] = False
 
602
 
544
603
    def __render_bool(self, key, value):
545
604
        return self.__render_data(key, 0x15, [chr(bool(value))])
546
605
 
548
607
        self[atom.name] = []
549
608
        pos = 0
550
609
        while pos < atom.length - 8:
551
 
            length, name, imageformat = struct.unpack(">I4sI", data[pos:pos+12])
 
610
            length, name, imageformat = struct.unpack(">I4sI",
 
611
                                                      data[pos:pos+12])
552
612
            if name != "data":
553
613
                if name == "name":
554
614
                    pos += length
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)
563
622
            pos += length
 
623
 
564
624
    def __render_cover(self, key, value):
565
625
        atom_data = []
566
626
        for cover in value:
567
 
            try: imageformat = cover.imageformat
568
 
            except AttributeError: imageformat = MP4Cover.FORMAT_JPEG
569
 
            atom_data.append(
570
 
                Atom.render("data", struct.pack(">2I", imageformat, 0) + cover))
 
627
            try:
 
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))
572
634
 
573
635
    def __parse_text(self, atom, data, expected_flags=1):
576
638
                 if flags == expected_flags]
577
639
        if value:
578
640
            self[atom.name] = value
 
641
 
579
642
    def __render_text(self, key, value, flags=1):
580
643
        if isinstance(value, basestring):
581
644
            value = [value]
583
646
            key, flags, map(utf8, value))
584
647
 
585
648
    def delete(self, filename):
 
649
        """Remove the metadata from the given filename."""
 
650
 
586
651
        self.clear()
587
652
        self.save(filename)
588
653
 
598
663
        "covr": (__parse_cover, __render_cover),
599
664
        "purl": (__parse_text, __render_text, 0),
600
665
        "egid": (__parse_text, __render_text, 0),
601
 
        }
 
666
    }
 
667
 
 
668
    # the text atoms we know about which should make loading fail if parsing
 
669
    # any of them fails
 
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)
602
675
 
603
676
    def pprint(self):
604
677
        values = []
613
686
                values.append("%s=%s" % (key, value))
614
687
        return "\n".join(values)
615
688
 
 
689
 
616
690
class MP4Info(object):
617
691
    """MPEG-4 stream information.
618
692
 
619
693
    Attributes:
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
 
694
 
 
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
625
700
    """
626
701
 
627
702
    bitrate = 0
687
762
        return "MPEG-4 audio, %.2f seconds, %d bps" % (
688
763
            self.length, self.bitrate)
689
764
 
 
765
 
690
766
class MP4(FileType):
691
767
    """An MPEG-4 audio file, probably containing AAC.
692
768
 
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.
 
771
 
 
772
    :ivar info: :class:`MP4Info`
 
773
    :ivar tags: :class:`MP4Tags`
695
774
    """
696
775
 
697
776
    MP4Tags = MP4Tags
698
 
    
 
777
 
699
778
    _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
700
779
 
701
780
    def load(self, filename):
703
782
        fileobj = open(filename, "rb")
704
783
        try:
705
784
            atoms = Atoms(fileobj)
706
 
            try: self.info = MP4Info(atoms, fileobj)
 
785
 
 
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")
 
789
 
 
790
            try:
 
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:
 
794
 
 
795
            if not MP4Tags._can_load(atoms):
711
796
                self.tags = None
712
 
            except StandardError, err:
713
 
                raise MP4MetadataError, err, sys.exc_info()[2]
 
797
            else:
 
798
                try:
 
799
                    self.tags = self.MP4Tags(atoms, fileobj)
 
800
                except StandardError, err:
 
801
                    raise MP4MetadataError, err, sys.exc_info()[2]
714
802
        finally:
715
803
            fileobj.close()
716
804
 
717
805
    def add_tags(self):
718
 
        self.tags = self.MP4Tags()
 
806
        if self.tags is None:
 
807
            self.tags = self.MP4Tags()
 
808
        else:
 
809
            raise error("an MP4 tag already exists")
719
810
 
 
811
    @staticmethod
720
812
    def score(filename, fileobj, header):
721
813
        return ("ftyp" in header) + ("mp4" in header)
722
 
    score = staticmethod(score)
 
814
 
723
815
 
724
816
Open = MP4
725
817
 
 
818
 
726
819
def delete(filename):
727
820
    """Remove tags from a file."""
 
821
 
728
822
    MP4(filename).delete()