1#!/usr/bin/env python 2# Copyright (c) 2004-2007 Jeremy Evans 3# 4# Permission is hereby granted, free of charge, to any person obtaining a copy 5# of this software and associated documentation files (the "Software"), to deal 6# in the Software without restriction, including without limitation the rights 7# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8# copies of the Software, and to permit persons to whom the Software is 9# furnished to do so, subject to the following conditions: 10# 11# The above copyright notice and this permission notice shall be included in 12# all copies or substantial portions of the Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20# SOFTWARE. 21 22'''Module for manipulating APE and ID3v1 tags 23 24Public Function Arguments 25------------------------- 26fil: filename string OR already opened file or file-like object that supports 27 flush, seek, read, truncate, tell, and write 28fields: dictionary like object of tag fields that has an iteritems method 29 which is an iterator of key, value tuples. 30 APE: 31 key: must be a regular string with length 2-255 inclusive, containing 32 only ASCII characters in the range 0x20-0x7f 33 value: must be a string or a list or tuple of them, or an ApeItem 34 ID3: 35 key: must be title, artist, album, year, comment, genre, or track* 36 (i.e. track or tracknumber) 37 value: should be a string except for track* and genre 38 track*: integer or sting representation of one 39 genre: integer or string (if string, must be a case insensitive 40 match for one of the strings in id3genres to be recognized) 41removefields (updateape and updatetags): iterable of fields to remove from the 42 APE tag (and set to blank in the ID3 tag). 43 44Public Functions Return 45----------------------- 460 on success of delete functions 47bool on success of has functions 48string on success of getraw functions 49dict on success of create, update, replace, modify, or getfields 50 key is the field name as a string 51 (APE) value is an ApeItem, which is a list subclass with the field values 52 stored in the list as strings, and the following special attributes: 53 key: same as key of dict 54 readonly: whether the field was marked read only 55 type: type of tag field (utf8, binary, external, or reserved), 56 utf8 type means values in list are unicode strings 57 (ID3) value is a regular string 58 59Public Functions Raise 60---------------------- 61IOError on problem accessing file (make sure read/write access is allowed 62 for the file if you are trying to modify the tag) 63(APE functions only) UnicodeError on problems converting regular strings to 64 UTF-8 (See note, or just use unicode strings) 65TagError on other errors 66 67Callback Functions 68------------------ 69The modify* functions take callback functions and extra keyword arguments. 70The callback functions are called with the tag dictionary and any extra keyword 71arguments given in the call to modify*. This dictionary should be modified and 72must be returned by the callback functions. There isn't much error checking 73done after this stage, so incorrectly written callback functions may result in 74corrupt tags or exceptions being raised elsewhere in the module. The 75modifytags function takes two separate callback functions, one for the APE tag 76and one for the ID3 tag. See the _update*tagcallback functions for examples of 77how callback functions should be written. 78 79Notes 80----- 81When using functions that modify both tags, the accepted arguments and return 82 value are the same for the APE funtion. 83Raising errors other than IOError, UnicodeError, or TagError is considered a 84 bug unless fields contains a non-basestring (or a list containing a 85 non-basestring). 86Only APEv2 tags are supported. APEv1 tags without a header are not supported. 87Only writes ID3v1.1 tags. Assumes all tags are ID3v1.1. The only exception to 88 this is when it detects an ID3v1.0 tag, it will return 0 as the track 89 number in getfields. 90The APE tag is appended to the end of the file. If the file already has an 91 ID3v1 tag at the end, it is recognized and the APE tag is placed directly 92 before it. 93Default maximum size for the APE tag is 8192 bytes, as recommended by the APE 94 spec. This can be changed by modifying the _maxapesize variable. 95Read-only flags can be read, created, and modified (they are not respected). 96If you are storing non 7-bit ASCII data in a tag, you should pass in unicode 97 strings instead of regular strings, or pass in an already created ApeItem. 98Inserting binary data into tags is "strongly unrecommended." 99This library doesn't check to make sure that tag items marked as external are 100 in the proper format. 101APEv2 specification is here: 102 http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification 103''' 104 105from os.path import isfile as _isfile 106from struct import pack as _pack, unpack as _unpack 107 108# Variable definitions 109 110__version__ = '1.2' 111_maxapesize = 8192 112_commands = '''create update replace delete getfields getrawtag getnewrawtag 113 hastag'''.split() 114_tagmustexistcommands = 'update getfields getrawtag'.split() 115_stringallowedcommands = 'getrawtag getnewrawtag getfields hastag'.split() 116_filelikeattrs = 'flush read seek tell truncate write'.split() 117_badapeitemkeys = 'id3 tag oggs mp+'.split() 118_badapeitemkeychars = ''.join([chr(x) for x in range(32) + range(128,256)]) 119_apeitemtypes = 'utf8 binary external reserved'.split() 120_apeheaderflags = "\x00\x00\xA0" 121_apefooterflags = "\x00\x00\x80" 122_apepreamble = "APETAGEX\xD0\x07\x00\x00" 123_id3tagformat = 'TAG%(title)s%(artist)s%(album)s%(year)s%(comment)s' \ 124 '\x00%(track)s%(genre)s' 125_id3fields = {'title': (3,33), 'artist': (33,63), 'album': (63,93), 126 'year': (93,97), 'comment': (97,125) } # (start, end) 127_id3genresstr = '''Blues, Classic Rock, Country, Dance, Disco, Funk, Grunge, 128 Hip-Hop, Jazz, Metal, New Age, Oldies, Other, Pop, R & B, Rap, Reggae, 129 Rock, Techno, Industrial, Alternative, Ska, Death Metal, Prank, Soundtrack, 130 Euro-Techno, Ambient, Trip-Hop, Vocal, Jazz + Funk, Fusion, Trance, 131 Classical, Instrumental, Acid, House, Game, Sound Clip, Gospel, Noise, 132 Alternative Rock, Bass, Soul, Punk, Space, Meditative, Instrumental Pop, 133 Instrumental Rock, Ethnic, Gothic, Darkwave, Techno-Industrial, Electronic, 134 Pop-Fol, Eurodance, Dream, Southern Rock, Comedy, Cult, Gangsta, Top 40, 135 Christian Rap, Pop/Funk, Jungle, Native US, Cabaret, New Wave, Psychadelic, 136 Rave, Showtunes, Trailer, Lo-Fi, Tribal, Acid Punk, Acid Jazz, Polka, 137 Retro, Musical, Rock & Roll, Hard Rock, Folk, Folk-Rock, National Folk, 138 Swing, Fast Fusion, Bebop, Latin, Revival, Celtic, Bluegrass, Avantgarde, 139 Gothic Rock, Progressive Rock, Psychedelic Rock, Symphonic Rock, Slow Rock, 140 Big Band, Chorus, Easy Listening, Acoustic, Humour, Speech, Chanson, Opera, 141 Chamber Music, Sonata, Symphony, Booty Bass, Primus, Porn Groove, Satire, 142 Slow Jam, Club, Tango, Samba, Folklore, Ballad, Power Ballad, Rhytmic Soul, 143 Freestyle, Duet, Punk Rock, Drum Solo, Acapella, Euro-House, Dance Hall, 144 Goa, Drum & Bass, Club-House, Hardcore, Terror, Indie, BritPop, Negerpunk, 145 Polsk Punk, Beat, Christian Gangsta Rap, Heavy Metal, Black Metal, 146 Crossover, Contemporary Christian, Christian Rock, Merengue, Salsa, 147 Trash Meta, Anime, Jpop, Synthpop''' 148_apeitemkeys = '''Title, Artist, Album, Year, Comment, Genre, Track, 149 Debut Album, Subtitle, Publisher, Conductor, Composer, Copyright, 150 Publicationright, File, EAN/UPC, ISBN, Catalog, LC, Record Date, 151 Record Location, Media, Index, Related, ISRC, Abstract, Language, 152 Bibliography, Introplay, Dummy''' 153id3genres = [x.strip() for x in _id3genresstr.split(',')] 154_id3genresdict = {} 155for i, x in enumerate(id3genres): 156 _id3genresdict[x.lower()] = i 157apeitemkeys = [x.strip() for x in _apeitemkeys.split(',')] 158del x 159del i 160 161# Classes 162 163class TagError(StandardError): 164 '''Raised when there is an error during a tagging operation''' 165 pass 166 167class ApeItem(list): 168 '''Contains individual APE tag items''' 169 def __init__(self, key = None, values = [], type = 'utf8', readonly = False): 170 list.__init__(self) 171 if key is None: 172 return 173 if not self.validkey(key): 174 raise TagError, 'Invalid item key for ape tag item: %r' % key 175 if type not in _apeitemtypes: 176 raise TagError, 'Invalid item type for ape tag item: %r' % type 177 self.key = key 178 self.readonly = bool(readonly) 179 self.type = type 180 if isinstance(values, basestring): 181 values = [values] 182 if type == 'utf8' or type == 'external': 183 values = [unicode(value) for value in values] 184 self.extend(values) 185 186 def maketag(self): 187 '''Return on disk representation of tag item 188 189 self.parsetag(self.maketag(), 0) should result in no change to self 190 ''' 191 if self.type == 'utf8' or self.type == 'external': 192 values = '\x00'.join([value.encode('utf8') for value in self]) 193 else: 194 values = '\x00'.join(self) 195 size = _pack("<i",len(values)) 196 flags = chr(int(self.readonly) + 2 * (_apeitemtypes.index(self.type))) 197 return '%s\x00\x00\x00%s%s\x00%s' % (size, flags, self.key, values) 198 199 def parsetag(self, data, curpos): 200 '''Parse next tag from data string, starting at current position''' 201 del self[:] 202 itemlength = _unpack("<i",data[curpos:curpos+4])[0] 203 if itemlength < 0: 204 raise TagError, 'Corrupt tag, invalid item length at position ' \ 205 '%i: %i bytes' % (curpos, itemlength) 206 if data[curpos+4:curpos+7] != '\x00\x00\x00': 207 raise TagError, 'Corrupt tag, invalid item flags, bits 8-31 ' \ 208 'nonzero at position %i' % curpos 209 type, readonly = divmod(ord(data[curpos+7]), 2) 210 if type > 3: 211 raise TagError, 'Corrupt tag, invalid item flags, bits 3-7 ' \ 212 'nonzero at position %i' % curpos 213 self.type = _apeitemtypes[type] 214 self.readonly = bool(readonly) 215 curpos += 8 216 keyend = data.find("\x00", curpos) 217 if keyend < curpos: 218 raise TagError, 'Corrupt tag, unterminated item key at position ' \ 219 '%i' % curpos 220 itemkey = data[curpos:keyend] 221 if not self.validkey(itemkey): 222 raise TagError, 'Corrupt tag, invalid item key at position ' \ 223 '%i: %r' % (curpos, itemkey) 224 self.key = itemkey 225 curpos = keyend + itemlength + 1 226 itemvalue = data[keyend+1:curpos] 227 if self.type == 'utf8' or self.type == 'external': 228 self.extend(itemvalue.decode('utf8').split('\x00')) 229 else: 230 self.append(itemvalue) 231 return curpos 232 233 def validkey(self, key): 234 '''Check key to make sure it is a valid ApeItem key''' 235 return isinstance(key, str) and 2 <= len(key) <= 255 \ 236 and not _stringoverlaps(key, _badapeitemkeychars) \ 237 and key.lower() not in _badapeitemkeys 238 239# Private functions 240 241def _ape(fil, action, callback = None, callbackkwargs = {}, updateid3 = False): 242 '''Get or Modify APE tag for file''' 243 apesize = 0 244 tagstart = None 245 filesize, id3data, data = _getfilesizeandid3andapefooter(fil) 246 247 if _apepreamble != data[:12]: 248 if action in _tagmustexistcommands: 249 raise TagError, "Nonexistant or corrupt tag, can't %s" % action 250 elif action == "delete": 251 return 0 252 data = '' 253 tagstart = filesize - len(id3data) 254 elif _apefooterflags != data[21:24] or \ 255 (data[20] != '\0' and data[20] != '\1'): 256 raise TagError, "Bad tag footer flags" 257 else: 258 # file has a valid APE footer 259 apesize = _unpack("<i",data[12:16])[0] + 32 260 if apesize > _maxapesize: 261 raise TagError, 'Existing tag is too large: %i bytes' % apesize 262 if apesize + len(id3data) > filesize: 263 raise TagError, 'Existing tag says it is larger than the file: ' \ 264 '%i bytes' % apesize 265 fil.seek(-apesize - len(id3data), 2) 266 tagstart = fil.tell() 267 data = fil.read(apesize) 268 if _apepreamble != data[:12] or _apeheaderflags != data[21:24] or \ 269 (data[20] != '\0' and data[20] != '\1'): 270 raise TagError, 'Nonexistent or corrupt tag, missing tag header' 271 if apesize != _unpack("<i",data[12:16])[0] + 32: 272 raise TagError, 'Corrupt tag, header and footer sizes do not match' 273 if action == "delete": 274 fil.seek(tagstart) 275 if not updateid3: 276 fil.write(id3data) 277 fil.truncate() 278 fil.flush() 279 return 0 280 281 if action == "hastag": 282 if updateid3: 283 return bool(data) and bool(id3data) 284 return bool(data) 285 if action == "getrawtag": 286 if updateid3: 287 return data, id3data 288 return data 289 if action == "getfields": 290 if updateid3: 291 return _restoredictcase(_parseapetag(data)), \ 292 _id3(id3data, "getfields") 293 return _restoredictcase(_parseapetag(data)) 294 if not data or action == "replace": 295 apeitems = {} 296 else: 297 apeitems = _parseapetag(data) 298 299 if callable(callback): 300 apeitems = callback(apeitems, **callbackkwargs) 301 302 newtag = _makeapev2tag(apeitems) 303 304 if action == "getnewrawtag": 305 if updateid3: 306 return newtag, _id3(id3data, "getnewrawtag") 307 return newtag 308 309 if len(newtag) > _maxapesize: 310 raise TagError, 'New tag is too large: %i bytes' % len(data) 311 312 if updateid3: 313 if action == 'replace': 314 id3data = '' 315 elif action != 'create' and not id3data: 316 raise TagError, "Nonexistant or corrupt tag, can't %s" % action 317 if callable(updateid3): 318 id3data = _id3(id3data, "getnewrawtag", updateid3, callbackkwargs) 319 else: 320 callbackkwargs['convertfromape'] = True 321 id3data = _id3(id3data, "getnewrawtag", _updateid3tagcallback, 322 callbackkwargs) 323 324 fil.seek(tagstart) 325 fil.write(newtag + id3data) 326 fil.truncate() 327 fil.flush() 328 return _restoredictcase(apeitems) 329 330def _apefieldstoid3fields(fields): 331 '''Convert APE tag fields to ID3 tag fields ''' 332 id3fields = {} 333 for key, value in fields.iteritems(): 334 key = key.lower() 335 if isinstance(value, (list, tuple)): 336 if not value: 337 value = '' 338 else: 339 value = ', '.join(value) 340 if key.startswith('track'): 341 try: 342 value = int(value) 343 except ValueError: 344 value = 0 345 if (0 <= value < 256): 346 id3fields['track'] = value 347 else: 348 id3fields['track'] = 0 349 elif key == 'genre': 350 if isinstance(value, basestring) and value.lower() in _id3genresdict: 351 id3fields[key] = value 352 else: 353 id3fields[key] = '' 354 elif key == 'date': 355 try: 356 id3fields['year'] = str(int(value)) 357 except ValueError: 358 pass 359 elif key in _id3fields: 360 if isinstance(value, unicode): 361 value = value.encode('utf8') 362 id3fields[key] = value 363 return id3fields 364 365_apelengthreduce = lambda i1, i2: i1 + len(i2) 366 367def _checkargs(fil, action): 368 '''Check that arguments are valid, convert them, or raise an error''' 369 if not (isinstance(action,str) and action.lower() in _commands): 370 raise TagError, "%r is not a valid action" % action 371 action = action.lower() 372 fil = _getfileobj(fil, action) 373 for attr in _filelikeattrs: 374 if not hasattr(fil, attr) or not callable(getattr(fil, attr)): 375 raise TagError, "fil does not support method %r" % attr 376 return fil, action 377 378def _checkfields(fields): 379 '''Check that the fields quacks like a dict''' 380 if not hasattr(fields, 'items') or not callable(fields.items): 381 raise TagError, "fields does not support method 'items'" 382 383def _checkremovefields(removefields): 384 '''Check that removefields is iterable''' 385 if not hasattr(removefields, '__iter__') \ 386 or not callable(removefields.__iter__): 387 raise TagError, "removefields is not an iterable" 388 389def _getfileobj(fil, action): 390 '''Return a file object if given a filename, otherwise return file''' 391 if isinstance(fil, basestring) and _isfile(fil): 392 if action in _stringallowedcommands: 393 mode = 'rb' 394 else: 395 mode = 'r+b' 396 return file(fil, mode) 397 return fil 398 399def _getfilesizeandid3andapefooter(fil): 400 '''Return file size and ID3 tag if it exists, and seek to start of APE footer''' 401 fil.seek(0, 2) 402 filesize = fil.tell() 403 id3 = '' 404 apefooter = '' 405 if filesize < 64: #No possible APE or ID3 tag 406 apefooter = '' 407 elif filesize < 128: #No possible ID3 tag 408 fil.seek(filesize - 32) 409 apefooter = fil.read(32) 410 else: 411 fil.seek(filesize - 128) 412 data = fil.read(128) 413 if data[:3] != 'TAG': 414 apefooter = data[96:] 415 else: 416 id3 = data 417 if filesize >= 160: 418 fil.seek(filesize - 160) 419 apefooter = fil.read(32) 420 return filesize, id3, apefooter 421 422def _id3(fil, action, callback = None, callbackkwargs={}): 423 '''Get or Modify ID3 tag for file''' 424 if isinstance(fil, str): 425 if action not in _stringallowedcommands: 426 raise TagError, "String not allowed for %s action" % action 427 data = fil 428 else: 429 fil.seek(0, 2) 430 tagstart = fil.tell() 431 if tagstart < 128: 432 data = '' 433 else: 434 fil.seek(-128,2) 435 data = fil.read(128) 436 if data[0:3] != 'TAG': 437 # Tag doesn't exist 438 if action == "delete": 439 return 0 440 if action in _tagmustexistcommands: 441 raise TagError, "Nonexistant or corrupt tag, can't %s" % action 442 data = '' 443 else: 444 tagstart -= 128 445 if action == "delete": 446 fil.truncate(tagstart) 447 return 0 448 449 if action == "hastag": 450 return bool(data) 451 if action == "getrawtag": 452 return data 453 if action == "getfields": 454 return _parseid3tag(data) 455 456 if not data or action == "replace": 457 tagfields = {} 458 else: 459 tagfields = _parseid3tag(data) 460 461 if callable(callback): 462 tagfields = callback(tagfields, **callbackkwargs) 463 464 newtag = _makeid3tag(tagfields) 465 466 if action == "getnewrawtag": 467 return newtag 468 469 fil.seek(tagstart) 470 fil.write(newtag) 471 fil.flush() 472 return _parseid3tag(newtag) 473 474def _makeapev2tag(apeitems): 475 '''Construct an APE tag string from a dict of ApeItems''' 476 apeentries = [item.maketag() for item in apeitems.itervalues()] 477 apeentries.sort(_sortapeitems) 478 apesize = _pack("<i",reduce(_apelengthreduce, apeentries, 32)) 479 numitems = _pack("<i",len(apeentries)) 480 headerfooter = _apepreamble + apesize + numitems 481 apeentries.insert(0, headerfooter + '\0' + _apeheaderflags + "\x00" * 8) 482 apeentries.append(headerfooter + '\0' + _apefooterflags + "\x00" * 8) 483 return "".join(apeentries) 484 485def _makeid3tag(fields): 486 '''Make an ID3 tag from the given dictionary''' 487 newfields = {} 488 for field, value in fields.iteritems(): 489 if not isinstance(field, str): 490 continue 491 newfields[field.lower()] = fields[field] 492 field = field.lower() 493 if field.startswith('track'): 494 try: 495 if not value: 496 value = 0 497 newfields['track'] = chr(int(value)) 498 except ValueError: 499 raise TagError, '%r is an invalid value for %r' % (value, field) 500 elif field == 'genre': 501 if not isinstance(value, int): 502 if not isinstance(value, basestring): 503 raise TagError, "%r is an invalid value for 'genre'" % value 504 value = value.lower() 505 if not value: 506 value = 255 507 elif value in _id3genresdict: 508 value = _id3genresdict[value] 509 else: 510 raise TagError, "%r is an invalid value for 'genre'" % value 511 elif not (0 <= value < 256): 512 value = 255 513 newfields[field] = chr(value) 514 for field, (startpos, endpos) in _id3fields.iteritems(): 515 maxlength = endpos - startpos 516 if field in newfields: 517 fieldlength = len(newfields[field]) 518 if fieldlength > maxlength: 519 newfields[field] = newfields[field][:maxlength] 520 elif fieldlength < maxlength: 521 newfields[field] = newfields[field] + \ 522 '\x00' * (maxlength - fieldlength) 523 # If fieldlength = maxlength, no changes need to be made 524 else: 525 newfields[field] = '\x00' * maxlength 526 if 'track' not in newfields: 527 newfields['track'] = '\x00' 528 if 'genre' not in newfields: 529 newfields['genre'] = '\xff' 530 return _id3tagformat % newfields 531 532def _parseapetag(data): 533 '''Parse an APEv2 tag and return a dictionary of tag fields''' 534 apeitems = {} 535 numitems = _unpack("<i",data[16:20])[0] 536 if numitems != _unpack("<i",data[-16:-12])[0]: 537 raise TagError, 'Corrupt tag, mismatched header and footer item count' 538 # 32 is size of footer, 11 is minimum item length item 539 if numitems > (len(data) - 32)/11: 540 raise TagError, 'Corrupt tag, specifies more items that is possible ' \ 541 'given space remaining: %i items' % numitems 542 curpos = 32 543 tagitemend = len(data) - 32 544 for x in range(numitems): 545 if curpos >= tagitemend: 546 raise TagError, 'Corrupt tag, end of tag reached with more items' \ 547 'specified' 548 item = ApeItem() 549 curpos = item.parsetag(data, curpos) 550 itemkey = item.key.lower() 551 if itemkey in apeitems: 552 raise TagError, 'Corrupt tag, duplicate item key: %r' % itemkey 553 apeitems[itemkey] = item 554 if tagitemend - curpos: 555 raise TagError, 'Corrupt tag, parsing complete but not at end ' \ 556 'of input: %i bytes remaining' % (len(data) - curpos) 557 return apeitems 558 559def _parseid3tag(data): 560 '''Parse an ID3 tag and return a dictionary of tag fields''' 561 fields = {} 562 for key,(start,end) in _id3fields.iteritems(): 563 fields[key] = data[start:end].rstrip("\x00") 564 if data[125] == "\x00": 565 # ID3v1.1 tags have tracks 566 fields["track"] = str(ord(data[126])) 567 else: 568 fields["track"] = '0' 569 genreid = ord(data[127]) 570 if genreid < len(id3genres): 571 fields["genre"] = id3genres[genreid] 572 else: 573 fields["genre"] = '' 574 return fields 575 576def _printapeitems(apeitems): 577 '''Pretty print given APE Items''' 578 items = apeitems.items() 579 items.sort() 580 print 'APE Tag\n-------' 581 for key, value in items: 582 if value.readonly: 583 key = '[read only] %s' % key 584 if value.type == 'utf8': 585 value = u', '.join([v.encode('ascii', 'replace') for v in value]) 586 else: 587 key = '[%s] %s' % (value.type, key) 588 if value.type == 'binary': 589 value = '[binary data]' 590 else: 591 value = ', '.join(value) 592 print '%s: %s' % (key, value) 593 594def _printid3items(tagfields): 595 '''Pretty print given ID3 Fields''' 596 items = tagfields.items() 597 items.sort() 598 print 'ID3 Tag\n-------' 599 for key, value in items: 600 if value: 601 print '%s: %s' % (key, value) 602 603def _removeapeitems(apeitems, removefields): 604 '''Remove items from the APE tag''' 605 for key in [key.lower() for key in removefields if hasattr(key, 'lower')]: 606 if key in apeitems: 607 del apeitems[key] 608 609def _restoredictcase(apeitems): 610 '''Restore the case of the dictionary keys for the ApeItems''' 611 fixeditems = {} 612 for value in apeitems.itervalues(): 613 fixeditems[value.key] = value 614 return fixeditems 615 616def _stringoverlaps(string1, string2): 617 '''Check if any character in either string is in the other string''' 618 if len(string1) > len(string2): 619 string1, string2 = string2, string1 620 for char in string1: 621 if char in string2: 622 return True 623 return False 624 625_sortapeitems = lambda a, b: cmp(len(a), len(b)) 626 627def _tag(function, fil, action="update", *args, **kwargs): 628 '''Preform tagging operation, check args, open/close file if necessary''' 629 origfil = fil 630 fil, action = _checkargs(fil, action) 631 if 'callbackkwargs' in kwargs: 632 if 'fields' in kwargs['callbackkwargs']: 633 _checkfields(kwargs['callbackkwargs']['fields']) 634 try: 635 return function(fil, action, *args, **kwargs) 636 finally: 637 if isinstance(origfil, basestring): 638 # filename given as an argument, close file object 639 fil.close() 640 641def _updateapeitems(apeitems, fields): 642 '''Add/Update apeitems using data from fields''' 643 for key, value in fields.iteritems(): 644 if isinstance(value, ApeItem): 645 apeitems[value.key.lower()] = value 646 else: 647 apeitems[key.lower()] = ApeItem(key, value) 648 return apeitems 649 650def _updateapetagcallback(apeitems, fields={}, removefields=[]): 651 '''Add and/or remove fields from the apeitems''' 652 if removefields: 653 _removeapeitems(apeitems, removefields) 654 return _updateapeitems(apeitems, fields) 655 656def _updateid3fields(tagfields, fields): 657 '''Update ID3v1 tagfields using fields''' 658 for field, value in fields.iteritems(): 659 if isinstance(field, str): 660 tagfields[field.lower()] = value 661 return tagfields 662 663def _updateid3tagcallback(tagfields, fields={}, removefields=[], 664 convertfromape = False): 665 '''Add and/or remove fields from the ID3v1 tagfields''' 666 if convertfromape: 667 fields = _apefieldstoid3fields(fields) 668 for field in removefields: 669 if field.lower() in tagfields: 670 tagfields[field.lower()] = '' 671 return _updateid3fields(tagfields, fields) 672 673# Public functions 674 675def createape(fil, fields = {}): 676 '''Create/update APE tag in fil with the information in fields''' 677 return _tag(_ape, fil, 'create', callback=_updateapetagcallback, 678 callbackkwargs={'fields':fields}) 679 680def createid3(fil, fields = {}): 681 '''Create/update ID3v1 tag in fil with the information in fields''' 682 return _tag(_id3, fil, 'create', callback=_updateid3tagcallback, 683 callbackkwargs={'fields':fields}) 684 685def createtags(fil, fields = {}): 686 '''Create/update both APE and ID3v1 tags on fil with the information in fields''' 687 return _tag(_ape, fil, 'create', callback=_updateapetagcallback, 688 callbackkwargs={'fields':fields}, updateid3=True) 689 690def deleteape(fil): 691 '''Delete APE tag from fil if it exists''' 692 return _tag(_ape, fil, action='delete') 693 694def deleteid3(fil): 695 '''Delete ID3v1 tag from fil if it exists''' 696 return _tag(_id3, fil, action='delete') 697 698def deletetags(fil): 699 '''Delete APE and ID3v1 tags from fil if either exists''' 700 deleteid3(fil) 701 return _tag(_ape, fil, action='delete', updateid3=True) 702 703def getapefields(fil): 704 '''Return fields from APE tag in fil''' 705 return _tag(_ape, fil, action='getfields') 706 707def getid3fields(fil): 708 '''Return fields from ID3v1 tag in fil (including blank fields)''' 709 return _tag(_id3, fil, action='getfields') 710 711def gettagfields(fil): 712 '''Get APE and ID3v1 tag fields tuple''' 713 return _tag(_ape, fil, action='getfields', updateid3=True) 714 715def getrawape(fil): 716 '''Return raw APE tag from fil''' 717 return _tag(_ape, fil, action='getrawtag') 718 719def getrawid3(fil): 720 '''Return raw ID3v1 tag from fil''' 721 return _tag(_id3, fil, action='getrawtag') 722 723def getrawtags(fil): 724 '''Get raw APE and ID3v1 tag tuple''' 725 return _tag(_ape, fil, action='getrawtag', updateid3=True) 726 727def hasapetag(fil): 728 '''Return raw APE tag from fil''' 729 return _tag(_ape, fil, action='hastag') 730 731def hasid3tag(fil): 732 '''Return raw ID3v1 tag from fil''' 733 return _tag(_id3, fil, action='hastag') 734 735def hastags(fil): 736 '''Get raw APE and ID3v1 tag tuple''' 737 return _tag(_ape, fil, action='hastag', updateid3=True) 738 739def modifyape(fil, callback, action='update', **kwargs): 740 '''Modify APE tag using user-defined callback and kwargs''' 741 return _tag(_ape, fil, action=action, callback=callback, 742 callbackkwargs=kwargs) 743 744def modifyid3(fil, callback, action='update', **kwargs): 745 '''Modify ID3v1 tag using user-defined callback and kwargs''' 746 return _tag(_id3, fil, action=action, callback=callback, 747 callbackkwargs=kwargs) 748 749def modifytags(fil, apecallback, id3callback=True, action='update', **kwargs): 750 '''Modify APE and ID3v1 tags using user-defined callbacks and kwargs 751 752 Both apecallback and id3callback receive the same kwargs provided, so they 753 need to have the same interface. 754 ''' 755 return _tag(_ape, fil, action=action, callback=apecallback, 756 updateid3=id3callback, callbackkwargs=kwargs) 757 758def printapetag(fil): 759 '''Print APE tag fields for fil''' 760 _printapeitems(getapefields(fil)) 761 762def printid3tag(fil): 763 '''Print ID3 tag fields for fil''' 764 _printid3items(getid3fields(fil)) 765 766def printtags(fil): 767 '''Print APE and ID3 tag fields for fil''' 768 apeitems, tagfields = gettagfields(fil) 769 _printapeitems(apeitems) 770 _printid3items(tagfields) 771 772def replaceape(fil, fields = {}): 773 '''Replace/create APE tag in fil with the information in fields''' 774 return _tag(_ape, fil, 'replace', callback=_updateapetagcallback, 775 callbackkwargs={'fields':fields}) 776 777def replaceid3(fil, fields = {}): 778 '''Replace/create ID3v1 tag in fil with the information in fields''' 779 return _tag(_id3, fil, 'replace', callback=_updateid3tagcallback, 780 callbackkwargs={'fields':fields}) 781 782def replacetags(fil, fields = {}): 783 '''Replace/create both APE and ID3v1 tags on fil with the information in fields''' 784 return _tag(_ape, fil, 'replace', callback=_updateapetagcallback, 785 callbackkwargs={'fields':fields}, updateid3=True) 786 787def updateape(fil, fields = {}, removefields = []): 788 '''Update APE tag in fil with the information in fields''' 789 _checkremovefields(removefields) 790 return _tag(_ape, fil, 'update', callback=_updateapetagcallback, 791 callbackkwargs={'fields':fields, 'removefields':removefields}) 792 793def updateid3(fil, fields = {}): 794 '''Update ID3v1 tag in fil with the information in fields''' 795 return _tag(_id3, fil, 'update', callback=_updateid3tagcallback, 796 callbackkwargs={'fields':fields}) 797 798def updatetags(fil, fields = {}, removefields = []): 799 '''Update both APE and ID3v1 tags on fil with the information in fields''' 800 _checkremovefields(removefields) 801 return _tag(_ape, fil, 'update', callback=_updateapetagcallback, 802 callbackkwargs={'fields':fields, 'removefields':removefields}, 803 updateid3=True) 804 805if __name__ == '__main__': 806 import sys 807 for filename in sys.argv[1:]: 808 if _isfile(filename): 809 print '\n%s' % filename 810 try: 811 printtags(filename) 812 except TagError: 813 print 'Missing APE or ID3 Tag' 814 else: 815 print "%s: file doesn't exist" % filename 816