Embedding Images in MP3 Files

This article has been superseded by a more recent posting.

So you purchased an iPhone, whoopie! Now you’ve added your music and flip it on its side to watch an awesome stream of covers for all your awesome music. Instead of all that, you see a bunch of grey music notes. What gives?

Well, the problem is that iTunes has not associated an image with the MP3s in your music collection. There is a feature of iTunes that can grab album artwork. Which perhaps works well for popular music, but given my eclectic music collection I have had very little success with the feature. I happen to use a different music player as my default anyway and it has a very nice set of plug-ins that get the appropriate artwork (most of the time). For the few remaining albums, I got the artwork myself.

Getting the artwork is one thing, the trouble is how to associate the images with the appropriate audio files. Well, assuming you have some sort of order to your music collection, the problem is solvable. Below is a script that can help eliminate this problem. The original idea came from a bash script here, but I re-wrote it with python when I realized that it didn’t handle directories with spaces well.

   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
 import os, subprocess, sys
 from shutil import copyfile
 #from stat import *

 imageExt = [".jpg", ".png", ".gif"]
 coverJpg = "cover.jpg"
 coverPng = "cover.png"

 # args: extension to find
 # dirname: name of current directory
 # list of files in the current directory
 def find(arg, dirname, names):
   print "args are " +  arg
   print "dirname is " +  dirname
   print "names  are:"
   print names

   foundMp3 = "false"
   for item in names:
     pathname = os.path.join(dirname, item)
     if os.path.isfile(pathname) and pathname.lower().find(arg)>0:
       foundMp3 = "true"

   if foundMp3 == "false":
     print "No mp3s found"
     return

   # (1) Get an image to embed
   fullCoverJpg = findImage(dirname, names)

   #if fullCoverJpg  "":
   if fullCoverJpg is None or len(fullCoverJpg)  0:
     print "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.
   for item in names:
     pathname = os.path.join(dirname, item)
     #mode = os.stat(pathname)[ST_MODE]
     #if S_ISREG(mode):
     #  if arg in item:
     #    #print pathname
     #    embedImage(pathname, fullCoverJpg)
     if os.path.isfile(pathname) and pathname.lower().find(arg)>0:
       embedImage(pathname, fullCoverJpg)
 	
 # Given a directory, attempt to find a suitable cover image
 def findImage(directory, filenames):
   fullCoverJpg = ""
   # 1) Look in directory for if there is an
   # album cover, leave if not
   if coverJpg in filenames:
     print "Found JPEG cover image, no conversion necessary"
     fullCoverJpg = os.path.join(directory, coverJpg)
   elif coverPng in filenames:
     # No JPEG image found, but a legally named PNG exists
     fullCoverJpg = convertImage(directory, coverPng, coverJpg)
   else:
     desiredImage = guessImageFile(filenames);
     # found an acceptable image (based on extension)
     # if it is already a JPG, just make a copy. Otherwise do a
     # do a conversion
     if desiredImage is not None and len(desiredImage) > 0:
       print desiredImage
       print len(desiredImage)
       print imageExt[0]
       print len(imageExt[0])
       print desiredImage.lower().rfind(imageExt[0])
       fullCoverJpg = convertImage(directory, desiredImage, coverJpg)
       print fullCoverJpg
     # by virtue of making it out of the loop, we have admitted that
     # no acceptable image was present.
   return fullCoverJpg;

 def guessImageFile(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 extension in imageExt:
       if aFile.lower().rfind(extension)  len(aFile)-len(extension):
         potentialImages.append(aFile)
       
   print "potential images:"
   print potentialImages
   desiredImage = ""
   for current in potentialImages:
     if len(desiredImage) > 0:
       break;
     goodKeywords = [ "front", "large", "big", "cover" ]
     for keyword in goodKeywords:
       print keyword
       print current
       if current.lower().find(keyword) > 0:
         desiredImage = current
         print "found image: " + desiredImage
         break;
 	
   # The first heuristic did not work, just pick the first image
   if desiredImage is None or desiredImage  "":
     if len(potentialImages) > 0:
       print "defaulting to first image found"
       desiredImage = potentialImages[0];

   return desiredImage

 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 300×300 cover.jpg
   print "converting image"
   print "found image name: " + foundImage
   print "target image name: " + desiredFilename
   fullCoverJpg = os.path.join(directory, desiredFilename)
   fullCoverPng = os.path.join(directory, foundImage)
   retcode = subprocess.call(["convert", fullCoverPng, "-geometry", "300×300", fullCoverJpg])
   return fullCoverJpg

 def embedImage(audiofile, image):
   zeroBpm(audiofile);

   # (1) Remove all existing images
   removeOtherImage(audiofile);
   removeOtherImage(audiofile);
   removeFrontImage(audiofile);
   removeFrontImage(audiofile);

   # (2) remove cruft
   removeCruft(audiofile);

   # (3) check if valid ID3 tag
   checkValidId3Tag(audiofile);

   # (4) add front image
   addFrontImage(audiofile, image);

 def addFrontImage(theFile, imageFile):
   print "the file: " + theFile
   print "image: " + imageFile
   imagePathArg = "—add-image=" + imageFile + ":FRONT_COVER" 
   return  subprocess.call(["eyeD3", "—no-color", imagePathArg, theFile])

 def checkValidId3Tag(theFile):
   return  subprocess.call(["eyeD3", "—no-color", "—to-v2.4", theFile])

 def zeroBpm(theFile):
   return  subprocess.call(["eyeD3", "—bpm=90", theFile])

 def removeFrontImage(theFile):
   return  subprocess.call(["eyeD3", "—no-color", "—add-image=:FRONT_COVER", theFile])

 def removeOtherImage(theFile):
   return  subprocess.call(["eyeD3", "—no-color", "—add-image=:OTHER", theFile])

 def removeCruft(theFile):
   return  subprocess.call(["id3v2", "-APIC", "", theFile])

 if len(sys.argv) != 2:
   print "Review usage information"
 else:
   os.path.walk(sys.argv[1], find, ".mp3")
 

Download the Script:

Other interesting links: