1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407 | # * coding: utf-8 *
import getopt, os, platform, subprocess, sys
version = "0.0.3"
imageExt = [[".jpg", "image/jpeg"], [".png", "image/png"], [".gif", "image/gif"]]
USE_MUTAGEN = "false"
USE_FIRST_IMAGE_FOUND = "false"
MUTAGEN_MODULE = ""
CMD_CONVERT = "convert"
CMD_EYED3 = "eyeD3"
CMD_ID3 = "id3v2"
DEBUG_FLAG = "false"
DEFAULT_RESOLUTION = "600×600"
coverJpg = "cover-embed-" + DEFAULT_RESOLUTION + ".jpg"
coverPng = "cover-embed-" + DEFAULT_RESOLUTION + ".png"
#######################################################################
#
# Method called to walk the directory tree
#
# args: extention to find
# dirname: name of current directory
# list of files in the current directory
#
def find(arg, dirname, names):
foundMp3 = "false"
for item in names:
pathname = os.path.join(dirname, item)
pathname = os.path.normpath(pathname)
if os.path.isfile(pathname) and pathname.lower().find(arg)>0:
debug("Found " + arg + " in " + dirname)
foundMp3 = "true"
if foundMp3 == "false":
debug("No "+ arg +"s found in directory " + dirname)
return
# (1) Get an image to embed
fullCoverJpg = findImage(dirname, names)
if fullCoverJpg is None or len(fullCoverJpg) == 0:
debug("No acceptable image found for " + dirname)
return;
# 2) Loop through contents and determine
# if it is a file or a directory. Directories
# can be skipped. Whereas for files, we'll
# want to make a callback to embed the images.
badAudioFiles = []
for item in names:
pathname = os.getcwd()
pathname = os.path.join(pathname, dirname)
pathname = os.path.join(pathname, item)
pathname = os.path.normpath(pathname)
if os.path.isfile(pathname) and pathname.lower().find(arg)>0:
embedReturnCode = embedImage(pathname, fullCoverJpg)
if 0 < embedReturnCode:
badAudioFiles.append(pathname)
if 0 < len(badAudioFiles):
print "Errors occurred embedding images in the following files:"
sys.stdout.write(" * ")
print "\n\r * ".join(badAudioFiles)
#######################################################################
#
# Given a directory, attempt to find a suitable cover image
#
def findImage(directory, filenames):
imageFileToUse = ""
# 1) Look in directory for if there is an
# album cover, leave if not
if coverJpg in filenames:
debug("Found JPEG cover image, no conversion necessary")
fullCoverJpg = os.path.join(directory, coverJpg)
fullCoverJpg = os.path.normpath(fullCoverJpg)
imageFileToUse = [fullCoverJpg, "image/jpeg"]
elif coverPng in filenames:
# No JPEG image found, but a legally named PNG exists
imageFileToUse = convertImage(directory, coverPng, coverJpg)
else:
desiredImage = guessImageFile(directory, filenames);
# found an acceptable image (based on extention)
# if it is already a JPG, just make a copy. Otherwise do a
# do a conversion
if desiredImage "":
print >> sys.stderr, "no image found in " + directory
elif USE_FIRST_IMAGE_FOUND "true":
imageFileToUse = desiredImage
elif len(desiredImage[0]) > 0:
imageFileToUse = convertImage(directory, desiredImage[0], coverJpg)
# by virtue of making it out of the loop, we have admitted that
# no acceptable image was present.
return imageFileToUse;
#######################################################################
#
# Simple heuristic for determining the correct cover art file.
#
def guessImageFile(directory, filenames):
# There is no well-defined image for this directory.
# Attempt to guestimate a reasonable replacement. Look
# through what is available & sort those
potentialImages = []
for aFile in filenames:
for extention,mime in imageExt:
if aFile.lower().rfind(extention) len(aFile)-len(extention):
debug("Looking at using file: " + aFile + " | Mime Type: " + mime)
fullpath = os.getcwd()
fullpath = os.path.join(fullpath, directory)
fullpath = os.path.join(fullpath, aFile)
fullpath = os.path.normpath(fullpath)
potentialImages.append([fullpath, mime])
desiredImage = ""
for currentFile, currentMime in potentialImages:
if len(desiredImage) > 0:
break;
goodKeywords = [ "front", "large", "big", "cover"]
for keyword in goodKeywords:
if currentFile.lower().find(keyword) > 0:
desiredImage = [currentFile, currentMime]
debug("found image: " + currentFile)
break;
# The first heuristic did not work, just pick the first image
if desiredImage is None or desiredImage "":
if len(potentialImages) > 0:
desiredImage = potentialImages[0];
return desiredImage
#######################################################################
#
# Linux command-line approach for converting/shrinking an image file.
#
# directory: directory where the file lives
# foundImage: fullpath to the the desired image
# desiredFilename: name of the output file
#
def convertImage(directory, foundImage, desiredFilename):
# With imagemagick version 6.4.8.3, the command
# to convert the image is similar to:
# convert cover.png -geometry 600×600 cover.jpg
fullCoverJpg = os.path.join(directory, desiredFilename)
fullCoverJpg = os.path.normpath(fullCoverJpg)
cmd = [CMD_CONVERT, fullCoverPng, "-geometry", DEFAULT_RESOLUTION , fullCoverJpg]
shellCommandWrapper(cmd)
return [fullCoverJpg, "image/jpeg"]
#######################################################################
#
# Wrapper function to pick the correct means of embedding the
# album artwork into the audio file.
#
def embedImage(audiofile, image):
addReturnCode = 0
if USE_MUTAGEN:
addReturnCode = embedImageViaMutagen(audiofile, image)
else:
addReturnCode = embedImageViaLinuxCommandLine(audiofile, image)
return addReturnCode
#######################################################################
#
# Use the "v2" approach of Mutagen
#
def embedImageViaMutagen(audiofile, image):
returnCode = 0
exec "from mutagen.mp3 import MP3"
exec "from mutagen.id3 import ID3, APIC, error"
mp3Object = MP3(audiofile, ID3=ID3)
try:
debug("Adding " + image[0] + " to the file " + audiofile )
mp3Object.tags.add(APIC(encoding=3, mime=image[1], type=3, desc=u'Cover',data=open(image[0]).read()))
mp3Object.save()
except error:
returnCode = 1
return returnCode
#######################################################################
#
# Use the "v1" approach of several command-line tools
#
def embedImageViaLinuxCommandLine(audiofile, image):
removeReturnCode = 0
addReturnCode = 0
removeReturnCode += zeroBpm(audiofile);
# (1) Remove all existing images
removeReturnCode += removeOtherImage(audiofile);
removeReturnCode += removeOtherImage(audiofile);
removeReturnCode += removeFrontImage(audiofile);
removeReturnCode += removeFrontImage(audiofile);
# (2) remove cruft
removeReturnCode += removeCruft(audiofile);
# (3) check if valid ID3 tag
removeReturnCode += checkValidId3Tag(audiofile);
# (4) add front image
addReturnCode += addFrontImage(audiofile, image[0]);
if 0 < removeReturnCode:
debug("——————————————————————————————————")
debug("Errors occured when removing/cleaning ID3 tag for: ")
debug(audiofile)
debug("This behavior is unexpected, but may not impact embedding the image.")
debug("——————————————————————————————————")
if 0 < addReturnCode:
debug("——————————————————————————————————")
debug("The unable to embed image in: ")
debug(audiofile)
debug("——————————————————————————————————")
return addReturnCode
#######################################################################
#
# Linux command-line wrapper for adding the front cover artwork
#
def addFrontImage(theFile, imageFile):
imagePathArg = "—add-image=" + imageFile + ":FRONT_COVER"
cmd = [CMD_EYED3, "—no-color", imagePathArg, theFile]
return shellCommandWrapper(cmd)
#######################################################################
#
# Linux command-line wrapper for converting to the right ID3 version
#
def checkValidId3Tag(theFile):
cmd = [CMD_EYED3, "—no-color", "—to-v2.4", theFile]
return shellCommandWrapper(cmd)
#######################################################################
#
# Linux command-line wrapper hack for dealing with floating-point BPMs
#
def zeroBpm(theFile):
cmd = [CMD_EYED3, "—bpm=90", theFile]
return shellCommandWrapper(cmd)
#######################################################################
#
# Linux command-line wrapper for removing the front cover artwork
#
def removeFrontImage(theFile):
cmd = [CMD_EYED3, "—no-color", "—add-image=:FRONT_COVER", theFile]
return shellCommandWrapper(cmd)
#######################################################################
#
# Linux command-line wrapper for removing the other artwork
#
def removeOtherImage(theFile):
cmd = [CMD_EYED3, "—no-color", "—add-image=:OTHER", theFile]
return shellCommandWrapper(cmd)
#######################################################################
#
# Linux command-line wrapper for removing cruft
# * Mainly needed for older ID3 editors, may no longer be an issue
# on newer (2010+) installs
#
def removeCruft(theFile):
cmd = [CMD_ID3, "-APIC", "", theFile]
return shellCommandWrapper(cmd)
#######################################################################
#
# Simple environment sanity check
#
def checkEnvironment():
#
# try to dynamically load mutagen
#
try:
global MUTAGEN_MODULE
MUTAGEN_MODULE = import("mutagen")
debug(MUTAGEN_MODULE)
USE_MUTAGEN = "true"
debug("Able to dynamically load mutagen")
except:
debug("Unable to dynamically load mutagen")
if "Linux" <> platform.system():
print >> sys.stderr, "*********************************************************"
print >> sys.stderr, "*** Environment Check Failed! ***"
print >> sys.stderr, "*** ***"
print >> sys.stderr, " mutagen must be installed"
print >> sys.stderr, "*** ***"
print >> sys.stderr, " http://code.google.com/p/mutagen/"
print >> sys.stderr, "*** ***"
print >> sys.stderr, "*********************************************************"
print
sys.exit(1)
#
# fallback 'raw' method, look for the appropriate command-line utilities
#
checkEnvironmentHelper([CMD_EYED3, "—help"])
checkEnvironmentHelper([CMD_ID3])
if "Linux" platform.system():
checkEnvironmentHelper([CMD_CONVERT])
else:
global USE_FIRST_IMAGE_FOUND
USE_FIRST_IMAGE_FOUND = "true"
#######################################################################
#
# Helper method for displaying errors about some simple command-line
# environment checks (for Linux)
#
def checkEnvironmentHelper(command):
if 0 < shellCommandWrapper(command):
print >> sys.stderr, "*********************************************************"
print >> sys.stderr, "*** Environment Check Failed! ***"
print >> sys.stderr, " Unable to locate '" + command[0] + "' in the PATH"
print >> sys.stderr, "*********************************************************"
print
#######################################################################
#
# Prints debugging messages (if the debugging flag has been set)
#
def debug(message):
if DEBUG_FLAG "true":
print message
#######################################################################
#
# Small wrapper method for executing Linux Shell Commands
#
def shellCommandWrapper(command):
FNULL = open(os.devnull, 'w')
process = subprocess.Popen(command, shell=False, bufsize=1, \
stdin=None, stdout=FNULL, stderr=FNULL)
process.wait()
return process.returncode
#######################################################################
#
# Print Usage Information
#
def usage():
print
print "This script takes one or more arguments that are expected to be a " + \
"a single directory or set of directories. For each argument supplied, " + \
"the script will recurse into the directory and look for an image file. If a " + \
"file is found, it will be converted to the default resolution (" + DEFAULT_RESOLUTION + \
"). The script will then look for any MP3s in the directory. If found, the " + \
"image will be embedded into each file (overwriting an existing embedded image)."
#######################################################################
#
#
# Start of the 'main()' function
#
def main():
#######################################################################
#
# Read Command-Line
#
try:
opts, args = getopt.getopt(sys.argv[1:], "hd", ["help", "debug", \
"use-first-image", "use-mutagen"])
except getopt.GetoptError, err:
# print help information and exit:
print >> sys.stderr, err
usage()
sys.exit(2)
#######################################################################
#
# Parse Command-Line Options and Arguments
#
for o, a in opts:
if o in ("h", "-help"):
usage()
sys.exit()
elif o in ("d", "-debug"):
global DEBUG_FLAG
DEBUG_FLAG = "true"
elif o "--use-first-image":
global USE_FIRST_IMAGE_FOUND
USE_FIRST_IMAGE_FOUND = "true"
elif o "—use-mutagen":
global USE_MUTAGEN
USE_MUTAGEN = "true"
else:
assert False, "unhandled option"
#######################################################################
#
# Now that all the processing is out of the way, walk the directory
# structure and embed artwork into any discovered MP3 files.
#
if len(args) == 0:
print >> sys.stderr, "Missing command-line arguments"
usage()
else:
checkEnvironment()
for directory in args:
os.path.walk(directory, find, ".mp3")
if name == "main":
main()
|