# Description: # wmainfo-py gives you access to low level information on wma files. # * It identifies all "ASF_..." objects and shows each objects size # * It returns info such as bitrate, size, length, creation date etc # * It returns meta-tags from ASF_Content_Description_Object # # Note: # I wrestled with the ASF spec (150 page .doc format!) with no joy for # a while, then found Dan Sully's Audio-WMA Perl module: # (http://cpants.perl.org/dist/Audio-WMA :: http://www.slimdevices.com/) # This entire library is essentially a translation of parts of WMA.pm # to Python. All credit for the hard work is owed to Dan... # # License:: Artistic/Perl # Author:: Darren Kirby (mailto:bulliver@badcomputer.org) # Website:: http://badcomputer.org/unix/code/wmainfo// from os import stat from struct import unpack import time, re class WmaInfoError: def __init__(self, error): self.error = error def __str__(self): return self.error class WmaInfo: def __init__(self, file, debug=None): # 'public' attributes self.drm = None # boolean self.tags = {} # hash self.headerObject = {} # hash of arrays self.info = {} # hash self.file = file self.debug = debug self.__parseWmaHeader() def printobjects(self): ''' ASF_Header_Object prints: "name: GUID size num_objects" All other objects print: "name: GUID size offset" ''' for key,val in self.headerObject.iteritems(): print "%s: %s %i %i" % (key, val[0], val[1], val[2]) def hasdrm(self): ''' Returns true if the file has DRM, ie: if a *Content_Encryption_Object is present ''' if self.drm: return True else: return False def printtags(self): ''' This is all the 'id3' like info gathered from: ASF_Content_Description_Object and ASF_Extended_Content_Description_Object ''' for k,v in self.tags.iteritems(): print "%s:%s%s" % (k, " " * (13 - len(k)), v) def hastag(self, tag): '''returns true if tags[tag] has a value''' if self.tags.has_key(tag) and self.tags[tag] != "": return True else: return False def printinfo(self): ''' This is all the 'non-id3' like info gathered from ASF_File_Properties_Object and ASF_Extended_Content_Description_Object ''' for k,v in self.info.iteritems(): print "%s:%s%s" % (k, " " * (20 - len(k)), v) def hasinfo(self, field): '''Returns true if info[field] has a value''' if self.info.has_key(field) and self.tags[tag] != "": return True else: return False def parsestream(self): ''' I don't think most people will want/need this info so it is not parsed automatically ''' self.stream = {} try: offset = int(self.headerObject['ASF_Stream_Properties_Object'][2]) self.__parseASFStreamPropertiesObject(offset) except: raise WmaInfoError("Cannot grok ASF_Stream_Properties_Object") def __parseWmaHeader(self): self.size = int(stat(self.file)[6]) self.fh = open(self.file, "rb") self.offset = 0 self.fileOffset = 30 self.guidMapping = self.__knownGUIDs() self.reverseGuidMapping = {} for k,v in self.guidMapping.iteritems(): self.reverseGuidMapping[v] = k try: objectId = self.__byteStringToGUID(self.fh.read(16)) objectSize = unpack("<2L", self.fh.read(8))[0] headerObjects = unpack("<1L", self.fh.read(4))[0] reserved1 = unpack("b", self.fh.read(1))[0] reserved2 = unpack("b", self.fh.read(1))[0] objectIdName = self.reverseGuidMapping[objectId] except: raise WmaInfoError(self.file + " doesn't appear to have a valid ASF header") if objectSize > self.size: raise WmaInfoError("Header size reported larger than file size") self.headerObject[objectIdName] = [objectId, objectSize, headerObjects, reserved1, reserved2] if self.debug: print "objectId: %s" % objectId print "objectIdName: %s" % self.reverseGuidMapping[objectId] print "objectSize: %s" % objectSize print "headerObjects: %s" % headerObjects print "reserved1: %s" % reserved1 print "reserved2: %s" % reserved2 self.headerData = self.fh.read(objectSize - 30) self.fh.close # Done with the file before we get going for n in range(headerObjects): nextObject = self.__readAndIncrementOffset(16) nextObjectText = self.__byteStringToGUID(nextObject) nextObjectSize = self.__parse64BitString(self.__readAndIncrementOffset(8)) nextObjectName = self.reverseGuidMapping[nextObjectText] self.headerObject[nextObjectName] = [nextObjectText, nextObjectSize, self.fileOffset] self.fileOffset += nextObjectSize if self.debug: print "nextObjectGUID: %s" % nextObjectText print "nextObjectName: %s" % nextObjectName print "nextObjectSize: %s" % nextObjectSize # start looking at object contents if (nextObjectName == 'ASF_File_Properties_Object'): self.__parseASFFilePropertiesObject() continue elif (nextObjectName == 'ASF_Content_Description_Object'): self.__parseASFContentDescriptionObject() continue elif (nextObjectName == 'ASF_Extended_Content_Description_Object'): self.__parseASFExtendedContentDescriptionObject() continue elif (nextObjectName == 'ASF_Content_Encryption_Object') or (nextObjectName == 'ASF_Extended_Content_Encryption_Object'): self.drm = 1 # set our next object size self.offset += nextObjectSize - 24 def __parseASFFilePropertiesObject(self): fileid = self.__readAndIncrementOffset(16) self.info['fileid_guid'] = self.__byteStringToGUID(fileid) self.info['filesize'] = int(self.__parse64BitString(self.__readAndIncrementOffset(8))) self.info['creation_date'] = unpack("