Posted By

Twain on 10/28/07


Tagged

mp3


Versions (?)

Who likes this?

3 people have marked this snippet as a favorite

tivi
copyleft
sensimevanidus


MP3 checksum in ID3 tag


 / Published in: Python
 

Creates an MD5 checksums for MP3 files and stores the checksum in an ID3v2 UID (Universal IDentifier) tag. It skips ID3 tags in the calculation. This way you can check for corruption later on, if you notice some file is jumping.

  1. #!/usr/bin/python
  2.  
  3. """mp3md5: MP3 checksums stored in ID3v2
  4.  
  5. mp3md5 calculates MD5 checksums for all MP3's on the command line
  6. (either individual files, or directories which are recursively
  7. processed).
  8.  
  9. Checksums are calculated by skipping the ID3v2 tag at the start of the
  10. file and any ID3v1 tag at the end (does not however know about APEv2
  11. tags). The checksum is stored in an ID3v2 UFID (Unique File ID) frame
  12. with owner 'md5' (the ID3v2 tag is created if necessary).
  13.  
  14. Usage: mp3md5.py [options] [files or directories]
  15.  
  16. -h/--help
  17. Output this message and exit.
  18.  
  19. -l/--license
  20. Output license terms for mp3md5 and exit.
  21.  
  22. -n/--nocheck
  23. Do not check existing checksums (so no CONFIRMED or CHANGED lines
  24. will be output). Causes --update to be ignored.
  25.  
  26. -r/--remove
  27. Remove checksums, outputting REMOVED lines (outputs NOCHECKSUM for
  28. files already without them). Ignores --nocheck and --update.
  29.  
  30. -u/--update
  31. Instead of printing changes, update the checksum aand output UPDATED
  32. lines.
  33.  
  34. Depends on the eyeD3 module (http://eyeD3.nicfit.net)
  35.  
  36. Copyright 2007 G raham P oulter
  37. """
  38.  
  39. __copyright__ = "2007 G raham P oulter"
  40. __author__ = "G raham P oulter"
  41. __license__ = """This program is free software: you can redistribute it and/or
  42. modify it under the terms of the GNU General Public License as published by the
  43. Free Software Foundation, either version 3 of the License, or (at your option)
  44. any later version.
  45.  
  46. This program is distributed in the hope that it will be useful, but WITHOUT ANY
  47. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  48. PARTICULAR PURPOSE. See the GNU General Public License for more details.
  49.  
  50. You should have received a copy of the GNU General Public License along with
  51. this program. If not, see <http://www.gnu.org/licenses/>."""
  52.  
  53. import eyeD3
  54. from getopt import getopt
  55. import md5
  56. import os
  57. import struct
  58. import sys
  59.  
  60. pretend = False # Whether to pretend to write tags
  61. nocheck = False # Whether to not check existing sums
  62. remove = False # Whether to remove checksums
  63. update = False # Whether to update changed checksums
  64.  
  65. def log(head, body, *args):
  66. """Print a message to standard output"""
  67. print head + " "*(12-len(head)) + (body % args)
  68.  
  69. def openTag(fpath):
  70. """Attempt to open ID3 tag, creating a new one if not present"""
  71. if not eyeD3.tag.isMp3File(fpath):
  72. raise ValueError("NOT AN MP3: %s" % fpath)
  73. try:
  74. audioFile = eyeD3.tag.Mp3AudioFile(fpath, eyeD3.ID3_V2)
  75. except eyeD3.tag.InvalidAudioFormatException, ex:
  76. raise ValueError("ERROR IN MP3: %s" % fpath)
  77. tag = audioFile.getTag()
  78. if tag is None:
  79. tag = eyeD3.Tag(fpath)
  80. tag.header.setVersion(eyeD3.ID3_V2_3)
  81. if not pretend:
  82. tag.update()
  83. return tag
  84.  
  85. ### WARNING: REMEMBER TO UPDATE THE COPY IN MD5DIR
  86. def calculateUID(filepath):
  87. """Calculate MD5 for an MP3 excluding ID3v1 and ID3v2 tags if
  88. present. See www.id3.org for tag format specifications."""
  89. f = open(filepath, "rb")
  90. # Detect ID3v1 tag if present
  91. finish = os.stat(filepath).st_size;
  92. f.seek(-128, 2)
  93. if f.read(3) == "TAG":
  94. finish -= 128
  95. # ID3 at the start marks ID3v2 tag (0-2)
  96. f.seek(0)
  97. start = f.tell()
  98. if f.read(3) == "ID3":
  99. # Bytes w major/minor version (3-4)
  100. version = f.read(2)
  101. # Flags byte (5)
  102. flags = struct.unpack("B", f.read(1))[0]
  103. # Flat bit 4 means footer is present (10 bytes)
  104. footer = flags & (1<<4)
  105. # Size of tag body synchsafe integer (6-9)
  106. bs = struct.unpack("BBBB", f.read(4))
  107. bodysize = (bs[0]<<21) + (bs[1]<<14) + (bs[2]<<7) + bs[3]
  108. # Seek to end of ID3v2 tag
  109. f.seek(bodysize, 1)
  110. if footer:
  111. f.seek(10, 1)
  112. # Start of rest of the file
  113. start = f.tell()
  114. # Calculate MD5 using stuff between tags
  115. f.seek(start)
  116. h = md5.new()
  117. h.update(f.read(finish-start))
  118. f.close()
  119. return h.hexdigest()
  120.  
  121. def readUID(fpath):
  122. """Read MD5 UID from ID3v2 tag of fpath."""
  123. tag = openTag(fpath)
  124. for x in tag.getUniqueFileIDs():
  125. if x.owner_id == "md5":
  126. return x.id
  127. return None
  128.  
  129. def removeUID(fpath):
  130. """Remove MD5 UID from ID3v2 tag of fpath"""
  131. tag = openTag(fpath)
  132. todel = None
  133. for i, x in enumerate(tag.frames):
  134. if isinstance(x, eyeD3.frames.UniqueFileIDFrame) \
  135. and x.owner_id == "md5":
  136. todel = i
  137. break
  138. if todel is not None:
  139. del tag.frames[i]
  140. if not pretend:
  141. tag.update(eyeD3.ID3_V2_3)
  142. return True
  143. else:
  144. return False
  145.  
  146. def writeUID(fpath, uid):
  147. """Write the MD5 UID in the ID3v2 tag of fpath."""
  148. tag = openTag(fpath)
  149. present = False
  150. for x in tag.getUniqueFileIDs():
  151. if x.owner_id == "md5":
  152. present = True
  153. x.id = uid
  154. break
  155. if not present:
  156. tag.addUniqueFileID("md5", uid)
  157. if not pretend:
  158. tag.update(eyeD3.ID3_V2_3)
  159.  
  160. def mungeUID(fpath):
  161. "Update the MD5 UID on the tag"""
  162. if remove:
  163. if removeUID(fpath):
  164. log("REMOVED", fpath)
  165. else:
  166. log("NOCHECKSUM", fpath)
  167. else:
  168. cur_uid = readUID(fpath)
  169. if cur_uid is None:
  170. new_uid = calculateUID(fpath)
  171. writeUID(fpath, new_uid)
  172. log("ADDED", fpath)
  173. elif not nocheck:
  174. new_uid = calculateUID(fpath)
  175. if cur_uid == new_uid:
  176. log("CONFIRMED", fpath)
  177. elif update:
  178. writeUID(fpath, new_uid)
  179. log("UPDATED", fpath)
  180. else:
  181. log("BROKEN", fpath)
  182.  
  183. if __name__ == "__main__":
  184. optlist, args = getopt(sys.argv[1:], "hlnru", ["help","license","nocheck","remove","update"])
  185. for key, value in optlist:
  186. if key in ("-h","--help"):
  187. print __doc__
  188. sys.exit(0)
  189. elif key in ("-l","--license"):
  190. print license
  191. sys.exit(0)
  192. elif key in ("-n","--nocheck"):
  193. nocheck = True
  194. elif key in ("-r", "--remove"):
  195. remove = True
  196. elif key in ("-u", "--update"):
  197. update = True
  198. for start in args:
  199. if os.path.isfile(start):
  200. if start.endswith(".mp3"):
  201. mungeUID(start)
  202. elif os.path.isdir(start):
  203. for root, dirs, files in os.walk(start):
  204. dirs.sort()
  205. files.sort()
  206. for fname in files:
  207. if fname.endswith(".mp3"):
  208. mungeUID(os.path.join(root,fname))
  209. else:
  210. log("WARNING", "%s does not exist", start)

Report this snippet  

Comments

RSS Icon Subscribe to comments
Posted By: AnotherHowie on May 6, 2009

One typo: pretent => pretend on line 81

And it also completely tanks in files with no ID3 tag once that is corrected :-)

Traceback (most recent call last): File "./mp3-checksum-in-id3.py", line 201, in mungeUID(start) File "./mp3-checksum-in-id3.py", line 171, in mungeUID writeUID(fpath, new_uid) File "./mp3-checksum-in-id3.py", line 156, in writeUID tag.addUniqueFileID("md5", uid)

ValueError: ID3 vNone.None is not supported.

You need to login to post a comment.