2 # Reencode the ID3v1 and ID3v2 tag of a mp3 file.
3 # Copyright 2010 CHEN, Xing (cxcxcxcx@gmail.com)
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of version 2 of the GNU General Public License as
7 # published by the Free Software Foundation.
10 # https://code.google.com/p/mp3tagiconv/
11 # http://linux-wiki.cn/wiki/Mp3%E6%A0%87%E7%AD%BE%E4%B9%B1%E7%A0%81%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88
12 # http://blog.icehoney.me/posts/2013-10-06-Archlinux-ape-cue-splitting
18 from optparse
import OptionParser
24 return not string
or min(string
) < '\x127'
26 def MakeID3v1(id3
, enc
):
27 """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames."""
29 """Copied from id3.py of mutagen. What I modified is the encoding of ID3v1, so that the tag can be parsed
30 correctly by Windows Media Player, etc."""
34 for v2id
, name
in {"TIT2": "title", "TPE1": "artist",
35 "TALB": "album"}.items():
37 text
= id3
[v2id
].text
[0].encode(enc
, 'replace')[:30]
40 v1
[name
] = text
+ ("\x00" * (30 - len(text
)))
43 cmnt
= id3
["COMM"].text
[0].encode(enc
, 'replace')[:28]
45 v1
["comment"] = cmnt
+ ("\x00" * (29 - len(cmnt
)))
48 try: v1
["track"] = chr(+id3
["TRCK"])
49 except ValueError: v1
["track"] = "\x00"
50 else: v1
["track"] = "\x00"
53 try: genre
= id3
["TCON"].genres
[0]
54 except IndexError: pass
56 if genre
in TCON
.GENRES
:
57 v1
["genre"] = chr(TCON
.GENRES
.index(genre
))
58 if "genre" not in v1
: v1
["genre"] = "\xff"
61 v1
["year"] = str(id3
["TDRC"])[:4]
63 v1
["year"] = str(id3
["TYER"])[:4]
65 v1
["year"] = "\x00\x00\x00\x00"
67 return ("TAG%(title)s%(artist)s%(album)s%(year)s%(comment)s"
68 "%(track)s%(genre)s") % v1
70 def has_id3v1(filename
):
71 f
= open(filename
, 'rb+')
74 else: return (f
.read(3) == "TAG")
76 def get_id3v1(filename
):
78 f
= open(filename
, 'rb+')
82 id3v1
= mutagen
.id3
.ParseID3v1(f
.read(128))
85 def convTxt(encList
, s
):
89 decF
= s
.encode('latin1').decode(i
)
97 def saveTagToFile(filename
, fileID3
, v1enc
):
98 """ Update the file."""
102 hasID3v1
= has_id3v1(filename
)
103 try: f
= open(filename
, 'rb+')
105 from errno
import ENOENT
106 if err
.errno
!= ENOENT
: raise
107 f
= open(filename
, 'ab') # create, then reopen
108 f
= open(filename
, 'rb+')
113 if v1
> 0: f
.write(MakeID3v1(fileID3
, v1enc
))
117 f
.write(MakeID3v1(fileID3
, v1enc
))
122 from mutagen
import File
124 default_enclist
= "utf8,gbk"
126 mutagen_version
= ".".join(map(str, mutagen
.version
))
127 my_version
= ".".join(map(str, VERSION
))
128 version
= "mp3tagiconv %s\nUses Mutagen %s" % (
129 my_version
, mutagen_version
)
131 parser
= OptionParser(usage
="%prog [OPTION] [FILE]...", version
=version
,
132 description
=("Mutagen-based mp3 tag encoding converter, which "
133 "can automatically detect the encoding "
134 "of the tags of a MP3 file, and can convert it so "
135 "that the tags can be recognized correctly in most "
138 "-e", "--encoding-list", metavar
="ENCODING_LIST", action
="store",
139 type="string", dest
="enclist",
140 help=("Specify original tag encoding, comma seperated if you want "
141 "me to guess one by one(default: %s)" % default_enclist
))
143 "--v1-encoding", metavar
="ID3V1_ENCODING", action
="store",
144 type="string", dest
="v1enc",
145 help=("Specify tag encoding for ID3v1, the default value(gbk) should **ALWAYS** be OK for Simplified Chinese tags."))
147 "--do-update", action
="store_true", dest
="notest",
148 help="Save updates WITHOUT confirming. It is NOT recommended.")
150 #"--confirm-test", action="store_true", dest="notest",
151 #help="Actually modify files. This is NOT default in order to protect your mp3 file from being damaged by wrong parameters.")
153 (options
, args
) = parser
.parse_args(argv
[1:])
155 raise SystemExit(parser
.print_help() or 1)
157 enclist
= options
.enclist
or default_enclist
158 enclist
= enclist
.split(',')
160 notest
= options
.notest
161 v1enc
= options
.v1enc
or "gbk"
163 enc
= locale
.getpreferredencoding()
164 for filename
in args
:
167 # Prepare information from ID3v1
168 id3v1
= get_id3v1(filename
)
170 fileID3
= mutagen
.id3
.ID3(filename
)
171 if id3v1
is not None:
172 # Merge ID3v1 and ID3v2
173 for i
in id3v1
.keys():
174 if i
not in fileID3
.keys():
175 fileID3
.add(id3v1
[i
])
178 for tag
in filter(lambda t
: t
.startswith("T"), fileID3
):
180 if isinstance(frame
, mutagen
.id3
.TimeStampTextFrame
):
186 except AttributeError:
190 #print frame.encoding
191 if frame
.encoding
== 0:
192 text
= map(convTxt
, [enclist
,]*len(frame
.text
), frame
.text
)
193 except (UnicodeError, LookupError):
198 #print i.encode('utf8')
199 if not text
or min(map(isascii
, text
)):
203 print frame
.pprint().encode(enc
)
209 #print fileID3.pprint().encode(enc)
210 print "Do you want to save?(y/N)",
212 doSave
= p
=="y" or p
=="Y"
215 print "*** Saving..."
216 saveTagToFile(filename
, fileID3
, v1enc
)
219 print "*** File is NOT updated."
221 except AttributeError: print "- Unknown file type"
222 except KeyboardInterrupt: raise
223 except Exception, err
: print str(err
)
226 if __name__
== "__main__":
229 sys
.path
.append(os
.path
.abspath("../"))