#!/usr/bin/python ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # 2001-apr-21 first version by Kristian Kvilekval kris@cs.ucsb.edu # 2001-july-26 0.2 version by Kristian Kvilekval kris@cs.ucsb.edu # # TODO # Intgrate with id3v2 """name2id3 generate a tag from a filename (by Kristian Kvilekval) Tag an mp3 using a filename. Extracts ID3 data from a filename for tagging using a format string. A format string is fixed(or regular expression) string extended with the following tags: %artist% %album% %year% %comment% %track% %genre% Tag groups of file based on the format string Example: to match a filename 'Jean Michel Jarre - Oxygene 1.mp3' '%artist% - %album% %track%' """ # import os, sys, string, re, getopt # Specify the command to use # For now use an external id3 program # (Change when python id3v2 library is available) # Both of these have similar command arguments : *** id3fields = [ 'artist', 'album' , 'title' , 'genre' , 'year' , 'track' , 'comment' ] # Some constants mp3ext = r'\.[mM][pP]3' id3rexps = { '%artist%' : r'(?P[\w\s\.\,]+)', '%title%' : r'(?P.+)', '%album%' : r'(?P<album>.+)', '%comment%' : r'(?P<comment>.*?)', '%genre%' : r'(?P<genre>[\w\s\.]+)', '%track%' : r'(?P<track>\d+)', '%year%' : r'(?P<year>\d+)', '%skip%' : r'.+', } genres = [ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "Alt. Rock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrum. Pop", "Instrum. Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Indust.", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing", "Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progress. Rock", "Psychadel. Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A Capella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", "Synthpop" ] # Globals version = 0.2 program = "" verbose = 0 def build_re(template, substitution): """Build a regular expression substiting elements in the template""" rex = template for field in substitution.keys() : rex = string.replace(rex, field, substitution[field]) rex = '^' + rex + '$' if verbose: print "Created : " + rex return rex def re_escape(rex): """Escape regular expression special characters""" escaped = "" for ch in rex: if ch in r'^$[]\+*?.(){},|' : escaped = escaped + '\\' + ch else: escaped = escaped + ch return escaped def strip_padding(s): l = len(s) i = 0 while i < l: if s[i] == chr(0): break i = i+1 s = s[:i] l = len(s)-1 while l >= 0: if s[l] in string.whitespace: l = l -1 else: break s = s[:l+1] return s def lengthen(string, num_spaces): string = string[:num_spaces] return string + (' ' * (num_spaces - len(string))) def get_genrestring (g): """Turn a character code into the full genre string""" if ord(g) < len(genres): return genres[ord(g)] return "Unknown" def get_genrecode (name): """Turn a full genre string into a character code """ lname = string.lower(name) for g in genres: if string.lower(g) == lname: return chr(genres.index(g)) return chr(255) def get_id3info (filename): """get id3 info in the form of a map from the file""" info = {} try: file = open(filename, 'r') file.seek(-128, 2) except IOError, msg: print msg return try: if file.read(3) == 'TAG': info['title'] = strip_padding(file.read(30)) info['artist'] = strip_padding(file.read(30)) info['album'] = strip_padding(file.read(30)) info['year'] = strip_padding(file.read(4)) comment = info['comment'] = file.read(30) if ord(comment[-2]) == 0 and ord(comment[-1]) != 0: info['track'] = str(ord (comment[-1])) info['comment'] = comment[:-2] info['genre'] = get_genrestring(file.read(1)) file.close() except IOError, msg: print msg # Remove blank tags for k in info.keys(): if not info[k]: del info[k] return info def set_id3info (filename, info): for k in id3fields: if k != 'track' and not info.has_key(k): info[k] = "" if verbose: print info try: file = open(filename, 'r+') file.seek(-128, 2) if file.read(3) == "TAG": file.seek(-128, 2) else: file.seek(0, 2) file.write('TAG') file.write(lengthen(info['title'], 30)) file.write(lengthen(info['artist'], 30)) file.write(lengthen(info['album'], 30)) file.write(lengthen(info['year'], 4)) comment = lengthen(info['comment'], 30) if info.has_key('track'): comment = comment[:-2] + "\0" + chr(int(info['track'])) file.write(comment) file.write(get_genrecode(info['genre'])) file.close() except IOError, msg: print msg def safe_name(name): """Make a filename safe for use (remove some special chars)""" escaped = "" for ch in name: if ch not in r'$[]/\*?();{}|\'"&.': escaped = escaped + ch if not escaped: return '""' return escaped def build_id3filename(format_string, keys, info, options): """Build a valid filename given a format string """ filename = format_string substitute = 0 for k in keys: key = k[1:-1] if info.has_key(key): v = info[key] if options.has_key('lowercase'): v = string.lower(v) if options.has_key('capitalize'): v[0:0] = string.upper(v[0:0]) if options.has_key('underscore'): v = string.replace(v, ' ', '_') if k == "%track%": v = "%02d" % int(v) substitute = substitute + 1 else: v = "XX" filename = string.replace (filename, k, v) if not substitute: return return safe_name(filename) def usage(): print "%s version %s" % (program, version) print "%s [options] format-string files..." % program print " options: -t title # set default title" print " -a artist # set default " print " -A album # set default " print " -y year # set default " print " -c comment # set default " print " -T track # set default " print " -g genre # set default " print " formatting option:" if program == "id3rename": print " -l all lowercase" print " -C Capitalize first words" print " -u replace spaces with underscores" else: print " -u remove underscores" print " -1 initialize fields from id3v1 tag" print " [-2 initialize fields from id3v2 tag: not done]" print " other options:" print " -h get some help" print " -v verbose [duplicate for more verbose]" print " -n dryrun (no files changed)" if program == "id3rename": print " format-string: a a string extended with the following tags:" print " %artist% %album% %year% %comment% %track% %genre% %skip%" print " Example: to generate title with line jead_michele_jarre-oxygene-01.mp3" print " use format-string '%artist%-%album%-%track%'" print " i.e. id3rename -l -u '%artist%-%album%-%track%' " else: print " -o overwrite existing tags" print " precedence 1st 2nd 3rd" print " normal precedence <commandline> <id3tag> <filename>" print " -o precedence <commandline> <filename> <id3tag>" print " -r don't special quote format-string (if an re)" print " -R formatstring # rename file using format (see id3rename)" print print " format-string: string with the following embedded tags:" print " %artist% %album% %year% %comment% %track% %genre% %skip%" print " Example: to match a filename 'Jean Michel Jarre - Oxygene 1.mp3'" print " '%artist% - %album% %track%'" print print " note: Leave off the extension when giving name" sys.exit(2) def name2id3(format_string, defaultinfo, options, files): """Convert filenames into ID3 tags for a set of files""" verbose = options['verbose'] overwrite = options.has_key('overwrite') dryrun = options.has_key('dryrun') clean = options.has_key('underscore') errors = 0 if not options.has_key('regexp'): rex = re_escape (format_string) else: rex = format_string rex = re.compile (build_re (rex + mp3ext, id3rexps)) allcmd = "" for f in files: mp3info = { } if verbose>1: print "Checking " + f oldinfo = get_id3info (f) if verbose: print "original : " , oldinfo m = rex.match (f) if m: if overwrite: mp3info.update (oldinfo) mp3info.update (m.groupdict()) if not overwrite: mp3info.update (oldinfo) mp3info.update (defaultinfo) if verbose>1: print "filename : ", mp3info for k in mp3info.keys(): if clean: mp3info[k] = string.replace(mp3info[k],"_", " ") mp3info[k] = string.strip (mp3info[k]) if dryrun: print mp3info else: set_id3info (f, mp3info) else: sys.stderr.write ("skipping %s : couldn't parse filename\n" % (f)) errors = errors+1 val = 0 if errors: sys.stderr.write("errors found: %d : please check format string %s\n" % (errors, format_string)) return 2 return val def id3rename(format_string, defaultinfo, options, files): """Rename a set of files based on their ID3 tags""" verbose = options['verbose'] overwrite = options.has_key('overwrite') dryrun = options.has_key('dryrun') for f in files: mp3info = get_id3info(f) if verbose: print f, mp3info mp3info.update (defaultinfo) newname = build_id3filename (format_string, id3rexps.keys(), mp3info, options) if not newname: name = f else: # Don't overwrite name (but skip if old name is the same ) index=1 name = newname + ".mp3" if f != name: while os.access(name, os.R_OK): name = "%s-%d.mp3" % (newname, index) index = index+1 if dryrun: print f + " -> " + name else: if f != name: os.renames (f, name) def main(): global verbose defaultinfo = {} options = { } options['verbose'] = 0 try: opts, args = getopt.getopt(sys.argv[1:], 't:T:a:A:y:c:g:hvuronlC') except getopt.error, msg: print msg sys.exit(2) if len(args) < 2: usage() for opt,arg in opts: if opt == '-h': usage() elif opt == '-v': options['verbose'] = verbose = verbose + 1 elif opt == '-t': defaultinfo['title'] = arg elif opt == '-a': defaultinfo['artist'] = arg elif opt == '-A': defaultinfo['album'] = arg elif opt == '-y': defaultinfo['year'] = arg elif opt == '-c': defaultinfo['comment'] = arg elif opt == '-T': defaultinfo['track'] = arg elif opt == '-g': defaultinfo['genre'] = arg elif opt == '-r': options['regexp'] = 1 elif opt == '-u': options['underscore'] = 1 elif opt == '-o': options['overwrite'] = 1 elif opt == '-n': options['dryrun'] = 1 elif opt == '-l': options['lowercase'] = 1 elif opt == '-C': options['capitalize'] = 1 if verbose: print "%s version %s" % (program, version) # Special case when we called by id3rename if program == "id3rename": id3rename (args[0], defaultinfo, options, args[1:]) sys.exit(0) name2id3(args[0], defaultinfo, options, args[1:]) ########################################### if __name__ == '__main__': dir, program = os.path.split(sys.argv[0]) main() sys.exit(0)