| 1 |
ninoborges |
8 |
#!/usr/bin/env python
|
| 2 |
|
|
#
|
| 3 |
|
|
# Copyright (c) 2002 Vivake Gupta (vivakeATomniscia.org). All rights reserved.
|
| 4 |
|
|
#
|
| 5 |
|
|
# This program is free software; you can redistribute it and/or
|
| 6 |
|
|
# modify it under the terms of the GNU General Public License as
|
| 7 |
|
|
# published by the Free Software Foundation; either version 2 of the
|
| 8 |
|
|
# License, or (at your option) any later version.
|
| 9 |
|
|
#
|
| 10 |
|
|
# This program is distributed in the hope that it will be useful,
|
| 11 |
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
|
|
# GNU General Public License for more details.
|
| 14 |
|
|
#
|
| 15 |
|
|
# You should have received a copy of the GNU General Public License
|
| 16 |
|
|
# along with this program; if not, write to the Free Software
|
| 17 |
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
| 18 |
|
|
# USA
|
| 19 |
|
|
#
|
| 20 |
|
|
# This software is maintained by Vivake (vivakeATomniscia.org) and is available at:
|
| 21 |
|
|
# http://www.omniscia.org/~vivake/python/MP3Info.py
|
| 22 |
|
|
|
| 23 |
|
|
import struct
|
| 24 |
|
|
import string
|
| 25 |
|
|
|
| 26 |
|
|
def _from_synch_safe(synchsafe):
|
| 27 |
|
|
if isinstance(synchsafe, type(1)):
|
| 28 |
|
|
(b3, b2, b1, b0) = struct.unpack('!4b', struct.pack('!1i', synchsafe))
|
| 29 |
|
|
else:
|
| 30 |
|
|
while len(synchsafe) < 4:
|
| 31 |
|
|
synchsafe = (0,) + synchsafe
|
| 32 |
|
|
(b3, b2, b1, b0) = synchsafe
|
| 33 |
|
|
|
| 34 |
|
|
x = 256
|
| 35 |
|
|
return (((b3 * x + b2) * x + b1) * x + b0)
|
| 36 |
|
|
|
| 37 |
|
|
def _strip_zero(s):
|
| 38 |
|
|
start = 0
|
| 39 |
|
|
while start < len(s) and (s[start] == '\0' or s[start] == ' '):
|
| 40 |
|
|
start = start + 1
|
| 41 |
|
|
|
| 42 |
|
|
end = len(s) - 1
|
| 43 |
|
|
while end >= 0 and (s[end] == '\0' or s[end] == ' '):
|
| 44 |
|
|
end = end - 1
|
| 45 |
|
|
|
| 46 |
|
|
return s[start:end+1]
|
| 47 |
|
|
|
| 48 |
|
|
class ID3v2Frame:
|
| 49 |
|
|
def __init__(self, file, version):
|
| 50 |
|
|
self.name = ""
|
| 51 |
|
|
self.version = 0
|
| 52 |
|
|
self.padding = 0
|
| 53 |
|
|
self.size = 0
|
| 54 |
|
|
self.data = ""
|
| 55 |
|
|
|
| 56 |
|
|
self.flags = {}
|
| 57 |
|
|
self.f_tag_alter_preservation = 0
|
| 58 |
|
|
self.f_file_alter_preservation = 0
|
| 59 |
|
|
self.f_read_only = 0
|
| 60 |
|
|
self.f_compression = 0
|
| 61 |
|
|
self.f_encryption = 0
|
| 62 |
|
|
self.f_grouping_identity = 0
|
| 63 |
|
|
self.f_unsynchronization = 0
|
| 64 |
|
|
self.f_data_length_indicator = 0
|
| 65 |
|
|
|
| 66 |
|
|
if version == 2:
|
| 67 |
|
|
nameSize = 3
|
| 68 |
|
|
else:
|
| 69 |
|
|
nameSize = 4
|
| 70 |
|
|
self.name = file.read(nameSize)
|
| 71 |
|
|
|
| 72 |
|
|
self.version = version
|
| 73 |
|
|
|
| 74 |
|
|
if self.name == nameSize * '\0':
|
| 75 |
|
|
self.padding = 1
|
| 76 |
|
|
return
|
| 77 |
|
|
|
| 78 |
|
|
if self.name[0] < 'A' or self.name[0] > 'Z':
|
| 79 |
|
|
self.padding = 1
|
| 80 |
|
|
return
|
| 81 |
|
|
|
| 82 |
|
|
size = ()
|
| 83 |
|
|
if version == 2:
|
| 84 |
|
|
size = struct.unpack('!3b', file.read(3))
|
| 85 |
|
|
elif version == 3 or version == 4:
|
| 86 |
|
|
size = struct.unpack('!4b', file.read(4))
|
| 87 |
|
|
|
| 88 |
|
|
if version == 3: # abc00000 def00000
|
| 89 |
|
|
(flags,) = struct.unpack('!1b', file.read(1))
|
| 90 |
|
|
self.f_tag_alter_preservation = flags >> 7 & 1 #a
|
| 91 |
|
|
self.f_file_alter_preservation = flags >> 6 & 1 #b
|
| 92 |
|
|
self.f_read_only = flags >> 5 & 1 #c
|
| 93 |
|
|
(flags,) = struct.unpack('!1b', file.read(1))
|
| 94 |
|
|
self.f_compression = flags >> 7 & 1 #d
|
| 95 |
|
|
self.f_encryption = flags >> 6 & 1 #e
|
| 96 |
|
|
self.f_grouping_identity = flags >> 5 & 1 #f
|
| 97 |
|
|
elif version == 4: # 0abc0000 0h00kmnp
|
| 98 |
|
|
(flags,) = struct.unpack('!1b', file.read(1))
|
| 99 |
|
|
self.f_tag_alter_preservation = flags >> 6 & 1 #a
|
| 100 |
|
|
self.f_file_alter_preservation = flags >> 5 & 1 #b
|
| 101 |
|
|
self.f_read_only = flags >> 4 & 1 #c
|
| 102 |
|
|
(flags,) = struct.unpack('!1b', file.read(1))
|
| 103 |
|
|
self.f_grouping_identity = flags >> 6 & 1 #h
|
| 104 |
|
|
self.f_compression = flags >> 3 & 1 #k
|
| 105 |
|
|
self.f_encryption = flags >> 2 & 1 #m
|
| 106 |
|
|
self.f_unsynchronization = flags >> 1 & 1 #n
|
| 107 |
|
|
self.f_data_length_indicator = flags >> 0 & 1 #p
|
| 108 |
|
|
|
| 109 |
|
|
self.size = _from_synch_safe(size)
|
| 110 |
|
|
self.data = _strip_zero(file.read(self.size))
|
| 111 |
|
|
|
| 112 |
|
|
_genres = [
|
| 113 |
|
|
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
|
| 114 |
|
|
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
|
| 115 |
|
|
"Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
|
| 116 |
|
|
"Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
|
| 117 |
|
|
"Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
|
| 118 |
|
|
"Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock",
|
| 119 |
|
|
"Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop",
|
| 120 |
|
|
"Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-industrial",
|
| 121 |
|
|
"Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy",
|
| 122 |
|
|
"Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle",
|
| 123 |
|
|
"Native American", "Cabaret", "New Wave", "Psychadelic", "Rave",
|
| 124 |
|
|
"Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz",
|
| 125 |
|
|
"Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk",
|
| 126 |
|
|
"Folk/Rock", "National Folk", "Swing", "Fast-Fusion", "Bebob", "Latin",
|
| 127 |
|
|
"Revival", "Celtic", "Bluegrass", "Avantegarde", "Gothic Rock",
|
| 128 |
|
|
"Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock",
|
| 129 |
|
|
"Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech",
|
| 130 |
|
|
"Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass",
|
| 131 |
|
|
"Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
|
| 132 |
|
|
"Folklore", "Ballad", "Power Ballad", "Rythmic Soul", "Freestyle", "Duet",
|
| 133 |
|
|
"Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", "Goa",
|
| 134 |
|
|
"Drum & Bass", "Club House", "Hardcore", "Terror", "Indie", "BritPop",
|
| 135 |
|
|
"NegerPunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal",
|
| 136 |
|
|
"Black Metal", "Crossover", "Contemporary C", "Christian Rock", "Merengue",
|
| 137 |
|
|
"Salsa", "Thrash Metal", "Anime", "JPop", "SynthPop",
|
| 138 |
|
|
]
|
| 139 |
|
|
|
| 140 |
|
|
class ID3v1:
|
| 141 |
|
|
def __init__(self, file):
|
| 142 |
|
|
self.valid = 0
|
| 143 |
|
|
|
| 144 |
|
|
self.tags = { }
|
| 145 |
|
|
|
| 146 |
|
|
try:
|
| 147 |
|
|
file.seek(-128, 2)
|
| 148 |
|
|
except IOError:
|
| 149 |
|
|
pass
|
| 150 |
|
|
|
| 151 |
|
|
data = file.read(128)
|
| 152 |
|
|
if data[0:3] != 'TAG':
|
| 153 |
|
|
return
|
| 154 |
|
|
else:
|
| 155 |
|
|
self.valid = 1
|
| 156 |
|
|
|
| 157 |
|
|
self.tags['TT2'] = _strip_zero(data[ 3: 33])
|
| 158 |
|
|
self.tags['TP1'] = _strip_zero(data[33: 63])
|
| 159 |
|
|
self.tags['TAL'] = _strip_zero(data[63: 93])
|
| 160 |
|
|
self.tags['TYE'] = _strip_zero(data[93: 97])
|
| 161 |
|
|
self.tags['COM'] = _strip_zero(data[97:125])
|
| 162 |
|
|
|
| 163 |
|
|
if data[125] == '\0':
|
| 164 |
|
|
self.tags['TRK'] = ord(data[126])
|
| 165 |
|
|
|
| 166 |
|
|
try:
|
| 167 |
|
|
self.tags['TCO'] = _genres[ord(data[127])]
|
| 168 |
|
|
except IndexError:
|
| 169 |
|
|
self.tags['TCO'] = "(%i)" % ord(data[127])
|
| 170 |
|
|
|
| 171 |
|
|
|
| 172 |
|
|
class ID3v2:
|
| 173 |
|
|
def __init__(self, file):
|
| 174 |
|
|
self.valid = 0
|
| 175 |
|
|
|
| 176 |
|
|
self.tags = { }
|
| 177 |
|
|
|
| 178 |
|
|
self.header_size = 0
|
| 179 |
|
|
|
| 180 |
|
|
self.major_version = 0
|
| 181 |
|
|
self.minor_version = 0
|
| 182 |
|
|
|
| 183 |
|
|
self.f_unsynchronization = 0
|
| 184 |
|
|
self.f_extended_header = 0
|
| 185 |
|
|
self.f_experimental = 0
|
| 186 |
|
|
self.f_footer = 0
|
| 187 |
|
|
|
| 188 |
|
|
self.f_extended_header_zie = 0
|
| 189 |
|
|
self.f_extended_num_flag_bytes = 0
|
| 190 |
|
|
|
| 191 |
|
|
self.ef_update = 0
|
| 192 |
|
|
self.ef_crc = 0
|
| 193 |
|
|
self.ef_restrictions = 0
|
| 194 |
|
|
|
| 195 |
|
|
self.crc = 0
|
| 196 |
|
|
self.restrictions = 0
|
| 197 |
|
|
|
| 198 |
|
|
self.frames = []
|
| 199 |
|
|
self.tags = {}
|
| 200 |
|
|
|
| 201 |
|
|
file.seek(0, 0)
|
| 202 |
|
|
if file.read(3) != "ID3":
|
| 203 |
|
|
return
|
| 204 |
|
|
else:
|
| 205 |
|
|
self.valid = 1
|
| 206 |
|
|
|
| 207 |
|
|
(self.major_version, self.minor_version) = struct.unpack('!2b', file.read(2))
|
| 208 |
|
|
|
| 209 |
|
|
# abcd 0000
|
| 210 |
|
|
(flags,) = struct.unpack('!1b', file.read(1))
|
| 211 |
|
|
self.f_unsynchronization = flags >> 7 & 1 # a
|
| 212 |
|
|
self.f_extended_header = flags >> 6 & 1 # b
|
| 213 |
|
|
self.f_experimental = flags >> 5 & 1 # c
|
| 214 |
|
|
self.f_footer = flags >> 4 & 1 # d
|
| 215 |
|
|
|
| 216 |
|
|
self.header_size = _from_synch_safe(struct.unpack('!4b', file.read(4)))
|
| 217 |
|
|
|
| 218 |
|
|
while 1:
|
| 219 |
|
|
if file.tell() >= self.header_size:
|
| 220 |
|
|
break
|
| 221 |
|
|
frame = ID3v2Frame(file, self.major_version)
|
| 222 |
|
|
if frame.padding:
|
| 223 |
|
|
file.seek(self.header_size)
|
| 224 |
|
|
break
|
| 225 |
|
|
|
| 226 |
|
|
self.frames = self.frames + [frame]
|
| 227 |
|
|
self.tags[frame.name] = frame.data
|
| 228 |
|
|
|
| 229 |
|
|
_bitrates = [
|
| 230 |
|
|
[ # MPEG-2 & 2.5
|
| 231 |
|
|
[0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256,None], # Layer 1
|
| 232 |
|
|
[0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,None], # Layer 2
|
| 233 |
|
|
[0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,None] # Layer 3
|
| 234 |
|
|
],
|
| 235 |
|
|
|
| 236 |
|
|
[ # MPEG-1
|
| 237 |
|
|
[0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,None], # Layer 1
|
| 238 |
|
|
[0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384,None], # Layer 2
|
| 239 |
|
|
[0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320,None] # Layer 3
|
| 240 |
|
|
]
|
| 241 |
|
|
]
|
| 242 |
|
|
|
| 243 |
|
|
_samplerates = [
|
| 244 |
|
|
[ 11025, 12000, 8000, None], # MPEG-2.5
|
| 245 |
|
|
[ None, None, None, None], # reserved
|
| 246 |
|
|
[ 22050, 24000, 16000, None], # MPEG-2
|
| 247 |
|
|
[ 44100, 48000, 32000, None], # MPEG-1
|
| 248 |
|
|
]
|
| 249 |
|
|
|
| 250 |
|
|
_modes = [ "stereo", "joint stereo", "dual channel", "mono" ]
|
| 251 |
|
|
|
| 252 |
|
|
_mode_extensions = [
|
| 253 |
|
|
[ "4-31", "8-31", "12-31", "16-31" ],
|
| 254 |
|
|
[ "4-31", "8-31", "12-31", "16-31" ],
|
| 255 |
|
|
[ "", "IS", "MS", "IS+MS" ]
|
| 256 |
|
|
]
|
| 257 |
|
|
|
| 258 |
|
|
_emphases = [ "none", "50/15 ms", "reserved", "CCIT J.17" ]
|
| 259 |
|
|
|
| 260 |
|
|
_MP3_HEADER_SEEK_LIMIT = 8192
|
| 261 |
|
|
|
| 262 |
|
|
class MPEG:
|
| 263 |
|
|
def __init__(self, file):
|
| 264 |
|
|
self.valid = 0
|
| 265 |
|
|
|
| 266 |
|
|
file.seek(0, 2)
|
| 267 |
|
|
self.filesize = file.tell()
|
| 268 |
|
|
file.seek(0, 0)
|
| 269 |
|
|
|
| 270 |
|
|
self.version = 0
|
| 271 |
|
|
self.layer = 0
|
| 272 |
|
|
self.protection = 0
|
| 273 |
|
|
self.bitrate = 0
|
| 274 |
|
|
self.samplerate = 0
|
| 275 |
|
|
self.padding = 0
|
| 276 |
|
|
self.private = 0
|
| 277 |
|
|
self.mode = ""
|
| 278 |
|
|
self.mode_extension = ""
|
| 279 |
|
|
self.copyright = 0
|
| 280 |
|
|
self.original = 0
|
| 281 |
|
|
self.emphasis = ""
|
| 282 |
|
|
self.length = 0
|
| 283 |
|
|
|
| 284 |
|
|
offset, header = self._find_header(file)
|
| 285 |
|
|
if offset == -1 or header is None:
|
| 286 |
|
|
return
|
| 287 |
|
|
|
| 288 |
|
|
self._parse_header(header)
|
| 289 |
|
|
### offset + framelength will find another header. verify??
|
| 290 |
|
|
if not self.valid:
|
| 291 |
|
|
return
|
| 292 |
|
|
|
| 293 |
|
|
self._parse_xing(file)
|
| 294 |
|
|
|
| 295 |
|
|
|
| 296 |
|
|
def _find_header(self, file):
|
| 297 |
|
|
file.seek(0, 0)
|
| 298 |
|
|
amount_read = 0
|
| 299 |
|
|
|
| 300 |
|
|
# see if we get lucky with the first four bytes
|
| 301 |
|
|
amt = 4
|
| 302 |
|
|
|
| 303 |
|
|
while amount_read < _MP3_HEADER_SEEK_LIMIT:
|
| 304 |
|
|
header = file.read(amt)
|
| 305 |
|
|
if len(header) < amt:
|
| 306 |
|
|
# awfully short file. just give up.
|
| 307 |
|
|
return -1, None
|
| 308 |
|
|
|
| 309 |
|
|
amount_read = amount_read + len(header)
|
| 310 |
|
|
|
| 311 |
|
|
# on the next read, grab a lot more
|
| 312 |
|
|
amt = 500
|
| 313 |
|
|
|
| 314 |
|
|
# look for the sync byte
|
| 315 |
|
|
offset = string.find(header, chr(255))
|
| 316 |
|
|
if offset == -1:
|
| 317 |
|
|
continue
|
| 318 |
|
|
### maybe verify more sync bits in next byte?
|
| 319 |
|
|
|
| 320 |
|
|
if offset + 4 > len(header):
|
| 321 |
|
|
more = file.read(4)
|
| 322 |
|
|
if len(more) < 4:
|
| 323 |
|
|
# end of file. can't find a header
|
| 324 |
|
|
return -1, None
|
| 325 |
|
|
amount_read = amount_read + 4
|
| 326 |
|
|
header = header + more
|
| 327 |
|
|
return amount_read - len(header) + offset, header[offset:offset+4]
|
| 328 |
|
|
|
| 329 |
|
|
# couldn't find the header
|
| 330 |
|
|
return -1, None
|
| 331 |
|
|
|
| 332 |
|
|
def _parse_header(self, header):
|
| 333 |
|
|
# AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
|
| 334 |
|
|
(bytes,) = struct.unpack('>i', header)
|
| 335 |
|
|
mpeg_version = (bytes >> 19) & 3 # BB 00 = MPEG2.5, 01 = res, 10 = MPEG2, 11 = MPEG1
|
| 336 |
|
|
layer = (bytes >> 17) & 3 # CC 00 = res, 01 = Layer 3, 10 = Layer 2, 11 = Layer 1
|
| 337 |
|
|
protection_bit = (bytes >> 16) & 1 # D 0 = protected, 1 = not protected
|
| 338 |
|
|
bitrate = (bytes >> 12) & 15 # EEEE 0000 = free, 1111 = bad
|
| 339 |
|
|
samplerate = (bytes >> 10) & 3 # F 11 = res
|
| 340 |
|
|
padding_bit = (bytes >> 9) & 1 # G 0 = not padded, 1 = padded
|
| 341 |
|
|
private_bit = (bytes >> 8) & 1 # H
|
| 342 |
|
|
mode = (bytes >> 6) & 3 # II 00 = stereo, 01 = joint stereo, 10 = dual channel, 11 = mono
|
| 343 |
|
|
mode_extension = (bytes >> 4) & 3 # JJ
|
| 344 |
|
|
copyright = (bytes >> 3) & 1 # K 00 = not copyrighted, 01 = copyrighted
|
| 345 |
|
|
original = (bytes >> 2) & 1 # L 00 = copy, 01 = original
|
| 346 |
|
|
emphasis = (bytes >> 0) & 3 # MM 00 = none, 01 = 50/15 ms, 10 = res, 11 = CCIT J.17
|
| 347 |
|
|
|
| 348 |
|
|
if mpeg_version == 0:
|
| 349 |
|
|
self.version = 2.5
|
| 350 |
|
|
elif mpeg_version == 2:
|
| 351 |
|
|
self.version = 2
|
| 352 |
|
|
elif mpeg_version == 3:
|
| 353 |
|
|
self.version = 1
|
| 354 |
|
|
else:
|
| 355 |
|
|
return
|
| 356 |
|
|
|
| 357 |
|
|
if layer > 0:
|
| 358 |
|
|
self.layer = 4 - layer
|
| 359 |
|
|
else:
|
| 360 |
|
|
return
|
| 361 |
|
|
|
| 362 |
|
|
self.protection = protection_bit
|
| 363 |
|
|
|
| 364 |
|
|
self.bitrate = _bitrates[mpeg_version & 1][self.layer - 1][bitrate]
|
| 365 |
|
|
self.samplerate = _samplerates[mpeg_version][samplerate]
|
| 366 |
|
|
|
| 367 |
|
|
if self.bitrate is None or self.samplerate is None:
|
| 368 |
|
|
return
|
| 369 |
|
|
|
| 370 |
|
|
self.padding = padding_bit
|
| 371 |
|
|
self.private = private_bit
|
| 372 |
|
|
self.mode = _modes[mode]
|
| 373 |
|
|
self.mode_extension = _mode_extensions[self.layer - 1][mode_extension]
|
| 374 |
|
|
self.copyright = copyright
|
| 375 |
|
|
self.original = original
|
| 376 |
|
|
self.emphasis = _emphases[emphasis]
|
| 377 |
|
|
|
| 378 |
|
|
if self.layer == 1:
|
| 379 |
|
|
self.framelength = (( 12 * (self.bitrate * 1000.0)/self.samplerate) + padding_bit) * 4
|
| 380 |
|
|
self.samplesperframe = 384.0
|
| 381 |
|
|
else:
|
| 382 |
|
|
self.framelength = ( 144 * (self.bitrate * 1000.0)/self.samplerate) + padding_bit
|
| 383 |
|
|
self.samplesperframe = 1152.0
|
| 384 |
|
|
self.length = int(round((self.filesize / self.framelength) * (self.samplesperframe / self.samplerate)))
|
| 385 |
|
|
|
| 386 |
|
|
self.valid = 1
|
| 387 |
|
|
|
| 388 |
|
|
def _parse_xing(self, file):
|
| 389 |
|
|
"""Parse the Xing-specific header.
|
| 390 |
|
|
|
| 391 |
|
|
For variable-bitrate (VBR) MPEG files, Xing includes a header which
|
| 392 |
|
|
can be used to approximate the (average) bitrate and the duration
|
| 393 |
|
|
of the file.
|
| 394 |
|
|
"""
|
| 395 |
|
|
file.seek(0, 0)
|
| 396 |
|
|
header = file.read(128)
|
| 397 |
|
|
|
| 398 |
|
|
i = string.find(header, 'Xing')
|
| 399 |
|
|
if i > 0:
|
| 400 |
|
|
(flags,) = struct.unpack('>i', header[i+4:i+8])
|
| 401 |
|
|
if flags & 3:
|
| 402 |
|
|
# flags says "frames" and "bytes" are present. use them.
|
| 403 |
|
|
(frames,) = struct.unpack('>i', header[i+8:i+12])
|
| 404 |
|
|
(bytes,) = struct.unpack('>i', header[i+12:i+16])
|
| 405 |
|
|
|
| 406 |
|
|
if self.samplerate:
|
| 407 |
|
|
self.length = int(round(frames * self.samplesperframe / self.samplerate))
|
| 408 |
|
|
self.bitrate = ((bytes * 8.0 / self.length) / 1000)
|
| 409 |
|
|
|
| 410 |
|
|
class MP3Info:
|
| 411 |
|
|
def __init__(self, file):
|
| 412 |
|
|
self.valid = 0
|
| 413 |
|
|
|
| 414 |
|
|
self.id3 = None
|
| 415 |
|
|
self.mpeg = None
|
| 416 |
|
|
|
| 417 |
|
|
id3 = ID3v1(file)
|
| 418 |
|
|
if id3.valid:
|
| 419 |
|
|
self.id3 = id3
|
| 420 |
|
|
|
| 421 |
|
|
id3 = ID3v2(file)
|
| 422 |
|
|
if id3.valid:
|
| 423 |
|
|
self.id3 = id3
|
| 424 |
|
|
|
| 425 |
|
|
self.mpeg = MPEG(file)
|
| 426 |
|
|
|
| 427 |
|
|
|
| 428 |
|
|
if self.id3 is None:
|
| 429 |
|
|
return
|
| 430 |
|
|
|
| 431 |
|
|
for tag in self.id3.tags.keys():
|
| 432 |
|
|
if tag == 'TT2' or tag == 'TIT2':
|
| 433 |
|
|
self.title = self.id3.tags[tag]
|
| 434 |
|
|
elif tag == 'TP1' or tag == 'TPE1':
|
| 435 |
|
|
self.artist = self.id3.tags[tag]
|
| 436 |
|
|
elif tag == 'TRK' or tag == 'TRCK':
|
| 437 |
|
|
self.track = self.id3.tags[tag]
|
| 438 |
|
|
elif tag == 'TYE' or tag == 'TYER':
|
| 439 |
|
|
self.year = self.id3.tags[tag]
|
| 440 |
|
|
elif tag == 'COM' or tag == 'COMM':
|
| 441 |
|
|
self.comment = self.id3.tags[tag]
|
| 442 |
|
|
elif tag == 'TCM':
|
| 443 |
|
|
self.composer = self.id3.tags[tag]
|
| 444 |
|
|
elif tag == 'TAL' or tag == 'TALB':
|
| 445 |
|
|
self.album = self.id3.tags[tag]
|
| 446 |
|
|
elif tag == 'TPA':
|
| 447 |
|
|
self.disc = self.id3.tags[tag]
|
| 448 |
|
|
elif tag == 'TCO' or tag == 'TCON':
|
| 449 |
|
|
self.genre = self.id3.tags[tag]
|
| 450 |
|
|
if self.genre and self.genre[0] == '(' and self.genre[-1] == ')':
|
| 451 |
|
|
try:
|
| 452 |
|
|
self.genre = _genres[int(self.genre[1:-1])]
|
| 453 |
|
|
except IndexError:
|
| 454 |
|
|
self.genre = ""
|
| 455 |
|
|
elif tag == 'TEN' or tag == 'TENC':
|
| 456 |
|
|
self.encoder = self.id3.tags[tag]
|
| 457 |
|
|
|
| 458 |
|
|
if __name__ == '__main__':
|
| 459 |
|
|
import sys
|
| 460 |
|
|
i = MP3Info(open(sys.argv[1], 'rb'))
|
| 461 |
|
|
print i.id3.tags
|
| 462 |
|
|
|