1# -*- coding: utf-8 -*-
2# Copyright (C) 2009  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
9from mutagen import Tags
10from mutagen._util import DictMixin, dict_match
11from mutagen.mp4 import MP4, MP4Tags, error, delete
12from ._compat import PY2, text_type, PY3
13
14
15__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"]
16
17
18class EasyMP4KeyError(error, KeyError, ValueError):
19    pass
20
21
22class EasyMP4Tags(DictMixin, Tags):
23    """EasyMP4Tags()
24
25    A file with MPEG-4 iTunes metadata.
26
27    Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII
28    strings, and values are a list of Unicode strings (and these lists
29    are always of length 0 or 1).
30
31    If you need access to the full MP4 metadata feature set, you should use
32    MP4, not EasyMP4.
33    """
34
35    Set = {}
36    Get = {}
37    Delete = {}
38    List = {}
39
40    def __init__(self, *args, **kwargs):
41        self.__mp4 = MP4Tags(*args, **kwargs)
42        self.load = self.__mp4.load
43        self.save = self.__mp4.save
44        self.delete = self.__mp4.delete
45
46    filename = property(lambda s: s.__mp4.filename,
47                        lambda s, fn: setattr(s.__mp4, 'filename', fn))
48
49    @property
50    def _padding(self):
51        return self.__mp4._padding
52
53    @classmethod
54    def RegisterKey(cls, key,
55                    getter=None, setter=None, deleter=None, lister=None):
56        """Register a new key mapping.
57
58        A key mapping is four functions, a getter, setter, deleter,
59        and lister. The key may be either a string or a glob pattern.
60
61        The getter, deleted, and lister receive an MP4Tags instance
62        and the requested key name. The setter also receives the
63        desired value, which will be a list of strings.
64
65        The getter, setter, and deleter are used to implement __getitem__,
66        __setitem__, and __delitem__.
67
68        The lister is used to implement keys(). It should return a
69        list of keys that are actually in the MP4 instance, provided
70        by its associated getter.
71        """
72        key = key.lower()
73        if getter is not None:
74            cls.Get[key] = getter
75        if setter is not None:
76            cls.Set[key] = setter
77        if deleter is not None:
78            cls.Delete[key] = deleter
79        if lister is not None:
80            cls.List[key] = lister
81
82    @classmethod
83    def RegisterTextKey(cls, key, atomid):
84        """Register a text key.
85
86        If the key you need to register is a simple one-to-one mapping
87        of MP4 atom name to EasyMP4Tags key, then you can use this
88        function::
89
90            EasyMP4Tags.RegisterTextKey("artist", "\xa9ART")
91        """
92        def getter(tags, key):
93            return tags[atomid]
94
95        def setter(tags, key, value):
96            tags[atomid] = value
97
98        def deleter(tags, key):
99            del(tags[atomid])
100
101        cls.RegisterKey(key, getter, setter, deleter)
102
103    @classmethod
104    def RegisterIntKey(cls, key, atomid, min_value=0, max_value=(2 ** 16) - 1):
105        """Register a scalar integer key.
106        """
107
108        def getter(tags, key):
109            return list(map(text_type, tags[atomid]))
110
111        def setter(tags, key, value):
112            clamp = lambda x: int(min(max(min_value, x), max_value))
113            tags[atomid] = [clamp(v) for v in map(int, value)]
114
115        def deleter(tags, key):
116            del(tags[atomid])
117
118        cls.RegisterKey(key, getter, setter, deleter)
119
120    @classmethod
121    def RegisterIntPairKey(cls, key, atomid, min_value=0,
122                           max_value=(2 ** 16) - 1):
123        def getter(tags, key):
124            ret = []
125            for (track, total) in tags[atomid]:
126                if total:
127                    ret.append(u"%d/%d" % (track, total))
128                else:
129                    ret.append(text_type(track))
130            return ret
131
132        def setter(tags, key, value):
133            clamp = lambda x: int(min(max(min_value, x), max_value))
134            data = []
135            for v in value:
136                try:
137                    tracks, total = v.split("/")
138                    tracks = clamp(int(tracks))
139                    total = clamp(int(total))
140                except (ValueError, TypeError):
141                    tracks = clamp(int(v))
142                    total = min_value
143                data.append((tracks, total))
144            tags[atomid] = data
145
146        def deleter(tags, key):
147            del(tags[atomid])
148
149        cls.RegisterKey(key, getter, setter, deleter)
150
151    @classmethod
152    def RegisterFreeformKey(cls, key, name, mean="com.apple.iTunes"):
153        """Register a text key.
154
155        If the key you need to register is a simple one-to-one mapping
156        of MP4 freeform atom (----) and name to EasyMP4Tags key, then
157        you can use this function::
158
159            EasyMP4Tags.RegisterFreeformKey(
160                "musicbrainz_artistid", "MusicBrainz Artist Id")
161        """
162        atomid = "----:" + mean + ":" + name
163
164        def getter(tags, key):
165            return [s.decode("utf-8", "replace") for s in tags[atomid]]
166
167        def setter(tags, key, value):
168            encoded = []
169            for v in value:
170                if not isinstance(v, text_type):
171                    if PY3:
172                        raise TypeError("%r not str" % v)
173                    v = v.decode("utf-8")
174                encoded.append(v.encode("utf-8"))
175            tags[atomid] = encoded
176
177        def deleter(tags, key):
178            del(tags[atomid])
179
180        cls.RegisterKey(key, getter, setter, deleter)
181
182    def __getitem__(self, key):
183        key = key.lower()
184        func = dict_match(self.Get, key)
185        if func is not None:
186            return func(self.__mp4, key)
187        else:
188            raise EasyMP4KeyError("%r is not a valid key" % key)
189
190    def __setitem__(self, key, value):
191        key = key.lower()
192
193        if PY2:
194            if isinstance(value, basestring):
195                value = [value]
196        else:
197            if isinstance(value, text_type):
198                value = [value]
199
200        func = dict_match(self.Set, key)
201        if func is not None:
202            return func(self.__mp4, key, value)
203        else:
204            raise EasyMP4KeyError("%r is not a valid key" % key)
205
206    def __delitem__(self, key):
207        key = key.lower()
208        func = dict_match(self.Delete, key)
209        if func is not None:
210            return func(self.__mp4, key)
211        else:
212            raise EasyMP4KeyError("%r is not a valid key" % key)
213
214    def keys(self):
215        keys = []
216        for key in self.Get.keys():
217            if key in self.List:
218                keys.extend(self.List[key](self.__mp4, key))
219            elif key in self:
220                keys.append(key)
221        return keys
222
223    def pprint(self):
224        """Print tag key=value pairs."""
225        strings = []
226        for key in sorted(self.keys()):
227            values = self[key]
228            for value in values:
229                strings.append("%s=%s" % (key, value))
230        return "\n".join(strings)
231
232for atomid, key in {
233    '\xa9nam': 'title',
234    '\xa9alb': 'album',
235    '\xa9ART': 'artist',
236    'aART': 'albumartist',
237    '\xa9day': 'date',
238    '\xa9cmt': 'comment',
239    'desc': 'description',
240    '\xa9grp': 'grouping',
241    '\xa9gen': 'genre',
242    'cprt': 'copyright',
243    'soal': 'albumsort',
244    'soaa': 'albumartistsort',
245    'soar': 'artistsort',
246    'sonm': 'titlesort',
247    'soco': 'composersort',
248}.items():
249    EasyMP4Tags.RegisterTextKey(key, atomid)
250
251for name, key in {
252    'MusicBrainz Artist Id': 'musicbrainz_artistid',
253    'MusicBrainz Track Id': 'musicbrainz_trackid',
254    'MusicBrainz Album Id': 'musicbrainz_albumid',
255    'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid',
256    'MusicIP PUID': 'musicip_puid',
257    'MusicBrainz Album Status': 'musicbrainz_albumstatus',
258    'MusicBrainz Album Type': 'musicbrainz_albumtype',
259    'MusicBrainz Release Country': 'releasecountry',
260}.items():
261    EasyMP4Tags.RegisterFreeformKey(key, name)
262
263for name, key in {
264    "tmpo": "bpm",
265}.items():
266    EasyMP4Tags.RegisterIntKey(key, name)
267
268for name, key in {
269    "trkn": "tracknumber",
270    "disk": "discnumber",
271}.items():
272    EasyMP4Tags.RegisterIntPairKey(key, name)
273
274
275class EasyMP4(MP4):
276    """EasyMP4(filelike)
277
278    Like :class:`MP4 <mutagen.mp4.MP4>`, but uses :class:`EasyMP4Tags` for
279    tags.
280
281    Attributes:
282        info (`mutagen.mp4.MP4Info`)
283        tags (`EasyMP4Tags`)
284    """
285
286    MP4Tags = EasyMP4Tags
287
288    Get = EasyMP4Tags.Get
289    Set = EasyMP4Tags.Set
290    Delete = EasyMP4Tags.Delete
291    List = EasyMP4Tags.List
292    RegisterTextKey = EasyMP4Tags.RegisterTextKey
293    RegisterKey = EasyMP4Tags.RegisterKey
294