1#!/usr/bin/python 2 3# Audio Tools, a module and set of tools for manipulating audio data 4# Copyright (C) 2007-2014 Brian Langenberger 5 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 20from audiotools import MetaData 21 22 23class ID3v1Comment(MetaData): 24 """a complete ID3v1.1 tag""" 25 26 ID3v1_FIELDS = {"track_name": "__track_name__", 27 "artist_name": "__artist_name__", 28 "album_name": "__album_name__", 29 "year": "__year__", 30 "comment": "__comment__", 31 "track_number": "__track_number__"} 32 33 FIELD_LENGTHS = {"track_name": 30, 34 "artist_name": 30, 35 "album_name": 30, 36 "year": 4, 37 "comment": 28} 38 39 def __init__(self, track_name=u"", 40 artist_name=u"", 41 album_name=u"", 42 year=u"", 43 comment=u"", 44 track_number=0, 45 genre=0): 46 """fields are as follows: 47 48 | field | length | 49 |--------------+--------| 50 | track_name | 30 | 51 | artist_name | 30 | 52 | album_name | 30 | 53 | year | 4 | 54 | comment | 28 | 55 | track_number | 1 | 56 | genre | 1 | 57 |--------------+--------| 58 59 track_name, artist_name, album_name, year and 60 comment are unicode strings 61 62 track_number and genre are integers 63 """ 64 65 if len(track_name) > 30: 66 raise ValueError("track_name cannot be longer than 30 characters") 67 if len(artist_name) > 30: 68 raise ValueError("artist_name cannot be longer than 30 characters") 69 if len(album_name) > 30: 70 raise ValueError("album_name cannot be longer than 30 characters") 71 if len(year) > 4: 72 raise ValueError("year cannot be longer than 4 characters") 73 if len(comment) > 28: 74 raise ValueError("comment cannot be longer than 28 characters") 75 76 MetaData.__setattr__(self, "__track_name__", track_name) 77 MetaData.__setattr__(self, "__artist_name__", artist_name) 78 MetaData.__setattr__(self, "__album_name__", album_name) 79 MetaData.__setattr__(self, "__year__", year) 80 MetaData.__setattr__(self, "__comment__", comment) 81 MetaData.__setattr__(self, "__track_number__", track_number) 82 MetaData.__setattr__(self, "__genre__", genre) 83 84 def __repr__(self): 85 return "ID3v1Comment(%s, %s, %s, %s, %s, %s, %s)" % \ 86 (repr(self.__track_name__), 87 repr(self.__artist_name__), 88 repr(self.__album_name__), 89 repr(self.__year__), 90 repr(self.__comment__), 91 repr(self.__track_number__), 92 repr(self.__genre__)) 93 94 def __getattr__(self, attr): 95 if attr == "track_number": 96 number = self.__track_number__ 97 if number > 0: 98 return number 99 else: 100 return None 101 elif attr in self.ID3v1_FIELDS: 102 value = getattr(self, self.ID3v1_FIELDS[attr]) 103 if len(value) > 0: 104 return value 105 else: 106 return None 107 elif attr in self.FIELDS: 108 return None 109 else: 110 return MetaData.__getattribute__(self, attr) 111 112 def __setattr__(self, attr, value): 113 if attr == "track_number": 114 MetaData.__setattr__( 115 self, 116 "__track_number__", 117 min(0 if (value is None) else int(value), 0xFF)) 118 elif attr in self.FIELD_LENGTHS: 119 if value is None: 120 delattr(self, attr) 121 else: 122 # all are text fields 123 MetaData.__setattr__( 124 self, 125 self.ID3v1_FIELDS[attr], 126 value[0:self.FIELD_LENGTHS[attr]]) 127 elif attr in self.FIELDS: 128 # field not supported by ID3v1Comment, so ignore it 129 pass 130 else: 131 MetaData.__setattr__(self, attr, value) 132 133 def __delattr__(self, attr): 134 if attr == "track_number": 135 MetaData.__setattr__(self, "__track_number__", 0) 136 elif attr in self.FIELD_LENGTHS: 137 MetaData.__setattr__(self, 138 self.ID3v1_FIELDS[attr], 139 u"") 140 elif attr in self.FIELDS: 141 # field not supported by ID3v1Comment, so ignore it 142 pass 143 else: 144 MetaData.__delattr__(self, attr) 145 146 def raw_info(self): 147 """returns a human-readable version of this metadata 148 as a unicode string""" 149 150 from os import linesep 151 152 return linesep.join( 153 [u"ID3v1.1:"] + 154 [u"%s = %s" % (label, getattr(self, attr)) 155 for (label, attr) in [(u" track name", "track_name"), 156 (u" artist name", "artist_name"), 157 (u" album name", "album_name"), 158 (u" year", "year"), 159 (u" comment", "comment"), 160 (u"track number", "track_number")] 161 if (getattr(self, attr) is not None)] + 162 [u" genre = %d" % (self.__genre__)]) 163 164 @classmethod 165 def parse(cls, mp3_file): 166 """given an MP3 file, returns an ID3v1Comment 167 168 raises ValueError if the comment is invalid""" 169 170 from audiotools.bitstream import parse 171 172 def decode_string(s): 173 return s.rstrip(b"\x00").decode("ascii", "replace") 174 175 mp3_file.seek(-128, 2) 176 (tag, 177 track_name, 178 artist_name, 179 album_name, 180 year, 181 comment, 182 track_number, 183 genre) = parse("3b 30b 30b 30b 4b 28b 8p 8u 8u", 184 False, 185 mp3_file.read(128)) 186 if tag != b'TAG': 187 raise ValueError(u"invalid ID3v1 tag") 188 189 return ID3v1Comment(track_name=decode_string(track_name), 190 artist_name=decode_string(artist_name), 191 album_name=decode_string(album_name), 192 year=decode_string(year), 193 comment=decode_string(comment), 194 track_number=track_number, 195 genre=genre) 196 197 def build(self, mp3_file): 198 """given an MP3 file positioned at the file's end, generate a tag""" 199 200 from audiotools.bitstream import build 201 202 def encode_string(u, max_chars): 203 s = u.encode("ascii", "replace") 204 if len(s) >= max_chars: 205 return s[0:max_chars] 206 else: 207 return s + b"\x00" * (max_chars - len(s)) 208 209 mp3_file.write( 210 build("3b 30b 30b 30b 4b 28b 8p 8u 8u", 211 False, 212 (b"TAG", 213 encode_string(self.__track_name__, 30), 214 encode_string(self.__artist_name__, 30), 215 encode_string(self.__album_name__, 30), 216 encode_string(self.__year__, 4), 217 encode_string(self.__comment__, 28), 218 self.__track_number__, 219 self.__genre__))) 220 221 @classmethod 222 def supports_images(cls): 223 """returns False""" 224 225 return False 226 227 @classmethod 228 def converted(cls, metadata): 229 """converts a MetaData object to an ID3v1Comment object""" 230 231 if metadata is None: 232 return None 233 elif isinstance(metadata, ID3v1Comment): 234 # duplicate all fields as-is 235 return ID3v1Comment(track_name=metadata.__track_name__, 236 artist_name=metadata.__artist_name__, 237 album_name=metadata.__album_name__, 238 year=metadata.__year__, 239 comment=metadata.__comment__, 240 track_number=metadata.__track_number__, 241 genre=metadata.__genre__) 242 else: 243 # convert fields using setattr 244 id3v1 = ID3v1Comment() 245 for attr in ["track_name", 246 "artist_name", 247 "album_name", 248 "year", 249 "comment", 250 "track_number"]: 251 setattr(id3v1, attr, getattr(metadata, attr)) 252 return id3v1 253 254 def images(self): 255 """returns an empty list of Image objects""" 256 257 return [] 258 259 def clean(self): 260 import sys 261 262 """returns a new ID3v1Comment object that's been cleaned of problems""" 263 264 from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, 265 CLEAN_REMOVE_LEADING_WHITESPACE) 266 267 fixes_performed = [] 268 fields = {} 269 for (attr, 270 name) in [("track_name", u"title"), 271 ("artist_name", u"artist"), 272 ("album_name", u"album"), 273 ("year", u"year"), 274 ("comment", u"comment")]: 275 # strip out trailing NULL bytes 276 initial_value = getattr(self, attr) 277 278 if initial_value is not None: 279 fix1 = initial_value.rstrip() 280 if fix1 != initial_value: 281 fixes_performed.append(CLEAN_REMOVE_TRAILING_WHITESPACE % 282 {"field": name}) 283 fix2 = fix1.lstrip() 284 if fix2 != fix1: 285 fixes_performed.append(CLEAN_REMOVE_LEADING_WHITESPACE % 286 {"field": name}) 287 288 # restore trailing NULL bytes 289 fields[attr] = fix2 290 291 # copy non-text fields as-is 292 return (ID3v1Comment(track_number=self.__track_number__, 293 genre=self.__genre__, 294 **fields), fixes_performed) 295