1# -*- coding: utf-8 -*-
2# Copyright (C) 2005  Michael Urman
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import zlib
10from struct import unpack
11
12from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch, \
13    ID3SaveConfig, error
14from ._specs import BinaryDataSpec, StringSpec, Latin1TextSpec, \
15    EncodedTextSpec, ByteSpec, EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, \
16    IntegerSpec, Encoding, VolumeAdjustmentsSpec, VolumePeakSpec, \
17    VolumeAdjustmentSpec, ChannelSpec, MultiSpec, SynchronizedTextSpec, \
18    KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \
19    EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \
20    Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec
21from .._compat import text_type, string_types, swap_to_string, iteritems, \
22    izip, itervalues
23
24
25def _bytes2key(b):
26    assert isinstance(b, bytes)
27
28    return b.decode("latin1")
29
30
31class Frame(object):
32    """Fundamental unit of ID3 data.
33
34    ID3 tags are split into frames. Each frame has a potentially
35    different structure, and so this base class is not very featureful.
36    """
37
38    FLAG23_ALTERTAG = 0x8000
39    FLAG23_ALTERFILE = 0x4000
40    FLAG23_READONLY = 0x2000
41    FLAG23_COMPRESS = 0x0080
42    FLAG23_ENCRYPT = 0x0040
43    FLAG23_GROUP = 0x0020
44
45    FLAG24_ALTERTAG = 0x4000
46    FLAG24_ALTERFILE = 0x2000
47    FLAG24_READONLY = 0x1000
48    FLAG24_GROUPID = 0x0040
49    FLAG24_COMPRESS = 0x0008
50    FLAG24_ENCRYPT = 0x0004
51    FLAG24_UNSYNCH = 0x0002
52    FLAG24_DATALEN = 0x0001
53
54    _framespec = []
55    _optionalspec = []
56
57    def __init__(self, *args, **kwargs):
58        if len(args) == 1 and len(kwargs) == 0 and \
59                isinstance(args[0], type(self)):
60            other = args[0]
61            # ask the sub class to fill in our data
62            other._to_other(self)
63        else:
64            for checker, val in izip(self._framespec, args):
65                setattr(self, checker.name, val)
66            for checker in self._framespec[len(args):]:
67                setattr(self, checker.name,
68                        kwargs.get(checker.name, checker.default))
69            for spec in self._optionalspec:
70                if spec.name in kwargs:
71                    setattr(self, spec.name, kwargs[spec.name])
72                else:
73                    break
74
75    def __setattr__(self, name, value):
76        for checker in self._framespec:
77            if checker.name == name:
78                self._setattr(name, checker.validate(self, value))
79                return
80        for checker in self._optionalspec:
81            if checker.name == name:
82                self._setattr(name, checker.validate(self, value))
83                return
84        super(Frame, self).__setattr__(name, value)
85
86    def _setattr(self, name, value):
87        self.__dict__[name] = value
88
89    def _to_other(self, other):
90        # this impl covers subclasses with the same framespec
91        if other._framespec is not self._framespec:
92            raise ValueError
93
94        for checker in other._framespec:
95            other._setattr(checker.name, getattr(self, checker.name))
96
97        # this impl covers subclasses with the same optionalspec
98        if other._optionalspec is not self._optionalspec:
99            raise ValueError
100
101        for checker in other._optionalspec:
102            if hasattr(self, checker.name):
103                other._setattr(checker.name, getattr(self, checker.name))
104
105    def _merge_frame(self, other):
106        # default impl, use the new tag over the old one
107        return other
108
109    def _upgrade_frame(self):
110        """Returns either this instance or a new instance if this is a v2.2
111        frame and an upgrade to a v2.3/4 equivalent is viable.
112
113        If this is a v2.2 instance and there is no upgrade path, returns None.
114        """
115
116        # turn 2.2 into 2.3/2.4 tags
117        if len(type(self).__name__) == 3:
118            base = type(self).__base__
119            if base is Frame:
120                return
121            return base(self)
122        else:
123            return self
124
125    def _get_v23_frame(self, **kwargs):
126        """Returns a frame copy which is suitable for writing into a v2.3 tag.
127
128        kwargs get passed to the specs.
129        """
130
131        new_kwargs = {}
132        for checker in self._framespec:
133            name = checker.name
134            value = getattr(self, name)
135            new_kwargs[name] = checker._validate23(self, value, **kwargs)
136
137        for checker in self._optionalspec:
138            name = checker.name
139            if hasattr(self, name):
140                value = getattr(self, name)
141                new_kwargs[name] = checker._validate23(self, value, **kwargs)
142
143        return type(self)(**new_kwargs)
144
145    @property
146    def HashKey(self):
147        """An internal key used to ensure frame uniqueness in a tag"""
148
149        return self.FrameID
150
151    @property
152    def FrameID(self):
153        """ID3v2 three or four character frame ID"""
154
155        return type(self).__name__
156
157    def __repr__(self):
158        """Python representation of a frame.
159
160        The string returned is a valid Python expression to construct
161        a copy of this frame.
162        """
163        kw = []
164        for attr in self._framespec:
165            # so repr works during __init__
166            if hasattr(self, attr.name):
167                kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
168        for attr in self._optionalspec:
169            if hasattr(self, attr.name):
170                kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
171        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
172
173    def _readData(self, id3, data):
174        """Raises ID3JunkFrameError; Returns leftover data"""
175
176        for reader in self._framespec:
177            if len(data) or reader.handle_nodata:
178                try:
179                    value, data = reader.read(id3, self, data)
180                except SpecError as e:
181                    raise ID3JunkFrameError(e)
182            else:
183                raise ID3JunkFrameError("no data left")
184            self._setattr(reader.name, value)
185
186        for reader in self._optionalspec:
187            if len(data) or reader.handle_nodata:
188                try:
189                    value, data = reader.read(id3, self, data)
190                except SpecError as e:
191                    raise ID3JunkFrameError(e)
192            else:
193                break
194            self._setattr(reader.name, value)
195
196        return data
197
198    def _writeData(self, config=None):
199        """Raises error"""
200
201        if config is None:
202            config = ID3SaveConfig()
203
204        if config.v2_version == 3:
205            frame = self._get_v23_frame(sep=config.v23_separator)
206        else:
207            frame = self
208
209        data = []
210        for writer in self._framespec:
211            try:
212                data.append(
213                    writer.write(config, frame, getattr(frame, writer.name)))
214            except SpecError as e:
215                raise error(e)
216
217        for writer in self._optionalspec:
218            try:
219                data.append(
220                    writer.write(config, frame, getattr(frame, writer.name)))
221            except AttributeError:
222                break
223            except SpecError as e:
224                raise error(e)
225
226        return b''.join(data)
227
228    def pprint(self):
229        """Return a human-readable representation of the frame."""
230        return "%s=%s" % (type(self).__name__, self._pprint())
231
232    def _pprint(self):
233        return "[unrepresentable data]"
234
235    @classmethod
236    def _fromData(cls, header, tflags, data):
237        """Construct this ID3 frame from raw string data.
238
239        Raises:
240
241        ID3JunkFrameError in case parsing failed
242        NotImplementedError in case parsing isn't implemented
243        ID3EncryptionUnsupportedError in case the frame is encrypted.
244        """
245
246        if header.version >= header._V24:
247            if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN):
248                # The data length int is syncsafe in 2.4 (but not 2.3).
249                # However, we don't actually need the data length int,
250                # except to work around a QL 0.12 bug, and in that case
251                # all we need are the raw bytes.
252                datalen_bytes = data[:4]
253                data = data[4:]
254            if tflags & Frame.FLAG24_UNSYNCH or header.f_unsynch:
255                try:
256                    data = unsynch.decode(data)
257                except ValueError:
258                    # Some things write synch-unsafe data with either the frame
259                    # or global unsynch flag set. Try to load them as is.
260                    # https://github.com/quodlibet/mutagen/issues/210
261                    # https://github.com/quodlibet/mutagen/issues/223
262                    pass
263            if tflags & Frame.FLAG24_ENCRYPT:
264                raise ID3EncryptionUnsupportedError
265            if tflags & Frame.FLAG24_COMPRESS:
266                try:
267                    data = zlib.decompress(data)
268                except zlib.error:
269                    # the initial mutagen that went out with QL 0.12 did not
270                    # write the 4 bytes of uncompressed size. Compensate.
271                    data = datalen_bytes + data
272                    try:
273                        data = zlib.decompress(data)
274                    except zlib.error as err:
275                        raise ID3JunkFrameError(
276                            'zlib: %s: %r' % (err, data))
277
278        elif header.version >= header._V23:
279            if tflags & Frame.FLAG23_COMPRESS:
280                usize, = unpack('>L', data[:4])
281                data = data[4:]
282            if tflags & Frame.FLAG23_ENCRYPT:
283                raise ID3EncryptionUnsupportedError
284            if tflags & Frame.FLAG23_COMPRESS:
285                try:
286                    data = zlib.decompress(data)
287                except zlib.error as err:
288                    raise ID3JunkFrameError('zlib: %s: %r' % (err, data))
289
290        frame = cls()
291        frame._readData(header, data)
292        return frame
293
294    def __hash__(self):
295        raise TypeError("Frame objects are unhashable")
296
297
298class CHAP(Frame):
299    """Chapter"""
300
301    _framespec = [
302        Latin1TextSpec("element_id"),
303        SizedIntegerSpec("start_time", 4, default=0),
304        SizedIntegerSpec("end_time", 4, default=0),
305        SizedIntegerSpec("start_offset", 4, default=0xffffffff),
306        SizedIntegerSpec("end_offset", 4, default=0xffffffff),
307        ID3FramesSpec("sub_frames"),
308    ]
309
310    @property
311    def HashKey(self):
312        return '%s:%s' % (self.FrameID, self.element_id)
313
314    def __eq__(self, other):
315        if not isinstance(other, CHAP):
316            return False
317
318        self_frames = self.sub_frames or {}
319        other_frames = other.sub_frames or {}
320        if sorted(self_frames.values()) != sorted(other_frames.values()):
321            return False
322
323        return self.element_id == other.element_id and \
324            self.start_time == other.start_time and \
325            self.end_time == other.end_time and \
326            self.start_offset == other.start_offset and \
327            self.end_offset == other.end_offset
328
329    __hash__ = Frame.__hash__
330
331    def _pprint(self):
332        frame_pprint = u""
333        for frame in itervalues(self.sub_frames):
334            for line in frame.pprint().splitlines():
335                frame_pprint += "\n" + " " * 4 + line
336        return u"%s time=%d..%d offset=%d..%d%s" % (
337            self.element_id, self.start_time, self.end_time,
338            self.start_offset, self.end_offset, frame_pprint)
339
340
341class CTOC(Frame):
342    """Table of contents"""
343
344    _framespec = [
345        Latin1TextSpec("element_id"),
346        CTOCFlagsSpec("flags", default=0),
347        Latin1TextListSpec("child_element_ids"),
348        ID3FramesSpec("sub_frames"),
349    ]
350
351    @property
352    def HashKey(self):
353        return '%s:%s' % (self.FrameID, self.element_id)
354
355    __hash__ = Frame.__hash__
356
357    def __eq__(self, other):
358        if not isinstance(other, CTOC):
359            return False
360
361        self_frames = self.sub_frames or {}
362        other_frames = other.sub_frames or {}
363        if sorted(self_frames.values()) != sorted(other_frames.values()):
364            return False
365
366        return self.element_id == other.element_id and \
367            self.flags == other.flags and \
368            self.child_element_ids == other.child_element_ids
369
370    def _pprint(self):
371        frame_pprint = u""
372        if getattr(self, "sub_frames", None):
373            frame_pprint += "\n" + "\n".join(
374                [" " * 4 + f.pprint() for f in self.sub_frames.values()])
375        return u"%s flags=%d child_element_ids=%s%s" % (
376            self.element_id, int(self.flags),
377            u",".join(self.child_element_ids), frame_pprint)
378
379
380@swap_to_string
381class TextFrame(Frame):
382    """Text strings.
383
384    Text frames support casts to unicode or str objects, as well as
385    list-like indexing, extend, and append.
386
387    Iterating over a TextFrame iterates over its strings, not its
388    characters.
389
390    Text frames have a 'text' attribute which is the list of strings,
391    and an 'encoding' attribute; 0 for ISO-8859 1, 1 UTF-16, 2 for
392    UTF-16BE, and 3 for UTF-8. If you don't want to worry about
393    encodings, just set it to 3.
394    """
395
396    _framespec = [
397        EncodingSpec('encoding', default=Encoding.UTF16),
398        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
399    ]
400
401    def __bytes__(self):
402        return text_type(self).encode('utf-8')
403
404    def __str__(self):
405        return u'\u0000'.join(self.text)
406
407    def __eq__(self, other):
408        if isinstance(other, bytes):
409            return bytes(self) == other
410        elif isinstance(other, text_type):
411            return text_type(self) == other
412        return self.text == other
413
414    __hash__ = Frame.__hash__
415
416    def __getitem__(self, item):
417        return self.text[item]
418
419    def __iter__(self):
420        return iter(self.text)
421
422    def append(self, value):
423        """Append a string."""
424
425        return self.text.append(value)
426
427    def extend(self, value):
428        """Extend the list by appending all strings from the given list."""
429
430        return self.text.extend(value)
431
432    def _merge_frame(self, other):
433        # merge in new values
434        for val in other[:]:
435            if val not in self:
436                self.append(val)
437                self.encoding = max(self.encoding, other.encoding)
438        return self
439
440    def _pprint(self):
441        return " / ".join(self.text)
442
443
444class NumericTextFrame(TextFrame):
445    """Numerical text strings.
446
447    The numeric value of these frames can be gotten with unary plus, e.g.::
448
449        frame = TLEN('12345')
450        length = +frame
451    """
452
453    _framespec = [
454        EncodingSpec('encoding', default=Encoding.UTF16),
455        MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000',
456                  default=[]),
457    ]
458
459    def __pos__(self):
460        """Return the numerical value of the string."""
461        return int(self.text[0])
462
463
464class NumericPartTextFrame(TextFrame):
465    """Multivalue numerical text strings.
466
467    These strings indicate 'part (e.g. track) X of Y', and unary plus
468    returns the first value::
469
470        frame = TRCK('4/15')
471        track = +frame # track == 4
472    """
473
474    _framespec = [
475        EncodingSpec('encoding', default=Encoding.UTF16),
476        MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000',
477                  default=[]),
478    ]
479
480    def __pos__(self):
481        return int(self.text[0].split("/")[0])
482
483
484@swap_to_string
485class TimeStampTextFrame(TextFrame):
486    """A list of time stamps.
487
488    The 'text' attribute in this frame is a list of ID3TimeStamp
489    objects, not a list of strings.
490    """
491
492    _framespec = [
493        EncodingSpec('encoding', default=Encoding.UTF16),
494        MultiSpec('text', TimeStampSpec('stamp'), sep=u',', default=[]),
495    ]
496
497    def __bytes__(self):
498        return text_type(self).encode('utf-8')
499
500    def __str__(self):
501        return u','.join([stamp.text for stamp in self.text])
502
503    def _pprint(self):
504        return u" / ".join([stamp.text for stamp in self.text])
505
506
507@swap_to_string
508class UrlFrame(Frame):
509    """A frame containing a URL string.
510
511    The ID3 specification is silent about IRIs and normalized URL
512    forms. Mutagen assumes all URLs in files are encoded as Latin 1,
513    but string conversion of this frame returns a UTF-8 representation
514    for compatibility with other string conversions.
515
516    The only sane way to handle URLs in MP3s is to restrict them to
517    ASCII.
518    """
519
520    _framespec = [
521        Latin1TextSpec('url'),
522    ]
523
524    def __bytes__(self):
525        return self.url.encode('utf-8')
526
527    def __str__(self):
528        return self.url
529
530    def __eq__(self, other):
531        return self.url == other
532
533    __hash__ = Frame.__hash__
534
535    def _pprint(self):
536        return self.url
537
538
539class UrlFrameU(UrlFrame):
540
541    @property
542    def HashKey(self):
543        return '%s:%s' % (self.FrameID, self.url)
544
545
546class TALB(TextFrame):
547    "Album"
548
549
550class TBPM(NumericTextFrame):
551    "Beats per minute"
552
553
554class TCOM(TextFrame):
555    "Composer"
556
557
558class TCON(TextFrame):
559    """Content type (Genre)
560
561    ID3 has several ways genres can be represented; for convenience,
562    use the 'genres' property rather than the 'text' attribute.
563    """
564
565    from mutagen._constants import GENRES
566    GENRES = GENRES
567
568    def __get_genres(self):
569        genres = []
570        import re
571        genre_re = re.compile(r"((?:\((?P<id>[0-9]+|RX|CR)\))*)(?P<str>.+)?")
572        for value in self.text:
573            # 255 possible entries in id3v1
574            if value.isdigit() and int(value) < 256:
575                try:
576                    genres.append(self.GENRES[int(value)])
577                except IndexError:
578                    genres.append(u"Unknown")
579            elif value == "CR":
580                genres.append(u"Cover")
581            elif value == "RX":
582                genres.append(u"Remix")
583            elif value:
584                newgenres = []
585                genreid, dummy, genrename = genre_re.match(value).groups()
586
587                if genreid:
588                    for gid in genreid[1:-1].split(")("):
589                        if gid.isdigit() and int(gid) < len(self.GENRES):
590                            gid = text_type(self.GENRES[int(gid)])
591                            newgenres.append(gid)
592                        elif gid == "CR":
593                            newgenres.append(u"Cover")
594                        elif gid == "RX":
595                            newgenres.append(u"Remix")
596                        else:
597                            newgenres.append(u"Unknown")
598
599                if genrename:
600                    # "Unescaping" the first parenthesis
601                    if genrename.startswith("(("):
602                        genrename = genrename[1:]
603                    if genrename not in newgenres:
604                        newgenres.append(genrename)
605
606                genres.extend(newgenres)
607
608        return genres
609
610    def __set_genres(self, genres):
611        if isinstance(genres, string_types):
612            genres = [genres]
613        self.text = [self.__decode(g) for g in genres]
614
615    def __decode(self, value):
616        if isinstance(value, bytes):
617            enc = EncodedTextSpec._encodings[self.encoding][0]
618            return value.decode(enc)
619        else:
620            return value
621
622    genres = property(__get_genres, __set_genres, None,
623                      "A list of genres parsed from the raw text data.")
624
625    def _pprint(self):
626        return " / ".join(self.genres)
627
628
629class TCOP(TextFrame):
630    "Copyright (c)"
631
632
633class TCMP(NumericTextFrame):
634    "iTunes Compilation Flag"
635
636
637class TDAT(TextFrame):
638    "Date of recording (DDMM)"
639
640
641class TDEN(TimeStampTextFrame):
642    "Encoding Time"
643
644
645class TDES(TextFrame):
646    "iTunes Podcast Description"
647
648
649class TKWD(TextFrame):
650    "iTunes Podcast Keywords"
651
652
653class TCAT(TextFrame):
654    "iTunes Podcast Category"
655
656
657class MVNM(TextFrame):
658    "iTunes Movement Name"
659
660
661class MVN(MVNM):
662    "iTunes Movement Name"
663
664
665class MVIN(NumericPartTextFrame):
666    "iTunes Movement Number/Count"
667
668
669class MVI(MVIN):
670    "iTunes Movement Number/Count"
671
672
673class GRP1(TextFrame):
674    "iTunes Grouping"
675
676
677class GP1(GRP1):
678    "iTunes Grouping"
679
680
681class TDOR(TimeStampTextFrame):
682    "Original Release Time"
683
684
685class TDLY(NumericTextFrame):
686    "Audio Delay (ms)"
687
688
689class TDRC(TimeStampTextFrame):
690    "Recording Time"
691
692
693class TDRL(TimeStampTextFrame):
694    "Release Time"
695
696
697class TDTG(TimeStampTextFrame):
698    "Tagging Time"
699
700
701class TENC(TextFrame):
702    "Encoder"
703
704
705class TEXT(TextFrame):
706    "Lyricist"
707
708
709class TFLT(TextFrame):
710    "File type"
711
712
713class TGID(TextFrame):
714    "iTunes Podcast Identifier"
715
716
717class TIME(TextFrame):
718    "Time of recording (HHMM)"
719
720
721class TIT1(TextFrame):
722    "Content group description"
723
724
725class TIT2(TextFrame):
726    "Title"
727
728
729class TIT3(TextFrame):
730    "Subtitle/Description refinement"
731
732
733class TKEY(TextFrame):
734    "Starting Key"
735
736
737class TLAN(TextFrame):
738    "Audio Languages"
739
740
741class TLEN(NumericTextFrame):
742    "Audio Length (ms)"
743
744
745class TMED(TextFrame):
746    "Source Media Type"
747
748
749class TMOO(TextFrame):
750    "Mood"
751
752
753class TOAL(TextFrame):
754    "Original Album"
755
756
757class TOFN(TextFrame):
758    "Original Filename"
759
760
761class TOLY(TextFrame):
762    "Original Lyricist"
763
764
765class TOPE(TextFrame):
766    "Original Artist/Performer"
767
768
769class TORY(NumericTextFrame):
770    "Original Release Year"
771
772
773class TOWN(TextFrame):
774    "Owner/Licensee"
775
776
777class TPE1(TextFrame):
778    "Lead Artist/Performer/Soloist/Group"
779
780
781class TPE2(TextFrame):
782    "Band/Orchestra/Accompaniment"
783
784
785class TPE3(TextFrame):
786    "Conductor"
787
788
789class TPE4(TextFrame):
790    "Interpreter/Remixer/Modifier"
791
792
793class TPOS(NumericPartTextFrame):
794    "Part of set"
795
796
797class TPRO(TextFrame):
798    "Produced (P)"
799
800
801class TPUB(TextFrame):
802    "Publisher"
803
804
805class TRCK(NumericPartTextFrame):
806    "Track Number"
807
808
809class TRDA(TextFrame):
810    "Recording Dates"
811
812
813class TRSN(TextFrame):
814    "Internet Radio Station Name"
815
816
817class TRSO(TextFrame):
818    "Internet Radio Station Owner"
819
820
821class TSIZ(NumericTextFrame):
822    "Size of audio data (bytes)"
823
824
825class TSO2(TextFrame):
826    "iTunes Album Artist Sort"
827
828
829class TSOA(TextFrame):
830    "Album Sort Order key"
831
832
833class TSOC(TextFrame):
834    "iTunes Composer Sort"
835
836
837class TSOP(TextFrame):
838    "Perfomer Sort Order key"
839
840
841class TSOT(TextFrame):
842    "Title Sort Order key"
843
844
845class TSRC(TextFrame):
846    "International Standard Recording Code (ISRC)"
847
848
849class TSSE(TextFrame):
850    "Encoder settings"
851
852
853class TSST(TextFrame):
854    "Set Subtitle"
855
856
857class TYER(NumericTextFrame):
858    "Year of recording"
859
860
861class TXXX(TextFrame):
862    """User-defined text data.
863
864    TXXX frames have a 'desc' attribute which is set to any Unicode
865    value (though the encoding of the text and the description must be
866    the same). Many taggers use this frame to store freeform keys.
867    """
868
869    _framespec = [
870        EncodingSpec('encoding'),
871        EncodedTextSpec('desc'),
872        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
873    ]
874
875    @property
876    def HashKey(self):
877        return '%s:%s' % (self.FrameID, self.desc)
878
879    def _pprint(self):
880        return "%s=%s" % (self.desc, " / ".join(self.text))
881
882
883class WCOM(UrlFrameU):
884    "Commercial Information"
885
886
887class WCOP(UrlFrame):
888    "Copyright Information"
889
890
891class WFED(UrlFrame):
892    "iTunes Podcast Feed"
893
894
895class WOAF(UrlFrame):
896    "Official File Information"
897
898
899class WOAR(UrlFrameU):
900    "Official Artist/Performer Information"
901
902
903class WOAS(UrlFrame):
904    "Official Source Information"
905
906
907class WORS(UrlFrame):
908    "Official Internet Radio Information"
909
910
911class WPAY(UrlFrame):
912    "Payment Information"
913
914
915class WPUB(UrlFrame):
916    "Official Publisher Information"
917
918
919class WXXX(UrlFrame):
920    """User-defined URL data.
921
922    Like TXXX, this has a freeform description associated with it.
923    """
924
925    _framespec = [
926        EncodingSpec('encoding', default=Encoding.UTF16),
927        EncodedTextSpec('desc'),
928        Latin1TextSpec('url'),
929    ]
930
931    @property
932    def HashKey(self):
933        return '%s:%s' % (self.FrameID, self.desc)
934
935
936class PairedTextFrame(Frame):
937    """Paired text strings.
938
939    Some ID3 frames pair text strings, to associate names with a more
940    specific involvement in the song. The 'people' attribute of these
941    frames contains a list of pairs::
942
943        [['trumpet', 'Miles Davis'], ['bass', 'Paul Chambers']]
944
945    Like text frames, these frames also have an encoding attribute.
946    """
947
948    _framespec = [
949        EncodingSpec('encoding', default=Encoding.UTF16),
950        MultiSpec('people',
951                  EncodedTextSpec('involvement'),
952                  EncodedTextSpec('person'), default=[])
953    ]
954
955    def __eq__(self, other):
956        return self.people == other
957
958    __hash__ = Frame.__hash__
959
960
961class TIPL(PairedTextFrame):
962    "Involved People List"
963
964
965class TMCL(PairedTextFrame):
966    "Musicians Credits List"
967
968
969class IPLS(TIPL):
970    "Involved People List"
971
972
973class BinaryFrame(Frame):
974    """Binary data
975
976    The 'data' attribute contains the raw byte string.
977    """
978
979    _framespec = [
980        BinaryDataSpec('data'),
981    ]
982
983    def __eq__(self, other):
984        return self.data == other
985
986    __hash__ = Frame.__hash__
987
988
989class MCDI(BinaryFrame):
990    "Binary dump of CD's TOC"
991
992
993class ETCO(Frame):
994    """Event timing codes."""
995
996    _framespec = [
997        ByteSpec("format", default=1),
998        KeyEventSpec("events", default=[]),
999    ]
1000
1001    def __eq__(self, other):
1002        return self.events == other
1003
1004    __hash__ = Frame.__hash__
1005
1006
1007class MLLT(Frame):
1008    """MPEG location lookup table.
1009
1010    This frame's attributes may be changed in the future based on
1011    feedback from real-world use.
1012    """
1013
1014    _framespec = [
1015        SizedIntegerSpec('frames', size=2, default=0),
1016        SizedIntegerSpec('bytes', size=3, default=0),
1017        SizedIntegerSpec('milliseconds', size=3, default=0),
1018        ByteSpec('bits_for_bytes', default=0),
1019        ByteSpec('bits_for_milliseconds', default=0),
1020        BinaryDataSpec('data'),
1021    ]
1022
1023    def __eq__(self, other):
1024        return self.data == other
1025
1026    __hash__ = Frame.__hash__
1027
1028
1029class SYTC(Frame):
1030    """Synchronised tempo codes.
1031
1032    This frame's attributes may be changed in the future based on
1033    feedback from real-world use.
1034    """
1035
1036    _framespec = [
1037        ByteSpec("format", default=1),
1038        BinaryDataSpec("data"),
1039    ]
1040
1041    def __eq__(self, other):
1042        return self.data == other
1043
1044    __hash__ = Frame.__hash__
1045
1046
1047@swap_to_string
1048class USLT(Frame):
1049    """Unsynchronised lyrics/text transcription.
1050
1051    Lyrics have a three letter ISO language code ('lang'), a
1052    description ('desc'), and a block of plain text ('text').
1053    """
1054
1055    _framespec = [
1056        EncodingSpec('encoding', default=Encoding.UTF16),
1057        StringSpec('lang', length=3, default=u"XXX"),
1058        EncodedTextSpec('desc'),
1059        EncodedTextSpec('text'),
1060    ]
1061
1062    @property
1063    def HashKey(self):
1064        return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
1065
1066    def __bytes__(self):
1067        return self.text.encode('utf-8')
1068
1069    def __str__(self):
1070        return self.text
1071
1072    def __eq__(self, other):
1073        return self.text == other
1074
1075    __hash__ = Frame.__hash__
1076
1077    def _pprint(self):
1078        return "%s=%s=%s" % (self.desc, self.lang, self.text)
1079
1080
1081@swap_to_string
1082class SYLT(Frame):
1083    """Synchronised lyrics/text."""
1084
1085    _framespec = [
1086        EncodingSpec('encoding'),
1087        StringSpec('lang', length=3, default=u"XXX"),
1088        ByteSpec('format', default=1),
1089        ByteSpec('type', default=0),
1090        EncodedTextSpec('desc'),
1091        SynchronizedTextSpec('text'),
1092    ]
1093
1094    @property
1095    def HashKey(self):
1096        return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
1097
1098    def _pprint(self):
1099        return str(self)
1100
1101    def __eq__(self, other):
1102        return str(self) == other
1103
1104    __hash__ = Frame.__hash__
1105
1106    def __str__(self):
1107        unit = 'fr' if self.format == 1 else 'ms'
1108        return u"\n".join("[{0}{1}]: {2}".format(time, unit, text)
1109                          for (text, time) in self.text)
1110
1111    def __bytes__(self):
1112        return text_type(self).encode("utf-8")
1113
1114
1115class COMM(TextFrame):
1116    """User comment.
1117
1118    User comment frames have a descrption, like TXXX, and also a three
1119    letter ISO language code in the 'lang' attribute.
1120    """
1121
1122    _framespec = [
1123        EncodingSpec('encoding'),
1124        StringSpec('lang', length=3, default="XXX"),
1125        EncodedTextSpec('desc'),
1126        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
1127    ]
1128
1129    @property
1130    def HashKey(self):
1131        return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
1132
1133    def _pprint(self):
1134        return "%s=%s=%s" % (self.desc, self.lang, " / ".join(self.text))
1135
1136
1137class RVA2(Frame):
1138    """Relative volume adjustment (2).
1139
1140    This frame is used to implemented volume scaling, and in
1141    particular, normalization using ReplayGain.
1142
1143    Attributes:
1144
1145    * desc -- description or context of this adjustment
1146    * channel -- audio channel to adjust (master is 1)
1147    * gain -- a + or - dB gain relative to some reference level
1148    * peak -- peak of the audio as a floating point number, [0, 1]
1149
1150    When storing ReplayGain tags, use descriptions of 'album' and
1151    'track' on channel 1.
1152    """
1153
1154    _framespec = [
1155        Latin1TextSpec('desc'),
1156        ChannelSpec('channel', default=1),
1157        VolumeAdjustmentSpec('gain', default=1),
1158        VolumePeakSpec('peak', default=1),
1159    ]
1160
1161    _channels = ["Other", "Master volume", "Front right", "Front left",
1162                 "Back right", "Back left", "Front centre", "Back centre",
1163                 "Subwoofer"]
1164
1165    @property
1166    def HashKey(self):
1167        return '%s:%s' % (self.FrameID, self.desc)
1168
1169    def __eq__(self, other):
1170        try:
1171            return ((str(self) == other) or
1172                    (self.desc == other.desc and
1173                     self.channel == other.channel and
1174                     self.gain == other.gain and
1175                     self.peak == other.peak))
1176        except AttributeError:
1177            return False
1178
1179    __hash__ = Frame.__hash__
1180
1181    def __str__(self):
1182        return "%s: %+0.4f dB/%0.4f" % (
1183            self._channels[self.channel], self.gain, self.peak)
1184
1185
1186class EQU2(Frame):
1187    """Equalisation (2).
1188
1189    Attributes:
1190    method -- interpolation method (0 = band, 1 = linear)
1191    desc -- identifying description
1192    adjustments -- list of (frequency, vol_adjustment) pairs
1193    """
1194
1195    _framespec = [
1196        ByteSpec("method", default=0),
1197        Latin1TextSpec("desc"),
1198        VolumeAdjustmentsSpec("adjustments", default=[]),
1199    ]
1200
1201    def __eq__(self, other):
1202        return self.adjustments == other
1203
1204    __hash__ = Frame.__hash__
1205
1206    @property
1207    def HashKey(self):
1208        return '%s:%s' % (self.FrameID, self.desc)
1209
1210
1211class RVAD(Frame):
1212    """Relative volume adjustment"""
1213
1214    _framespec = [
1215        RVASpec("adjustments", stereo_only=False),
1216    ]
1217
1218    __hash__ = Frame.__hash__
1219
1220    def __eq__(self, other):
1221        if not isinstance(other, RVAD):
1222            return False
1223        return self.adjustments == other.adjustments
1224
1225
1226# class EQUA: unsupported
1227
1228
1229class RVRB(Frame):
1230    """Reverb."""
1231
1232    _framespec = [
1233        SizedIntegerSpec('left', size=2, default=0),
1234        SizedIntegerSpec('right', size=2, default=0),
1235        ByteSpec('bounce_left', default=0),
1236        ByteSpec('bounce_right', default=0),
1237        ByteSpec('feedback_ltl', default=0),
1238        ByteSpec('feedback_ltr', default=0),
1239        ByteSpec('feedback_rtr', default=0),
1240        ByteSpec('feedback_rtl', default=0),
1241        ByteSpec('premix_ltr', default=0),
1242        ByteSpec('premix_rtl', default=0),
1243    ]
1244
1245    def __eq__(self, other):
1246        return (self.left, self.right) == other
1247
1248    __hash__ = Frame.__hash__
1249
1250
1251class APIC(Frame):
1252    """Attached (or linked) Picture.
1253
1254    Attributes:
1255
1256    * encoding -- text encoding for the description
1257    * mime -- a MIME type (e.g. image/jpeg) or '-->' if the data is a URI
1258    * type -- the source of the image (3 is the album front cover)
1259    * desc -- a text description of the image
1260    * data -- raw image data, as a byte string
1261
1262    Mutagen will automatically compress large images when saving tags.
1263    """
1264
1265    _framespec = [
1266        EncodingSpec('encoding'),
1267        Latin1TextSpec('mime'),
1268        PictureTypeSpec('type'),
1269        EncodedTextSpec('desc'),
1270        BinaryDataSpec('data'),
1271    ]
1272
1273    def __eq__(self, other):
1274        return self.data == other
1275
1276    __hash__ = Frame.__hash__
1277
1278    @property
1279    def HashKey(self):
1280        return '%s:%s' % (self.FrameID, self.desc)
1281
1282    def _merge_frame(self, other):
1283        other.desc += u" "
1284        return other
1285
1286    def _pprint(self):
1287        type_desc = text_type(self.type)
1288        if hasattr(self.type, "_pprint"):
1289            type_desc = self.type._pprint()
1290
1291        return "%s, %s (%s, %d bytes)" % (
1292            type_desc, self.desc, self.mime, len(self.data))
1293
1294
1295class PCNT(Frame):
1296    """Play counter.
1297
1298    The 'count' attribute contains the (recorded) number of times this
1299    file has been played.
1300
1301    This frame is basically obsoleted by POPM.
1302    """
1303
1304    _framespec = [
1305        IntegerSpec('count', default=0),
1306    ]
1307
1308    def __eq__(self, other):
1309        return self.count == other
1310
1311    __hash__ = Frame.__hash__
1312
1313    def __pos__(self):
1314        return self.count
1315
1316    def _pprint(self):
1317        return text_type(self.count)
1318
1319
1320class PCST(Frame):
1321    """iTunes Podcast Flag"""
1322
1323    _framespec = [
1324        IntegerSpec('value', default=0),
1325    ]
1326
1327    def __eq__(self, other):
1328        return self.value == other
1329
1330    __hash__ = Frame.__hash__
1331
1332    def __pos__(self):
1333        return self.value
1334
1335    def _pprint(self):
1336        return text_type(self.value)
1337
1338
1339class POPM(Frame):
1340    """Popularimeter.
1341
1342    This frame keys a rating (out of 255) and a play count to an email
1343    address.
1344
1345    Attributes:
1346
1347    * email -- email this POPM frame is for
1348    * rating -- rating from 0 to 255
1349    * count -- number of times the files has been played (optional)
1350    """
1351
1352    _framespec = [
1353        Latin1TextSpec('email'),
1354        ByteSpec('rating', default=0),
1355    ]
1356
1357    _optionalspec = [
1358        IntegerSpec('count', default=0),
1359    ]
1360
1361    @property
1362    def HashKey(self):
1363        return '%s:%s' % (self.FrameID, self.email)
1364
1365    def __eq__(self, other):
1366        return self.rating == other
1367
1368    __hash__ = Frame.__hash__
1369
1370    def __pos__(self):
1371        return self.rating
1372
1373    def _pprint(self):
1374        return "%s=%r %r/255" % (
1375            self.email, getattr(self, 'count', None), self.rating)
1376
1377
1378class GEOB(Frame):
1379    """General Encapsulated Object.
1380
1381    A blob of binary data, that is not a picture (those go in APIC).
1382
1383    Attributes:
1384
1385    * encoding -- encoding of the description
1386    * mime -- MIME type of the data or '-->' if the data is a URI
1387    * filename -- suggested filename if extracted
1388    * desc -- text description of the data
1389    * data -- raw data, as a byte string
1390    """
1391
1392    _framespec = [
1393        EncodingSpec('encoding'),
1394        Latin1TextSpec('mime'),
1395        EncodedTextSpec('filename'),
1396        EncodedTextSpec('desc'),
1397        BinaryDataSpec('data'),
1398    ]
1399
1400    @property
1401    def HashKey(self):
1402        return '%s:%s' % (self.FrameID, self.desc)
1403
1404    def __eq__(self, other):
1405        return self.data == other
1406
1407    __hash__ = Frame.__hash__
1408
1409
1410class RBUF(Frame):
1411    """Recommended buffer size.
1412
1413    Attributes:
1414
1415    * size -- recommended buffer size in bytes
1416    * info -- if ID3 tags may be elsewhere in the file (optional)
1417    * offset -- the location of the next ID3 tag, if any
1418
1419    Mutagen will not find the next tag itself.
1420    """
1421
1422    _framespec = [
1423        SizedIntegerSpec('size', size=3, default=0),
1424    ]
1425
1426    _optionalspec = [
1427        ByteSpec('info', default=0),
1428        SizedIntegerSpec('offset', size=4, default=0),
1429    ]
1430
1431    def __eq__(self, other):
1432        return self.size == other
1433
1434    __hash__ = Frame.__hash__
1435
1436    def __pos__(self):
1437        return self.size
1438
1439
1440@swap_to_string
1441class AENC(Frame):
1442    """Audio encryption.
1443
1444    Attributes:
1445
1446    * owner -- key identifying this encryption type
1447    * preview_start -- unencrypted data block offset
1448    * preview_length -- number of unencrypted blocks
1449    * data -- data required for decryption (optional)
1450
1451    Mutagen cannot decrypt files.
1452    """
1453
1454    _framespec = [
1455        Latin1TextSpec('owner'),
1456        SizedIntegerSpec('preview_start', size=2, default=0),
1457        SizedIntegerSpec('preview_length', size=2, default=0),
1458        BinaryDataSpec('data'),
1459    ]
1460
1461    @property
1462    def HashKey(self):
1463        return '%s:%s' % (self.FrameID, self.owner)
1464
1465    def __bytes__(self):
1466        return self.owner.encode('utf-8')
1467
1468    def __str__(self):
1469        return self.owner
1470
1471    def __eq__(self, other):
1472        return self.owner == other
1473
1474    __hash__ = Frame.__hash__
1475
1476
1477class LINK(Frame):
1478    """Linked information.
1479
1480    Attributes:
1481
1482    * frameid -- the ID of the linked frame
1483    * url -- the location of the linked frame
1484    * data -- further ID information for the frame
1485    """
1486
1487    _framespec = [
1488        FrameIDSpec('frameid', length=4),
1489        Latin1TextSpec('url'),
1490        BinaryDataSpec('data'),
1491    ]
1492
1493    @property
1494    def HashKey(self):
1495        return "%s:%s:%s:%s" % (
1496            self.FrameID, self.frameid, self.url, _bytes2key(self.data))
1497
1498    def __eq__(self, other):
1499        return (self.frameid, self.url, self.data) == other
1500
1501    __hash__ = Frame.__hash__
1502
1503
1504class POSS(Frame):
1505    """Position synchronisation frame
1506
1507    Attribute:
1508
1509    * format -- format of the position attribute (frames or milliseconds)
1510    * position -- current position of the file
1511    """
1512
1513    _framespec = [
1514        ByteSpec('format', default=1),
1515        IntegerSpec('position', default=0),
1516    ]
1517
1518    def __pos__(self):
1519        return self.position
1520
1521    def __eq__(self, other):
1522        return self.position == other
1523
1524    __hash__ = Frame.__hash__
1525
1526
1527class UFID(Frame):
1528    """Unique file identifier.
1529
1530    Attributes:
1531
1532    * owner -- format/type of identifier
1533    * data -- identifier
1534    """
1535
1536    _framespec = [
1537        Latin1TextSpec('owner'),
1538        BinaryDataSpec('data'),
1539    ]
1540
1541    @property
1542    def HashKey(self):
1543        return '%s:%s' % (self.FrameID, self.owner)
1544
1545    def __eq__(s, o):
1546        if isinstance(o, UFI):
1547            return s.owner == o.owner and s.data == o.data
1548        else:
1549            return s.data == o
1550
1551    __hash__ = Frame.__hash__
1552
1553    def _pprint(self):
1554        return "%s=%r" % (self.owner, self.data)
1555
1556
1557@swap_to_string
1558class USER(Frame):
1559    """Terms of use.
1560
1561    Attributes:
1562
1563    * encoding -- text encoding
1564    * lang -- ISO three letter language code
1565    * text -- licensing terms for the audio
1566    """
1567
1568    _framespec = [
1569        EncodingSpec('encoding'),
1570        StringSpec('lang', length=3, default=u"XXX"),
1571        EncodedTextSpec('text'),
1572    ]
1573
1574    @property
1575    def HashKey(self):
1576        return '%s:%s' % (self.FrameID, self.lang)
1577
1578    def __bytes__(self):
1579        return self.text.encode('utf-8')
1580
1581    def __str__(self):
1582        return self.text
1583
1584    def __eq__(self, other):
1585        return self.text == other
1586
1587    __hash__ = Frame.__hash__
1588
1589    def _pprint(self):
1590        return "%r=%s" % (self.lang, self.text)
1591
1592
1593@swap_to_string
1594class OWNE(Frame):
1595    """Ownership frame."""
1596
1597    _framespec = [
1598        EncodingSpec('encoding'),
1599        Latin1TextSpec('price'),
1600        StringSpec('date', length=8, default=u"19700101"),
1601        EncodedTextSpec('seller'),
1602    ]
1603
1604    def __bytes__(self):
1605        return self.seller.encode('utf-8')
1606
1607    def __str__(self):
1608        return self.seller
1609
1610    def __eq__(self, other):
1611        return self.seller == other
1612
1613    __hash__ = Frame.__hash__
1614
1615
1616class COMR(Frame):
1617    """Commercial frame."""
1618
1619    _framespec = [
1620        EncodingSpec('encoding'),
1621        Latin1TextSpec('price'),
1622        StringSpec('valid_until', length=8, default=u"19700101"),
1623        Latin1TextSpec('contact'),
1624        ByteSpec('format', default=0),
1625        EncodedTextSpec('seller'),
1626        EncodedTextSpec('desc'),
1627    ]
1628
1629    _optionalspec = [
1630        Latin1TextSpec('mime'),
1631        BinaryDataSpec('logo'),
1632    ]
1633
1634    @property
1635    def HashKey(self):
1636        return '%s:%s' % (self.FrameID, _bytes2key(self._writeData()))
1637
1638    def __eq__(self, other):
1639        return self._writeData() == other._writeData()
1640
1641    __hash__ = Frame.__hash__
1642
1643
1644@swap_to_string
1645class ENCR(Frame):
1646    """Encryption method registration.
1647
1648    The standard does not allow multiple ENCR frames with the same owner
1649    or the same method. Mutagen only verifies that the owner is unique.
1650    """
1651
1652    _framespec = [
1653        Latin1TextSpec('owner'),
1654        ByteSpec('method', default=0x80),
1655        BinaryDataSpec('data'),
1656    ]
1657
1658    @property
1659    def HashKey(self):
1660        return "%s:%s" % (self.FrameID, self.owner)
1661
1662    def __bytes__(self):
1663        return self.data
1664
1665    def __eq__(self, other):
1666        return self.data == other
1667
1668    __hash__ = Frame.__hash__
1669
1670
1671@swap_to_string
1672class GRID(Frame):
1673    """Group identification registration."""
1674
1675    _framespec = [
1676        Latin1TextSpec('owner'),
1677        ByteSpec('group', default=0x80),
1678        BinaryDataSpec('data'),
1679    ]
1680
1681    @property
1682    def HashKey(self):
1683        return '%s:%s' % (self.FrameID, self.group)
1684
1685    def __pos__(self):
1686        return self.group
1687
1688    def __bytes__(self):
1689        return self.owner.encode('utf-8')
1690
1691    def __str__(self):
1692        return self.owner
1693
1694    def __eq__(self, other):
1695        return self.owner == other or self.group == other
1696
1697    __hash__ = Frame.__hash__
1698
1699
1700@swap_to_string
1701class PRIV(Frame):
1702    """Private frame."""
1703
1704    _framespec = [
1705        Latin1TextSpec('owner'),
1706        BinaryDataSpec('data'),
1707    ]
1708
1709    @property
1710    def HashKey(self):
1711        return '%s:%s:%s' % (
1712            self.FrameID, self.owner, _bytes2key(self.data))
1713
1714    def __bytes__(self):
1715        return self.data
1716
1717    def __eq__(self, other):
1718        return self.data == other
1719
1720    def _pprint(self):
1721        return "%s=%r" % (self.owner, self.data)
1722
1723    __hash__ = Frame.__hash__
1724
1725
1726@swap_to_string
1727class SIGN(Frame):
1728    """Signature frame."""
1729
1730    _framespec = [
1731        ByteSpec('group', default=0x80),
1732        BinaryDataSpec('sig'),
1733    ]
1734
1735    @property
1736    def HashKey(self):
1737        return '%s:%s:%s' % (self.FrameID, self.group, _bytes2key(self.sig))
1738
1739    def __bytes__(self):
1740        return self.sig
1741
1742    def __eq__(self, other):
1743        return self.sig == other
1744
1745    __hash__ = Frame.__hash__
1746
1747
1748class SEEK(Frame):
1749    """Seek frame.
1750
1751    Mutagen does not find tags at seek offsets.
1752    """
1753
1754    _framespec = [
1755        IntegerSpec('offset', default=0),
1756    ]
1757
1758    def __pos__(self):
1759        return self.offset
1760
1761    def __eq__(self, other):
1762        return self.offset == other
1763
1764    __hash__ = Frame.__hash__
1765
1766
1767class ASPI(Frame):
1768    """Audio seek point index.
1769
1770    Attributes: S, L, N, b, and Fi. For the meaning of these, see
1771    the ID3v2.4 specification. Fi is a list of integers.
1772    """
1773
1774    _framespec = [
1775        SizedIntegerSpec("S", size=4, default=0),
1776        SizedIntegerSpec("L", size=4, default=0),
1777        SizedIntegerSpec("N", size=2, default=0),
1778        ByteSpec("b", default=0),
1779        ASPIIndexSpec("Fi", default=[]),
1780    ]
1781
1782    def __eq__(self, other):
1783        return self.Fi == other
1784
1785    __hash__ = Frame.__hash__
1786
1787
1788# ID3v2.2 frames
1789class UFI(UFID):
1790    "Unique File Identifier"
1791
1792
1793class TT1(TIT1):
1794    "Content group description"
1795
1796
1797class TT2(TIT2):
1798    "Title"
1799
1800
1801class TT3(TIT3):
1802    "Subtitle/Description refinement"
1803
1804
1805class TP1(TPE1):
1806    "Lead Artist/Performer/Soloist/Group"
1807
1808
1809class TP2(TPE2):
1810    "Band/Orchestra/Accompaniment"
1811
1812
1813class TP3(TPE3):
1814    "Conductor"
1815
1816
1817class TP4(TPE4):
1818    "Interpreter/Remixer/Modifier"
1819
1820
1821class TCM(TCOM):
1822    "Composer"
1823
1824
1825class TXT(TEXT):
1826    "Lyricist"
1827
1828
1829class TLA(TLAN):
1830    "Audio Language(s)"
1831
1832
1833class TCO(TCON):
1834    "Content Type (Genre)"
1835
1836
1837class TAL(TALB):
1838    "Album"
1839
1840
1841class TPA(TPOS):
1842    "Part of set"
1843
1844
1845class TRK(TRCK):
1846    "Track Number"
1847
1848
1849class TRC(TSRC):
1850    "International Standard Recording Code (ISRC)"
1851
1852
1853class TYE(TYER):
1854    "Year of recording"
1855
1856
1857class TDA(TDAT):
1858    "Date of recording (DDMM)"
1859
1860
1861class TIM(TIME):
1862    "Time of recording (HHMM)"
1863
1864
1865class TRD(TRDA):
1866    "Recording Dates"
1867
1868
1869class TMT(TMED):
1870    "Source Media Type"
1871
1872
1873class TFT(TFLT):
1874    "File Type"
1875
1876
1877class TBP(TBPM):
1878    "Beats per minute"
1879
1880
1881class TCP(TCMP):
1882    "iTunes Compilation Flag"
1883
1884
1885class TCR(TCOP):
1886    "Copyright (C)"
1887
1888
1889class TPB(TPUB):
1890    "Publisher"
1891
1892
1893class TEN(TENC):
1894    "Encoder"
1895
1896
1897class TST(TSOT):
1898    "Title Sort Order key"
1899
1900
1901class TSA(TSOA):
1902    "Album Sort Order key"
1903
1904
1905class TS2(TSO2):
1906    "iTunes Album Artist Sort"
1907
1908
1909class TSP(TSOP):
1910    "Perfomer Sort Order key"
1911
1912
1913class TSC(TSOC):
1914    "iTunes Composer Sort"
1915
1916
1917class TSS(TSSE):
1918    "Encoder settings"
1919
1920
1921class TOF(TOFN):
1922    "Original Filename"
1923
1924
1925class TLE(TLEN):
1926    "Audio Length (ms)"
1927
1928
1929class TSI(TSIZ):
1930    "Audio Data size (bytes)"
1931
1932
1933class TDY(TDLY):
1934    "Audio Delay (ms)"
1935
1936
1937class TKE(TKEY):
1938    "Starting Key"
1939
1940
1941class TOT(TOAL):
1942    "Original Album"
1943
1944
1945class TOA(TOPE):
1946    "Original Artist/Perfomer"
1947
1948
1949class TOL(TOLY):
1950    "Original Lyricist"
1951
1952
1953class TOR(TORY):
1954    "Original Release Year"
1955
1956
1957class TXX(TXXX):
1958    "User-defined Text"
1959
1960
1961class WAF(WOAF):
1962    "Official File Information"
1963
1964
1965class WAR(WOAR):
1966    "Official Artist/Performer Information"
1967
1968
1969class WAS(WOAS):
1970    "Official Source Information"
1971
1972
1973class WCM(WCOM):
1974    "Commercial Information"
1975
1976
1977class WCP(WCOP):
1978    "Copyright Information"
1979
1980
1981class WPB(WPUB):
1982    "Official Publisher Information"
1983
1984
1985class WXX(WXXX):
1986    "User-defined URL"
1987
1988
1989class IPL(IPLS):
1990    "Involved people list"
1991
1992
1993class MCI(MCDI):
1994    "Binary dump of CD's TOC"
1995
1996
1997class ETC(ETCO):
1998    "Event timing codes"
1999
2000
2001class MLL(MLLT):
2002    "MPEG location lookup table"
2003
2004
2005class STC(SYTC):
2006    "Synced tempo codes"
2007
2008
2009class ULT(USLT):
2010    "Unsychronised lyrics/text transcription"
2011
2012
2013class SLT(SYLT):
2014    "Synchronised lyrics/text"
2015
2016
2017class COM(COMM):
2018    "Comment"
2019
2020
2021class RVA(RVAD):
2022    "Relative volume adjustment"
2023
2024    _framespec = [
2025        RVASpec("adjustments", stereo_only=True),
2026    ]
2027
2028    def _to_other(self, other):
2029        if not isinstance(other, RVAD):
2030            raise TypeError
2031
2032        other.adjustments = list(self.adjustments)
2033
2034# class EQU(EQUA)
2035
2036
2037class REV(RVRB):
2038    "Reverb"
2039
2040
2041class PIC(APIC):
2042    """Attached Picture.
2043
2044    The 'mime' attribute of an ID3v2.2 attached picture must be either
2045    'PNG' or 'JPG'.
2046    """
2047
2048    _framespec = [
2049        EncodingSpec('encoding'),
2050        StringSpec('mime', length=3, default="JPG"),
2051        PictureTypeSpec('type'),
2052        EncodedTextSpec('desc'),
2053        BinaryDataSpec('data'),
2054    ]
2055
2056    def _to_other(self, other):
2057        if not isinstance(other, APIC):
2058            raise TypeError
2059
2060        other.encoding = self.encoding
2061        other.mime = self.mime
2062        other.type = self.type
2063        other.desc = self.desc
2064        other.data = self.data
2065
2066
2067class GEO(GEOB):
2068    "General Encapsulated Object"
2069
2070
2071class CNT(PCNT):
2072    "Play counter"
2073
2074
2075class POP(POPM):
2076    "Popularimeter"
2077
2078
2079class BUF(RBUF):
2080    "Recommended buffer size"
2081
2082
2083class CRM(Frame):
2084    """Encrypted meta frame"""
2085
2086    _framespec = [
2087        Latin1TextSpec('owner'),
2088        Latin1TextSpec('desc'),
2089        BinaryDataSpec('data'),
2090    ]
2091
2092    def __eq__(self, other):
2093        return self.data == other
2094    __hash__ = Frame.__hash__
2095
2096
2097class CRA(AENC):
2098    "Audio encryption"
2099
2100
2101class LNK(LINK):
2102    """Linked information"""
2103
2104    _framespec = [
2105        FrameIDSpec('frameid', length=3),
2106        Latin1TextSpec('url'),
2107        BinaryDataSpec('data'),
2108    ]
2109
2110    def _to_other(self, other):
2111        if not isinstance(other, LINK):
2112            raise TypeError
2113
2114        if isinstance(other, LNK):
2115            new_frameid = self.frameid
2116        else:
2117            try:
2118                new_frameid = Frames_2_2[self.frameid].__bases__[0].__name__
2119            except KeyError:
2120                new_frameid = self.frameid.ljust(4)
2121
2122        # we could end up with invalid IDs here, so bypass the validation
2123        other._setattr("frameid", new_frameid)
2124
2125        other.url = self.url
2126        other.data = self.data
2127
2128
2129Frames = {}
2130"""All supported ID3v2.3/4 frames, keyed by frame name."""
2131
2132
2133Frames_2_2 = {}
2134"""All supported ID3v2.2 frames, keyed by frame name."""
2135
2136
2137k, v = None, None
2138for k, v in iteritems(globals()):
2139    if isinstance(v, type) and issubclass(v, Frame):
2140        v.__module__ = "mutagen.id3"
2141
2142        if len(k) == 3:
2143            Frames_2_2[k] = v
2144        elif len(k) == 4:
2145            Frames[k] = v
2146
2147try:
2148    del k
2149    del v
2150except NameError:
2151    pass
2152