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