1# -*- coding: utf-8 -*-
2# Copyright (C) 2006  Joe Wreschnig
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
9"""Easier access to ID3 tags.
10
11EasyID3 is a wrapper around mutagen.id3.ID3 to make ID3 tags appear
12more like Vorbis or APEv2 tags.
13"""
14
15import mutagen.id3
16
17from ._compat import iteritems, text_type, PY2
18from mutagen import Metadata
19from mutagen._util import DictMixin, dict_match, loadfile
20from mutagen.id3 import ID3, error, delete, ID3FileType
21
22
23__all__ = ['EasyID3', 'Open', 'delete']
24
25
26class EasyID3KeyError(KeyError, ValueError, error):
27    """Raised when trying to get/set an invalid key.
28
29    Subclasses both KeyError and ValueError for API compatibility,
30    catching KeyError is preferred.
31    """
32
33
34class EasyID3(DictMixin, Metadata):
35    """EasyID3(filething=None)
36
37    A file with an ID3 tag.
38
39    Like Vorbis comments, EasyID3 keys are case-insensitive ASCII
40    strings. Only a subset of ID3 frames are supported by default. Use
41    EasyID3.RegisterKey and its wrappers to support more.
42
43    You can also set the GetFallback, SetFallback, and DeleteFallback
44    to generic key getter/setter/deleter functions, which are called
45    if no specific handler is registered for a key. Additionally,
46    ListFallback can be used to supply an arbitrary list of extra
47    keys. These can be set on EasyID3 or on individual instances after
48    creation.
49
50    To use an EasyID3 class with mutagen.mp3.MP3::
51
52        from mutagen.mp3 import EasyMP3 as MP3
53        MP3(filename)
54
55    Because many of the attributes are constructed on the fly, things
56    like the following will not work::
57
58        ezid3["performer"].append("Joe")
59
60    Instead, you must do::
61
62        values = ezid3["performer"]
63        values.append("Joe")
64        ezid3["performer"] = values
65
66    """
67
68    Set = {}
69    Get = {}
70    Delete = {}
71    List = {}
72
73    # For compatibility.
74    valid_keys = Get
75
76    GetFallback = None
77    SetFallback = None
78    DeleteFallback = None
79    ListFallback = None
80
81    @classmethod
82    def RegisterKey(cls, key,
83                    getter=None, setter=None, deleter=None, lister=None):
84        """Register a new key mapping.
85
86        A key mapping is four functions, a getter, setter, deleter,
87        and lister. The key may be either a string or a glob pattern.
88
89        The getter, deleted, and lister receive an ID3 instance and
90        the requested key name. The setter also receives the desired
91        value, which will be a list of strings.
92
93        The getter, setter, and deleter are used to implement __getitem__,
94        __setitem__, and __delitem__.
95
96        The lister is used to implement keys(). It should return a
97        list of keys that are actually in the ID3 instance, provided
98        by its associated getter.
99        """
100        key = key.lower()
101        if getter is not None:
102            cls.Get[key] = getter
103        if setter is not None:
104            cls.Set[key] = setter
105        if deleter is not None:
106            cls.Delete[key] = deleter
107        if lister is not None:
108            cls.List[key] = lister
109
110    @classmethod
111    def RegisterTextKey(cls, key, frameid):
112        """Register a text key.
113
114        If the key you need to register is a simple one-to-one mapping
115        of ID3 frame name to EasyID3 key, then you can use this
116        function::
117
118            EasyID3.RegisterTextKey("title", "TIT2")
119        """
120        def getter(id3, key):
121            return list(id3[frameid])
122
123        def setter(id3, key, value):
124            try:
125                frame = id3[frameid]
126            except KeyError:
127                id3.add(mutagen.id3.Frames[frameid](encoding=3, text=value))
128            else:
129                frame.encoding = 3
130                frame.text = value
131
132        def deleter(id3, key):
133            del(id3[frameid])
134
135        cls.RegisterKey(key, getter, setter, deleter)
136
137    @classmethod
138    def RegisterTXXXKey(cls, key, desc):
139        """Register a user-defined text frame key.
140
141        Some ID3 tags are stored in TXXX frames, which allow a
142        freeform 'description' which acts as a subkey,
143        e.g. TXXX:BARCODE.::
144
145            EasyID3.RegisterTXXXKey('barcode', 'BARCODE').
146        """
147        frameid = "TXXX:" + desc
148
149        def getter(id3, key):
150            return list(id3[frameid])
151
152        def setter(id3, key, value):
153            enc = 0
154            # Store 8859-1 if we can, per MusicBrainz spec.
155            for v in value:
156                if v and max(v) > u'\x7f':
157                    enc = 3
158                    break
159
160            id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc))
161
162        def deleter(id3, key):
163            del(id3[frameid])
164
165        cls.RegisterKey(key, getter, setter, deleter)
166
167    def __init__(self, filename=None):
168        self.__id3 = ID3()
169        if filename is not None:
170            self.load(filename)
171
172    load = property(lambda s: s.__id3.load,
173                    lambda s, v: setattr(s.__id3, 'load', v))
174
175    @loadfile(writable=True, create=True)
176    def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
177             padding=None):
178        """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
179
180        Save changes to a file.
181        See :meth:`mutagen.id3.ID3.save` for more info.
182        """
183
184        if v2_version == 3:
185            # EasyID3 only works with v2.4 frames, so update_to_v23() would
186            # break things. We have to save a shallow copy of all tags
187            # and restore it after saving. Due to CHAP/CTOC copying has
188            # to be done recursively implemented in ID3Tags.
189            backup = self.__id3._copy()
190            try:
191                self.__id3.update_to_v23()
192                self.__id3.save(
193                    filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep,
194                    padding=padding)
195            finally:
196                self.__id3._restore(backup)
197        else:
198            self.__id3.save(filething, v1=v1, v2_version=v2_version,
199                            v23_sep=v23_sep, padding=padding)
200
201    delete = property(lambda s: s.__id3.delete,
202                      lambda s, v: setattr(s.__id3, 'delete', v))
203
204    filename = property(lambda s: s.__id3.filename,
205                        lambda s, fn: setattr(s.__id3, 'filename', fn))
206
207    @property
208    def size(self):
209        return self.__id3.size
210
211    def __getitem__(self, key):
212        func = dict_match(self.Get, key.lower(), self.GetFallback)
213        if func is not None:
214            return func(self.__id3, key)
215        else:
216            raise EasyID3KeyError("%r is not a valid key" % key)
217
218    def __setitem__(self, key, value):
219        if PY2:
220            if isinstance(value, basestring):
221                value = [value]
222        else:
223            if isinstance(value, text_type):
224                value = [value]
225        func = dict_match(self.Set, key.lower(), self.SetFallback)
226        if func is not None:
227            return func(self.__id3, key, value)
228        else:
229            raise EasyID3KeyError("%r is not a valid key" % key)
230
231    def __delitem__(self, key):
232        func = dict_match(self.Delete, key.lower(), self.DeleteFallback)
233        if func is not None:
234            return func(self.__id3, key)
235        else:
236            raise EasyID3KeyError("%r is not a valid key" % key)
237
238    def keys(self):
239        keys = []
240        for key in self.Get.keys():
241            if key in self.List:
242                keys.extend(self.List[key](self.__id3, key))
243            elif key in self:
244                keys.append(key)
245        if self.ListFallback is not None:
246            keys.extend(self.ListFallback(self.__id3, ""))
247        return keys
248
249    def pprint(self):
250        """Print tag key=value pairs."""
251        strings = []
252        for key in sorted(self.keys()):
253            values = self[key]
254            for value in values:
255                strings.append("%s=%s" % (key, value))
256        return "\n".join(strings)
257
258
259Open = EasyID3
260
261
262def genre_get(id3, key):
263    return id3["TCON"].genres
264
265
266def genre_set(id3, key, value):
267    try:
268        frame = id3["TCON"]
269    except KeyError:
270        id3.add(mutagen.id3.TCON(encoding=3, text=value))
271    else:
272        frame.encoding = 3
273        frame.genres = value
274
275
276def genre_delete(id3, key):
277    del(id3["TCON"])
278
279
280def date_get(id3, key):
281    return [stamp.text for stamp in id3["TDRC"].text]
282
283
284def date_set(id3, key, value):
285    id3.add(mutagen.id3.TDRC(encoding=3, text=value))
286
287
288def date_delete(id3, key):
289    del(id3["TDRC"])
290
291
292def original_date_get(id3, key):
293    return [stamp.text for stamp in id3["TDOR"].text]
294
295
296def original_date_set(id3, key, value):
297    id3.add(mutagen.id3.TDOR(encoding=3, text=value))
298
299
300def original_date_delete(id3, key):
301    del(id3["TDOR"])
302
303
304def performer_get(id3, key):
305    people = []
306    wanted_role = key.split(":", 1)[1]
307    try:
308        mcl = id3["TMCL"]
309    except KeyError:
310        raise KeyError(key)
311    for role, person in mcl.people:
312        if role == wanted_role:
313            people.append(person)
314    if people:
315        return people
316    else:
317        raise KeyError(key)
318
319
320def performer_set(id3, key, value):
321    wanted_role = key.split(":", 1)[1]
322    try:
323        mcl = id3["TMCL"]
324    except KeyError:
325        mcl = mutagen.id3.TMCL(encoding=3, people=[])
326        id3.add(mcl)
327    mcl.encoding = 3
328    people = [p for p in mcl.people if p[0] != wanted_role]
329    for v in value:
330        people.append((wanted_role, v))
331    mcl.people = people
332
333
334def performer_delete(id3, key):
335    wanted_role = key.split(":", 1)[1]
336    try:
337        mcl = id3["TMCL"]
338    except KeyError:
339        raise KeyError(key)
340    people = [p for p in mcl.people if p[0] != wanted_role]
341    if people == mcl.people:
342        raise KeyError(key)
343    elif people:
344        mcl.people = people
345    else:
346        del(id3["TMCL"])
347
348
349def performer_list(id3, key):
350    try:
351        mcl = id3["TMCL"]
352    except KeyError:
353        return []
354    else:
355        return list(set("performer:" + p[0] for p in mcl.people))
356
357
358def musicbrainz_trackid_get(id3, key):
359    return [id3["UFID:http://musicbrainz.org"].data.decode('ascii')]
360
361
362def musicbrainz_trackid_set(id3, key, value):
363    if len(value) != 1:
364        raise ValueError("only one track ID may be set per song")
365    value = value[0].encode('ascii')
366    try:
367        frame = id3["UFID:http://musicbrainz.org"]
368    except KeyError:
369        frame = mutagen.id3.UFID(owner="http://musicbrainz.org", data=value)
370        id3.add(frame)
371    else:
372        frame.data = value
373
374
375def musicbrainz_trackid_delete(id3, key):
376    del(id3["UFID:http://musicbrainz.org"])
377
378
379def website_get(id3, key):
380    urls = [frame.url for frame in id3.getall("WOAR")]
381    if urls:
382        return urls
383    else:
384        raise EasyID3KeyError(key)
385
386
387def website_set(id3, key, value):
388    id3.delall("WOAR")
389    for v in value:
390        id3.add(mutagen.id3.WOAR(url=v))
391
392
393def website_delete(id3, key):
394    id3.delall("WOAR")
395
396
397def gain_get(id3, key):
398    try:
399        frame = id3["RVA2:" + key[11:-5]]
400    except KeyError:
401        raise EasyID3KeyError(key)
402    else:
403        return [u"%+f dB" % frame.gain]
404
405
406def gain_set(id3, key, value):
407    if len(value) != 1:
408        raise ValueError(
409            "there must be exactly one gain value, not %r.", value)
410    gain = float(value[0].split()[0])
411    try:
412        frame = id3["RVA2:" + key[11:-5]]
413    except KeyError:
414        frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1)
415        id3.add(frame)
416    frame.gain = gain
417
418
419def gain_delete(id3, key):
420    try:
421        frame = id3["RVA2:" + key[11:-5]]
422    except KeyError:
423        pass
424    else:
425        if frame.peak:
426            frame.gain = 0.0
427        else:
428            del(id3["RVA2:" + key[11:-5]])
429
430
431def peak_get(id3, key):
432    try:
433        frame = id3["RVA2:" + key[11:-5]]
434    except KeyError:
435        raise EasyID3KeyError(key)
436    else:
437        return [u"%f" % frame.peak]
438
439
440def peak_set(id3, key, value):
441    if len(value) != 1:
442        raise ValueError(
443            "there must be exactly one peak value, not %r.", value)
444    peak = float(value[0])
445    if peak >= 2 or peak < 0:
446        raise ValueError("peak must be => 0 and < 2.")
447    try:
448        frame = id3["RVA2:" + key[11:-5]]
449    except KeyError:
450        frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1)
451        id3.add(frame)
452    frame.peak = peak
453
454
455def peak_delete(id3, key):
456    try:
457        frame = id3["RVA2:" + key[11:-5]]
458    except KeyError:
459        pass
460    else:
461        if frame.gain:
462            frame.peak = 0.0
463        else:
464            del(id3["RVA2:" + key[11:-5]])
465
466
467def peakgain_list(id3, key):
468    keys = []
469    for frame in id3.getall("RVA2"):
470        keys.append("replaygain_%s_gain" % frame.desc)
471        keys.append("replaygain_%s_peak" % frame.desc)
472    return keys
473
474for frameid, key in iteritems({
475    "TALB": "album",
476    "TBPM": "bpm",
477    "TCMP": "compilation",  # iTunes extension
478    "TCOM": "composer",
479    "TCOP": "copyright",
480    "TENC": "encodedby",
481    "TEXT": "lyricist",
482    "TLEN": "length",
483    "TMED": "media",
484    "TMOO": "mood",
485    "TIT2": "title",
486    "TIT3": "version",
487    "TPE1": "artist",
488    "TPE2": "albumartist",
489    "TPE3": "conductor",
490    "TPE4": "arranger",
491    "TPOS": "discnumber",
492    "TPUB": "organization",
493    "TRCK": "tracknumber",
494    "TOLY": "author",
495    "TSO2": "albumartistsort",  # iTunes extension
496    "TSOA": "albumsort",
497    "TSOC": "composersort",  # iTunes extension
498    "TSOP": "artistsort",
499    "TSOT": "titlesort",
500    "TSRC": "isrc",
501    "TSST": "discsubtitle",
502    "TLAN": "language",
503}):
504    EasyID3.RegisterTextKey(key, frameid)
505
506EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete)
507EasyID3.RegisterKey("date", date_get, date_set, date_delete)
508EasyID3.RegisterKey("originaldate", original_date_get, original_date_set,
509                    original_date_delete)
510EasyID3.RegisterKey(
511    "performer:*", performer_get, performer_set, performer_delete,
512    performer_list)
513EasyID3.RegisterKey("musicbrainz_trackid", musicbrainz_trackid_get,
514                    musicbrainz_trackid_set, musicbrainz_trackid_delete)
515EasyID3.RegisterKey("website", website_get, website_set, website_delete)
516EasyID3.RegisterKey(
517    "replaygain_*_gain", gain_get, gain_set, gain_delete, peakgain_list)
518EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete)
519
520# At various times, information for this came from
521# http://musicbrainz.org/docs/specs/metadata_tags.html
522# http://bugs.musicbrainz.org/ticket/1383
523# http://musicbrainz.org/doc/MusicBrainzTag
524for desc, key in iteritems({
525    u"MusicBrainz Artist Id": "musicbrainz_artistid",
526    u"MusicBrainz Album Id": "musicbrainz_albumid",
527    u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid",
528    u"MusicBrainz TRM Id": "musicbrainz_trmid",
529    u"MusicIP PUID": "musicip_puid",
530    u"MusicMagic Fingerprint": "musicip_fingerprint",
531    u"MusicBrainz Album Status": "musicbrainz_albumstatus",
532    u"MusicBrainz Album Type": "musicbrainz_albumtype",
533    u"MusicBrainz Album Release Country": "releasecountry",
534    u"MusicBrainz Disc Id": "musicbrainz_discid",
535    u"ASIN": "asin",
536    u"ALBUMARTISTSORT": "albumartistsort",
537    u"PERFORMER": "performer",
538    u"BARCODE": "barcode",
539    u"CATALOGNUMBER": "catalognumber",
540    u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid",
541    u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid",
542    u"MusicBrainz Work Id": "musicbrainz_workid",
543    u"Acoustid Fingerprint": "acoustid_fingerprint",
544    u"Acoustid Id": "acoustid_id",
545}):
546    EasyID3.RegisterTXXXKey(key, desc)
547
548
549class EasyID3FileType(ID3FileType):
550    """EasyID3FileType(filething=None)
551
552    Like ID3FileType, but uses EasyID3 for tags.
553
554    Arguments:
555        filething (filething)
556
557    Attributes:
558        tags (`EasyID3`)
559    """
560
561    ID3 = EasyID3
562