1# Copyright 2004-2011 Joe Wreschnig, Michael Urman, Iñigo Serna, Nick Boultbee
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7
8import locale
9import re
10
11from quodlibet import _
12
13from .iso639 import ISO_639_2
14
15
16class ValidationError(Exception):
17    pass
18
19
20class Massager(object):
21    """Massage a tag value from various 'okay' formats to the
22    'correct' format."""
23
24    tags = []
25    error = "Metaerror. This should be overridden in subclasses."
26    options = []
27
28    _massagers = {}
29
30    def validate(self, value):
31        """Returns a validated value.
32
33        Raises ValidationError if invalid.
34        """
35
36        raise NotImplementedError
37
38    def is_valid(self, value):
39        """Returns True if a field is valid, False if not"""
40
41        try:
42            self.validate(value)
43        except ValidationError:
44            return False
45        return True
46
47    @classmethod
48    def _register(cls, other):
49        """Register a new massager implementation"""
50
51        assert issubclass(other, Massager)
52        assert other.tags
53        for tag in other.tags:
54            cls._massagers[tag] = other
55        return other
56
57    @classmethod
58    def get_massagers(cls):
59        """Returns all massager subclasses"""
60
61        return set(cls._massagers.values())
62
63    @classmethod
64    def for_tag(cls, tag):
65        """Returns a massager instance for the tag or raises KeyError"""
66
67        return cls._massagers[tag]()
68
69
70def validate(tag, value):
71    """Validate a value based on the tag.
72
73    Raises ValidationError if invalid
74    """
75
76    try:
77        return Massager.for_tag(tag).validate(value)
78    except KeyError:
79        return value
80
81
82def is_valid(tag, value):
83    """Returns True if the fields is valid"""
84
85    try:
86        return Massager.for_tag(tag).is_valid(value)
87    except KeyError:
88        return True
89
90
91def error_message(tag, value):
92    """Returns an error message for invalid tag values"""
93
94    try:
95        return Massager.for_tag(tag).error
96    except KeyError:
97        return u""
98
99
100def get_options(tag):
101    """Returns a list of suggested values for the tag. If the list is empty
102    this either means that the tag is unknown or the set of valid values would
103    be too large"""
104
105    try:
106        return list(Massager.for_tag(tag).options)
107    except KeyError:
108        return []
109
110
111@Massager._register
112class DateMassager(Massager):
113    tags = ["date"]
114    error = _("The date must be entered in 'YYYY', 'YYYY-MM-DD' or "
115              "'YYYY-MM-DD HH:MM:SS' format.")
116    __match = re.compile(r"^\d{4}([-.]\d{2}([-.]\d{2}([T ]\d{2}"
117                         r"([:.]\d{2}([:.]\d{2})?)?)?)?)?$").match
118
119    def validate(self, value):
120        value = value.strip().replace(".", "-").replace("/", "-")
121        if not self.__match(value):
122            raise ValidationError
123        return value
124
125
126@Massager._register
127class GainMassager(Massager):
128    tags = ["replaygain_album_gain", "replaygain_track_gain"]
129    error = _("Replay Gain gains must be entered in 'x.yy dB' format.")
130    __match = re.compile(r"^[+-]\d+\.?\d+?\s+dB$").match
131
132    def validate(self, value):
133        if self.__match(value):
134            return value
135        else:
136            try:
137                f = float(value.split()[0])
138            except (IndexError, TypeError, ValueError):
139                try:
140                    f = locale.atof(value.split()[0])
141                except (IndexError, TypeError, ValueError):
142                    raise ValidationError
143            else:
144                return (u"%+f" % f).rstrip("0") + " dB"
145
146
147@Massager._register
148class PeakMassager(Massager):
149    tags = ["replaygain_album_peak", "replaygain_track_peak"]
150    error = _("Replay Gain peaks must be entered in 'x.yy' format.")
151
152    def validate(self, value):
153        value = value.strip()
154        try:
155            f = float(value)
156        except (TypeError, ValueError):
157            try:
158                f = locale.atof(value)
159            except (TypeError, ValueError):
160                raise ValidationError
161        else:
162            if f < 0 or f >= 2:
163                raise ValidationError
164            return str(f)
165
166
167@Massager._register
168class MBIDMassager(Massager):
169    tags = ["musicbrainz_trackid", "musicbrainz_albumid",
170            "musicbrainz_artistid", "musicbrainz_albumartistid",
171            "musicbrainz_trmid", "musicip_puid"]
172    error = _("MusicBrainz IDs must be in UUID format.")
173
174    def validate(self, value):
175        value = value.encode('ascii', 'replace').decode("ascii")
176        value = u"".join(filter(str.isalnum, value.strip().lower()))
177        try:
178            int(value, 16)
179        except ValueError:
180            raise ValidationError
181        else:
182            if len(value) != 32:
183                raise ValidationError
184            else:
185                return u"-".join([value[:8], value[8:12], value[12:16],
186                                  value[16:20], value[20:]])
187
188
189@Massager._register
190class MBAlbumStatus(Massager):
191    tags = ["musicbrainz_albumstatus"]
192    # Translators: Leave "official", "promotional", and "bootleg"
193    # untranslated. They are the three possible literal values.
194    error = _("MusicBrainz release status must be 'official', "
195              "'promotional', or 'bootleg'.")
196    options = ["official", "promotional", "bootleg"]
197
198    def validate(self, value):
199        if value not in self.options:
200            raise ValidationError
201        return value
202
203
204@Massager._register
205class LanguageMassager(Massager):
206    tags = ["language"]
207    error = _("Language must be an ISO 639-2 three-letter code")
208
209    options = ISO_639_2
210
211    tags = ["language"]
212
213    def validate(self, value):
214        # Issue 439: Actually, allow free-text through
215        return value
216
217    def is_valid(self, value):
218        # Override, to allow empty string to be a valid language (freetext)
219        return True
220